From 0818e627c14042c7b7c30726df544dd973348456 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Sat, 26 Jan 2019 00:25:02 -0600 Subject: [PATCH 01/12] Fix new indicator for sync Missing for incremental syncs --- resources/lib/library.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/library.py b/resources/lib/library.py index 959e5e7e..a69098e4 100644 --- a/resources/lib/library.py +++ b/resources/lib/library.py @@ -151,6 +151,7 @@ class Library(threading.Thread): self.worker_notify() if self.pending_refresh: + window('emby_sync.bool', True) if self.total_updates > self.progress_display: queue_size = self.worker_queue_size() From 467c474f4d30dd12c8a19f1c02ff6bc43b3aeaea Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Sat, 26 Jan 2019 02:23:44 -0600 Subject: [PATCH 02/12] Fix profile masterlock fix missing content if a master lock is set. --- resources/lib/helper/xmls.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/lib/helper/xmls.py b/resources/lib/helper/xmls.py index a1e98a7b..5ca11099 100644 --- a/resources/lib/helper/xmls.py +++ b/resources/lib/helper/xmls.py @@ -36,20 +36,25 @@ def sources(): etree.SubElement(files, 'default', attrib={'pathversion': "1"}) video = xml.find('video') - count = 2 + count_http = 1 + count_smb = 1 for source in xml.findall('.//path'): if source.text == 'smb://': - count -= 1 + count_smb -= 1 + elif source.text == 'http://': + count_http -= 1 - if count == 0: + if not count_http and not count_smb: break else: - for i in range(0, count): - source = etree.SubElement(video, 'source') - etree.SubElement(source, 'name').text = "Emby" - etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" - etree.SubElement(source, 'allowsharing').text = "true" + for protocol in ('smb://', 'http://'): + if (protocol == 'smb://' and count_smb > 0) or (protocol == 'http://' and count_http > 0): + + source = etree.SubElement(video, 'source') + etree.SubElement(source, 'name').text = "Emby" + etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = protocol + etree.SubElement(source, 'allowsharing').text = "true" try: files = xml.find('files') From e5aea3fbdc2cdbad8e98dbaf4bdac2fad9a57dbf Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Sun, 27 Jan 2019 09:17:40 -0600 Subject: [PATCH 03/12] Allow homevideos under videos Pictures won't display correctly, but restore previous behavior --- resources/lib/entrypoint/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/entrypoint/default.py b/resources/lib/entrypoint/default.py index eea1511b..3c46a911 100644 --- a/resources/lib/entrypoint/default.py +++ b/resources/lib/entrypoint/default.py @@ -166,7 +166,7 @@ def listing(): if path: if xbmc.getCondVisibility('Window.IsActive(Pictures)') and node in ('photos', 'homevideos'): directory(label, path, artwork=artwork) - elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'homevideos', 'music', 'audiobooks'): + elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'music', 'audiobooks'): directory(label, path, artwork=artwork, context=context) elif xbmc.getCondVisibility('Window.IsActive(Music)') and node in ('music'): directory(label, path, artwork=artwork, context=context) From 3ea6890c3463a987d6ad9fed46f743e45b1843a5 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Sun, 27 Jan 2019 14:49:35 -0600 Subject: [PATCH 04/12] Filter webservice requests Only proceed if the id is a number. --- resources/lib/webservice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 5ef21824..34826af0 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -164,6 +164,10 @@ class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): ''' try: params = self.get_params() + + if not params.get('Id').isdigit(): + raise IndexError("Incorrect Id format: %s" % params.get('Id')) + LOG.info("Webservice called with params: %s", params) path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" @@ -176,6 +180,10 @@ class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): if not headers_only: self.wfile.write(path) + except IndexError as error: + + LOG.error(error) + self.send_error(403) except Exception as error: From b2bc90cb06efb9f6c2996427be1840510ca366e5 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Wed, 30 Jan 2019 06:42:06 -0600 Subject: [PATCH 05/12] Move libraries import --- context.py | 2 + context_play.py | 2 + default.py | 2 + .../dateutil/test => libraries}/__init__.py | 0 .../libraries => libraries}/dateutil/LICENSE | 0 .../lib/libraries => libraries}/dateutil/NEWS | 0 .../dateutil/README.rst | 0 .../dateutil/__init__.py | 2 +- .../dateutil/_common.py | 0 .../dateutil/easter.py | 0 .../dateutil/parser/__init__.py | 10 +- .../dateutil/parser/_parser.py | 8 +- .../dateutil/parser/isoparser.py | 4 +- .../dateutil/relativedelta.py | 0 .../libraries => libraries}/dateutil/rrule.py | 0 .../dateutil/test}/__init__.py | 0 .../dateutil/test/_common.py | 0 .../test/property/test_isoparse_prop.py | 0 .../test/property/test_parser_prop.py | 0 .../dateutil/test/test_easter.py | 0 .../dateutil/test/test_import_star.py | 0 .../dateutil/test/test_imports.py | 0 .../dateutil/test/test_internals.py | 0 .../dateutil/test/test_isoparser.py | 0 .../dateutil/test/test_parser.py | 0 .../dateutil/test/test_relativedelta.py | 0 .../dateutil/test/test_rrule.py | 0 .../dateutil/test/test_tz.py | 0 .../dateutil/test/test_utils.py | 0 .../dateutil/tz/__init__.py | 0 .../dateutil/tz/_common.py | 2 +- .../dateutil/tz/_factories.py | 0 .../libraries => libraries}/dateutil/tz/tz.py | 18 +- .../dateutil/tz/win.py | 0 .../libraries => libraries}/dateutil/tzwin.py | 0 .../libraries => libraries}/dateutil/utils.py | 0 .../dateutil/zoneinfo/__init__.py | 0 .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin .../dateutil/zoneinfo/rebuild.py | 0 .../requests/__init__.py | 0 .../requests/adapters.py | 0 .../libraries => libraries}/requests/api.py | 0 .../libraries => libraries}/requests/auth.py | 0 .../requests/cacert.pem | 0 .../libraries => libraries}/requests/certs.py | 0 .../requests/compat.py | 0 .../requests/cookies.py | 0 .../requests/exceptions.py | 0 .../libraries => libraries}/requests/hooks.py | 0 .../requests/models.py | 0 .../requests/packages/README.rst | 0 .../requests/packages/__init__.py | 0 .../requests/packages/chardet/__init__.py | 0 .../requests/packages/chardet/big5freq.py | 0 .../requests/packages/chardet/big5prober.py | 0 .../requests/packages/chardet/chardetect.py | 0 .../packages/chardet/chardistribution.py | 0 .../packages/chardet/charsetgroupprober.py | 0 .../packages/chardet/charsetprober.py | 0 .../packages/chardet/codingstatemachine.py | 0 .../requests/packages/chardet/compat.py | 0 .../requests/packages/chardet/constants.py | 0 .../requests/packages/chardet/cp949prober.py | 0 .../requests/packages/chardet/escprober.py | 0 .../requests/packages/chardet/escsm.py | 0 .../requests/packages/chardet/eucjpprober.py | 0 .../requests/packages/chardet/euckrfreq.py | 0 .../requests/packages/chardet/euckrprober.py | 0 .../requests/packages/chardet/euctwfreq.py | 0 .../requests/packages/chardet/euctwprober.py | 0 .../requests/packages/chardet/gb2312freq.py | 0 .../requests/packages/chardet/gb2312prober.py | 0 .../requests/packages/chardet/hebrewprober.py | 0 .../requests/packages/chardet/jisfreq.py | 0 .../requests/packages/chardet/jpcntx.py | 0 .../packages/chardet/langbulgarianmodel.py | 0 .../packages/chardet/langcyrillicmodel.py | 0 .../packages/chardet/langgreekmodel.py | 0 .../packages/chardet/langhebrewmodel.py | 0 .../packages/chardet/langhungarianmodel.py | 0 .../packages/chardet/langthaimodel.py | 0 .../requests/packages/chardet/latin1prober.py | 0 .../packages/chardet/mbcharsetprober.py | 0 .../packages/chardet/mbcsgroupprober.py | 0 .../requests/packages/chardet/mbcssm.py | 0 .../packages/chardet/sbcharsetprober.py | 0 .../packages/chardet/sbcsgroupprober.py | 0 .../requests/packages/chardet/sjisprober.py | 0 .../packages/chardet/universaldetector.py | 0 .../requests/packages/chardet/utf8prober.py | 0 .../requests/packages/urllib3/__init__.py | 0 .../requests/packages/urllib3/_collections.py | 0 .../requests/packages/urllib3/connection.py | 0 .../packages/urllib3/connectionpool.py | 0 .../packages/urllib3/contrib/__init__.py | 0 .../packages/urllib3/contrib/appengine.py | 0 .../packages/urllib3/contrib/ntlmpool.py | 0 .../packages/urllib3/contrib/pyopenssl.py | 0 .../requests/packages/urllib3/exceptions.py | 0 .../requests/packages/urllib3/fields.py | 0 .../requests/packages/urllib3/filepost.py | 0 .../packages/urllib3/packages/__init__.py | 0 .../packages/urllib3/packages/ordered_dict.py | 0 .../requests/packages/urllib3/packages/six.py | 0 .../packages/ssl_match_hostname/__init__.py | 0 .../ssl_match_hostname/_implementation.py | 0 .../requests/packages/urllib3/poolmanager.py | 0 .../requests/packages/urllib3/request.py | 0 .../requests/packages/urllib3/response.py | 0 .../packages/urllib3/util/__init__.py | 0 .../packages/urllib3/util/connection.py | 0 .../requests/packages/urllib3/util/request.py | 0 .../packages/urllib3/util/response.py | 0 .../requests/packages/urllib3/util/retry.py | 0 .../requests/packages/urllib3/util/ssl_.py | 0 .../requests/packages/urllib3/util/timeout.py | 0 .../requests/packages/urllib3/util/url.py | 0 .../requests/sessions.py | 0 .../requests/status_codes.py | 0 .../requests/structures.py | 0 .../libraries => libraries}/requests/utils.py | 0 .../libraries/dateutil => libraries}/six.py | 0 resources/lib/downloader.py | 2 +- resources/lib/emby/core/http.py | 2 +- resources/lib/entrypoint/service.py | 2 +- resources/lib/helper/playutils.py | 2 +- resources/lib/helper/utils.py | 3 +- resources/lib/libraries/__init__.py | 2 - resources/lib/libraries/mutagen/__init__.py | 43 - .../__pycache__/__init__.cpython-35.pyc | Bin 914 -> 0 bytes .../__pycache__/_compat.cpython-35.pyc | Bin 2754 -> 0 bytes .../__pycache__/_constants.cpython-35.pyc | Bin 3099 -> 0 bytes .../mutagen/__pycache__/_file.cpython-35.pyc | Bin 7763 -> 0 bytes .../__pycache__/_mp3util.cpython-35.pyc | Bin 8626 -> 0 bytes .../mutagen/__pycache__/_tags.cpython-35.pyc | Bin 2970 -> 0 bytes .../__pycache__/_toolsutil.cpython-35.pyc | Bin 6102 -> 0 bytes .../mutagen/__pycache__/_util.cpython-35.pyc | Bin 17420 -> 0 bytes .../__pycache__/_vorbis.cpython-35.pyc | Bin 10304 -> 0 bytes .../mutagen/__pycache__/aac.cpython-35.pyc | Bin 10050 -> 0 bytes .../mutagen/__pycache__/aiff.cpython-35.pyc | Bin 10216 -> 0 bytes .../mutagen/__pycache__/apev2.cpython-35.pyc | Bin 20902 -> 0 bytes .../__pycache__/easyid3.cpython-35.pyc | Bin 17989 -> 0 bytes .../__pycache__/easymp4.cpython-35.pyc | Bin 10727 -> 0 bytes .../mutagen/__pycache__/flac.cpython-35.pyc | Bin 28214 -> 0 bytes .../mutagen/__pycache__/m4a.cpython-35.pyc | Bin 3561 -> 0 bytes .../__pycache__/monkeysaudio.cpython-35.pyc | Bin 2904 -> 0 bytes .../mutagen/__pycache__/mp3.cpython-35.pyc | Bin 9514 -> 0 bytes .../__pycache__/musepack.cpython-35.pyc | Bin 8017 -> 0 bytes .../mutagen/__pycache__/ogg.cpython-35.pyc | Bin 16965 -> 0 bytes .../__pycache__/oggflac.cpython-35.pyc | Bin 4862 -> 0 bytes .../__pycache__/oggopus.cpython-35.pyc | Bin 4763 -> 0 bytes .../__pycache__/oggspeex.cpython-35.pyc | Bin 4600 -> 0 bytes .../__pycache__/oggtheora.cpython-35.pyc | Bin 4781 -> 0 bytes .../__pycache__/oggvorbis.cpython-35.pyc | Bin 4602 -> 0 bytes .../__pycache__/optimfrog.cpython-35.pyc | Bin 2545 -> 0 bytes .../__pycache__/trueaudio.cpython-35.pyc | Bin 2924 -> 0 bytes .../__pycache__/wavpack.cpython-35.pyc | Bin 3738 -> 0 bytes resources/lib/libraries/mutagen/_compat.py | 86 - resources/lib/libraries/mutagen/_constants.py | 199 -- resources/lib/libraries/mutagen/_file.py | 253 --- resources/lib/libraries/mutagen/_mp3util.py | 420 ---- resources/lib/libraries/mutagen/_tags.py | 101 - resources/lib/libraries/mutagen/_toolsutil.py | 231 -- resources/lib/libraries/mutagen/_util.py | 550 ----- resources/lib/libraries/mutagen/_vorbis.py | 330 --- resources/lib/libraries/mutagen/aac.py | 410 ---- resources/lib/libraries/mutagen/aiff.py | 357 --- resources/lib/libraries/mutagen/apev2.py | 710 ------ .../lib/libraries/mutagen/asf/__init__.py | 319 --- .../asf/__pycache__/__init__.cpython-35.pyc | Bin 8567 -> 0 bytes .../asf/__pycache__/_attrs.cpython-35.pyc | Bin 15131 -> 0 bytes .../asf/__pycache__/_objects.cpython-35.pyc | Bin 15903 -> 0 bytes .../asf/__pycache__/_util.cpython-35.pyc | Bin 10603 -> 0 bytes resources/lib/libraries/mutagen/asf/_attrs.py | 438 ---- .../lib/libraries/mutagen/asf/_objects.py | 437 ---- resources/lib/libraries/mutagen/asf/_util.py | 315 --- resources/lib/libraries/mutagen/easyid3.py | 534 ----- resources/lib/libraries/mutagen/easymp4.py | 285 --- resources/lib/libraries/mutagen/flac.py | 876 -------- .../lib/libraries/mutagen/id3/__init__.py | 1093 ---------- .../id3/__pycache__/__init__.cpython-35.pyc | Bin 27785 -> 0 bytes .../id3/__pycache__/_frames.cpython-35.pyc | Bin 64560 -> 0 bytes .../id3/__pycache__/_specs.cpython-35.pyc | Bin 22816 -> 0 bytes .../id3/__pycache__/_util.cpython-35.pyc | Bin 4933 -> 0 bytes .../lib/libraries/mutagen/id3/_frames.py | 1925 ----------------- resources/lib/libraries/mutagen/id3/_specs.py | 635 ------ resources/lib/libraries/mutagen/id3/_util.py | 167 -- resources/lib/libraries/mutagen/m4a.py | 101 - .../lib/libraries/mutagen/monkeysaudio.py | 86 - resources/lib/libraries/mutagen/mp3.py | 362 ---- .../lib/libraries/mutagen/mp4/__init__.py | 1010 --------- .../mp4/__pycache__/__init__.cpython-35.pyc | Bin 31145 -> 0 bytes .../mp4/__pycache__/_as_entry.cpython-35.pyc | Bin 14269 -> 0 bytes .../mp4/__pycache__/_atom.cpython-35.pyc | Bin 6248 -> 0 bytes .../mp4/__pycache__/_util.cpython-35.pyc | Bin 590 -> 0 bytes .../lib/libraries/mutagen/mp4/_as_entry.py | 542 ----- resources/lib/libraries/mutagen/mp4/_atom.py | 194 -- resources/lib/libraries/mutagen/mp4/_util.py | 21 - resources/lib/libraries/mutagen/musepack.py | 270 --- resources/lib/libraries/mutagen/ogg.py | 548 ----- resources/lib/libraries/mutagen/oggflac.py | 161 -- resources/lib/libraries/mutagen/oggopus.py | 158 -- resources/lib/libraries/mutagen/oggspeex.py | 154 -- resources/lib/libraries/mutagen/oggtheora.py | 148 -- resources/lib/libraries/mutagen/oggvorbis.py | 159 -- resources/lib/libraries/mutagen/optimfrog.py | 74 - resources/lib/libraries/mutagen/trueaudio.py | 84 - resources/lib/libraries/mutagen/wavpack.py | 125 -- service.py | 3 + 209 files changed, 36 insertions(+), 14941 deletions(-) rename {resources/lib/libraries/dateutil/test => libraries}/__init__.py (100%) rename {resources/lib/libraries => libraries}/dateutil/LICENSE (100%) rename {resources/lib/libraries => libraries}/dateutil/NEWS (100%) rename {resources/lib/libraries => libraries}/dateutil/README.rst (100%) rename {resources/lib/libraries => libraries}/dateutil/__init__.py (78%) rename {resources/lib/libraries => libraries}/dateutil/_common.py (100%) rename {resources/lib/libraries => libraries}/dateutil/easter.py (100%) rename {resources/lib/libraries => libraries}/dateutil/parser/__init__.py (87%) rename {resources/lib/libraries => libraries}/dateutil/parser/_parser.py (99%) rename {resources/lib/libraries => libraries}/dateutil/parser/isoparser.py (99%) rename {resources/lib/libraries => libraries}/dateutil/relativedelta.py (100%) rename {resources/lib/libraries => libraries}/dateutil/rrule.py (100%) rename {resources/lib/libraries/requests/packages/urllib3/contrib => libraries/dateutil/test}/__init__.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/_common.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/property/test_isoparse_prop.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/property/test_parser_prop.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_easter.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_import_star.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_imports.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_internals.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_isoparser.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_parser.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_relativedelta.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_rrule.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_tz.py (100%) rename {resources/lib/libraries => libraries}/dateutil/test/test_utils.py (100%) rename {resources/lib/libraries => libraries}/dateutil/tz/__init__.py (100%) rename {resources/lib/libraries => libraries}/dateutil/tz/_common.py (99%) rename {resources/lib/libraries => libraries}/dateutil/tz/_factories.py (100%) rename {resources/lib/libraries => libraries}/dateutil/tz/tz.py (99%) rename {resources/lib/libraries => libraries}/dateutil/tz/win.py (100%) rename {resources/lib/libraries => libraries}/dateutil/tzwin.py (100%) rename {resources/lib/libraries => libraries}/dateutil/utils.py (100%) rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/__init__.py (100%) rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz (100%) rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/rebuild.py (100%) rename {resources/lib/libraries => libraries}/requests/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/adapters.py (100%) rename {resources/lib/libraries => libraries}/requests/api.py (100%) rename {resources/lib/libraries => libraries}/requests/auth.py (100%) rename {resources/lib/libraries => libraries}/requests/cacert.pem (100%) rename {resources/lib/libraries => libraries}/requests/certs.py (100%) rename {resources/lib/libraries => libraries}/requests/compat.py (100%) rename {resources/lib/libraries => libraries}/requests/cookies.py (100%) rename {resources/lib/libraries => libraries}/requests/exceptions.py (100%) rename {resources/lib/libraries => libraries}/requests/hooks.py (100%) rename {resources/lib/libraries => libraries}/requests/models.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/README.rst (100%) rename {resources/lib/libraries => libraries}/requests/packages/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/big5freq.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/big5prober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/chardetect.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/chardistribution.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/charsetgroupprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/charsetprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/codingstatemachine.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/compat.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/constants.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/cp949prober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/escprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/escsm.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/eucjpprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/euckrfreq.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/euckrprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/euctwfreq.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/euctwprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/gb2312freq.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/gb2312prober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/hebrewprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/jisfreq.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/jpcntx.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langbulgarianmodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langcyrillicmodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langgreekmodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langhebrewmodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langhungarianmodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/langthaimodel.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/latin1prober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcharsetprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcsgroupprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcssm.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/sbcharsetprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/sbcsgroupprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/sjisprober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/universaldetector.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/chardet/utf8prober.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/_collections.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/connection.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/connectionpool.py (100%) create mode 100644 libraries/requests/packages/urllib3/contrib/__init__.py rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/appengine.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/ntlmpool.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/pyopenssl.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/exceptions.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/fields.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/filepost.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ordered_dict.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/six.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/poolmanager.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/request.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/response.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/__init__.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/connection.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/request.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/response.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/retry.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/ssl_.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/timeout.py (100%) rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/url.py (100%) rename {resources/lib/libraries => libraries}/requests/sessions.py (100%) rename {resources/lib/libraries => libraries}/requests/status_codes.py (100%) rename {resources/lib/libraries => libraries}/requests/structures.py (100%) rename {resources/lib/libraries => libraries}/requests/utils.py (100%) rename {resources/lib/libraries/dateutil => libraries}/six.py (100%) delete mode 100644 resources/lib/libraries/__init__.py delete mode 100644 resources/lib/libraries/mutagen/__init__.py delete mode 100644 resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/_compat.py delete mode 100644 resources/lib/libraries/mutagen/_constants.py delete mode 100644 resources/lib/libraries/mutagen/_file.py delete mode 100644 resources/lib/libraries/mutagen/_mp3util.py delete mode 100644 resources/lib/libraries/mutagen/_tags.py delete mode 100644 resources/lib/libraries/mutagen/_toolsutil.py delete mode 100644 resources/lib/libraries/mutagen/_util.py delete mode 100644 resources/lib/libraries/mutagen/_vorbis.py delete mode 100644 resources/lib/libraries/mutagen/aac.py delete mode 100644 resources/lib/libraries/mutagen/aiff.py delete mode 100644 resources/lib/libraries/mutagen/apev2.py delete mode 100644 resources/lib/libraries/mutagen/asf/__init__.py delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/asf/_attrs.py delete mode 100644 resources/lib/libraries/mutagen/asf/_objects.py delete mode 100644 resources/lib/libraries/mutagen/asf/_util.py delete mode 100644 resources/lib/libraries/mutagen/easyid3.py delete mode 100644 resources/lib/libraries/mutagen/easymp4.py delete mode 100644 resources/lib/libraries/mutagen/flac.py delete mode 100644 resources/lib/libraries/mutagen/id3/__init__.py delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/id3/_frames.py delete mode 100644 resources/lib/libraries/mutagen/id3/_specs.py delete mode 100644 resources/lib/libraries/mutagen/id3/_util.py delete mode 100644 resources/lib/libraries/mutagen/m4a.py delete mode 100644 resources/lib/libraries/mutagen/monkeysaudio.py delete mode 100644 resources/lib/libraries/mutagen/mp3.py delete mode 100644 resources/lib/libraries/mutagen/mp4/__init__.py delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc delete mode 100644 resources/lib/libraries/mutagen/mp4/_as_entry.py delete mode 100644 resources/lib/libraries/mutagen/mp4/_atom.py delete mode 100644 resources/lib/libraries/mutagen/mp4/_util.py delete mode 100644 resources/lib/libraries/mutagen/musepack.py delete mode 100644 resources/lib/libraries/mutagen/ogg.py delete mode 100644 resources/lib/libraries/mutagen/oggflac.py delete mode 100644 resources/lib/libraries/mutagen/oggopus.py delete mode 100644 resources/lib/libraries/mutagen/oggspeex.py delete mode 100644 resources/lib/libraries/mutagen/oggtheora.py delete mode 100644 resources/lib/libraries/mutagen/oggvorbis.py delete mode 100644 resources/lib/libraries/mutagen/optimfrog.py delete mode 100644 resources/lib/libraries/mutagen/trueaudio.py delete mode 100644 resources/lib/libraries/mutagen/wavpack.py diff --git a/context.py b/context.py index 15256491..b2143b01 100644 --- a/context.py +++ b/context.py @@ -13,11 +13,13 @@ import xbmcaddon __addon__ = xbmcaddon.Addon(id='plugin.video.emby') __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'librarie')).decode('utf-8') __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') sys.path.insert(0, __cache__) sys.path.insert(0, __pcache__) +sys.path.insert(0, __libraries__) sys.path.append(__base__) ################################################################################################# diff --git a/context_play.py b/context_play.py index 660fb468..58ee93fb 100644 --- a/context_play.py +++ b/context_play.py @@ -13,11 +13,13 @@ import xbmcaddon __addon__ = xbmcaddon.Addon(id='plugin.video.emby') __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') sys.path.insert(0, __cache__) sys.path.insert(0, __pcache__) +sys.path.insert(0, __libraries__) sys.path.append(__base__) ################################################################################################# diff --git a/default.py b/default.py index 0305ee81..6f1b05a0 100644 --- a/default.py +++ b/default.py @@ -13,11 +13,13 @@ import xbmcaddon __addon__ = xbmcaddon.Addon(id='plugin.video.emby') __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') sys.path.insert(0, __cache__) sys.path.insert(0, __pcache__) +sys.path.insert(0, __libraries__) sys.path.append(__base__) ################################################################################################# diff --git a/resources/lib/libraries/dateutil/test/__init__.py b/libraries/__init__.py similarity index 100% rename from resources/lib/libraries/dateutil/test/__init__.py rename to libraries/__init__.py diff --git a/resources/lib/libraries/dateutil/LICENSE b/libraries/dateutil/LICENSE similarity index 100% rename from resources/lib/libraries/dateutil/LICENSE rename to libraries/dateutil/LICENSE diff --git a/resources/lib/libraries/dateutil/NEWS b/libraries/dateutil/NEWS similarity index 100% rename from resources/lib/libraries/dateutil/NEWS rename to libraries/dateutil/NEWS diff --git a/resources/lib/libraries/dateutil/README.rst b/libraries/dateutil/README.rst similarity index 100% rename from resources/lib/libraries/dateutil/README.rst rename to libraries/dateutil/README.rst diff --git a/resources/lib/libraries/dateutil/__init__.py b/libraries/dateutil/__init__.py similarity index 78% rename from resources/lib/libraries/dateutil/__init__.py rename to libraries/dateutil/__init__.py index 10ad93fa..a29ffaa9 100644 --- a/resources/lib/libraries/dateutil/__init__.py +++ b/libraries/dateutil/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- try: - from ._version import version as __version__ + from _version import version as __version__ except ImportError: __version__ = 'unknown' diff --git a/resources/lib/libraries/dateutil/_common.py b/libraries/dateutil/_common.py similarity index 100% rename from resources/lib/libraries/dateutil/_common.py rename to libraries/dateutil/_common.py diff --git a/resources/lib/libraries/dateutil/easter.py b/libraries/dateutil/easter.py similarity index 100% rename from resources/lib/libraries/dateutil/easter.py rename to libraries/dateutil/easter.py diff --git a/resources/lib/libraries/dateutil/parser/__init__.py b/libraries/dateutil/parser/__init__.py similarity index 87% rename from resources/lib/libraries/dateutil/parser/__init__.py rename to libraries/dateutil/parser/__init__.py index 216762c0..2d20777c 100644 --- a/resources/lib/libraries/dateutil/parser/__init__.py +++ b/libraries/dateutil/parser/__init__.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from ._parser import parse, parser, parserinfo -from ._parser import DEFAULTPARSER, DEFAULTTZPARSER -from ._parser import UnknownTimezoneWarning +from _parser import parse, parser, parserinfo +from _parser import DEFAULTPARSER, DEFAULTTZPARSER +from _parser import UnknownTimezoneWarning -from ._parser import __doc__ +from _parser import __doc__ -from .isoparser import isoparser, isoparse +from isoparser import isoparser, isoparse __all__ = ['parse', 'parser', 'parserinfo', 'isoparse', 'isoparser', diff --git a/resources/lib/libraries/dateutil/parser/_parser.py b/libraries/dateutil/parser/_parser.py similarity index 99% rename from resources/lib/libraries/dateutil/parser/_parser.py rename to libraries/dateutil/parser/_parser.py index e8a522c9..66940802 100644 --- a/resources/lib/libraries/dateutil/parser/_parser.py +++ b/libraries/dateutil/parser/_parser.py @@ -39,15 +39,15 @@ import warnings from calendar import monthrange from io import StringIO -from .. import six -from ..six import binary_type, integer_types, text_type +import six +from six import binary_type, integer_types, text_type from decimal import Decimal from warnings import warn -from .. import relativedelta -from .. import tz +from dateutil import relativedelta +from dateutil import tz __all__ = ["parse", "parserinfo"] diff --git a/resources/lib/libraries/dateutil/parser/isoparser.py b/libraries/dateutil/parser/isoparser.py similarity index 99% rename from resources/lib/libraries/dateutil/parser/isoparser.py rename to libraries/dateutil/parser/isoparser.py index b63ef712..cd27f93d 100644 --- a/resources/lib/libraries/dateutil/parser/isoparser.py +++ b/libraries/dateutil/parser/isoparser.py @@ -9,12 +9,12 @@ ISO-8601 specification. """ from datetime import datetime, timedelta, time, date import calendar -from .. import tz +from dateutil import tz from functools import wraps import re -from .. import six +import six __all__ = ["isoparse", "isoparser"] diff --git a/resources/lib/libraries/dateutil/relativedelta.py b/libraries/dateutil/relativedelta.py similarity index 100% rename from resources/lib/libraries/dateutil/relativedelta.py rename to libraries/dateutil/relativedelta.py diff --git a/resources/lib/libraries/dateutil/rrule.py b/libraries/dateutil/rrule.py similarity index 100% rename from resources/lib/libraries/dateutil/rrule.py rename to libraries/dateutil/rrule.py diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py b/libraries/dateutil/test/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py rename to libraries/dateutil/test/__init__.py diff --git a/resources/lib/libraries/dateutil/test/_common.py b/libraries/dateutil/test/_common.py similarity index 100% rename from resources/lib/libraries/dateutil/test/_common.py rename to libraries/dateutil/test/_common.py diff --git a/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py b/libraries/dateutil/test/property/test_isoparse_prop.py similarity index 100% rename from resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py rename to libraries/dateutil/test/property/test_isoparse_prop.py diff --git a/resources/lib/libraries/dateutil/test/property/test_parser_prop.py b/libraries/dateutil/test/property/test_parser_prop.py similarity index 100% rename from resources/lib/libraries/dateutil/test/property/test_parser_prop.py rename to libraries/dateutil/test/property/test_parser_prop.py diff --git a/resources/lib/libraries/dateutil/test/test_easter.py b/libraries/dateutil/test/test_easter.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_easter.py rename to libraries/dateutil/test/test_easter.py diff --git a/resources/lib/libraries/dateutil/test/test_import_star.py b/libraries/dateutil/test/test_import_star.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_import_star.py rename to libraries/dateutil/test/test_import_star.py diff --git a/resources/lib/libraries/dateutil/test/test_imports.py b/libraries/dateutil/test/test_imports.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_imports.py rename to libraries/dateutil/test/test_imports.py diff --git a/resources/lib/libraries/dateutil/test/test_internals.py b/libraries/dateutil/test/test_internals.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_internals.py rename to libraries/dateutil/test/test_internals.py diff --git a/resources/lib/libraries/dateutil/test/test_isoparser.py b/libraries/dateutil/test/test_isoparser.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_isoparser.py rename to libraries/dateutil/test/test_isoparser.py diff --git a/resources/lib/libraries/dateutil/test/test_parser.py b/libraries/dateutil/test/test_parser.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_parser.py rename to libraries/dateutil/test/test_parser.py diff --git a/resources/lib/libraries/dateutil/test/test_relativedelta.py b/libraries/dateutil/test/test_relativedelta.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_relativedelta.py rename to libraries/dateutil/test/test_relativedelta.py diff --git a/resources/lib/libraries/dateutil/test/test_rrule.py b/libraries/dateutil/test/test_rrule.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_rrule.py rename to libraries/dateutil/test/test_rrule.py diff --git a/resources/lib/libraries/dateutil/test/test_tz.py b/libraries/dateutil/test/test_tz.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_tz.py rename to libraries/dateutil/test/test_tz.py diff --git a/resources/lib/libraries/dateutil/test/test_utils.py b/libraries/dateutil/test/test_utils.py similarity index 100% rename from resources/lib/libraries/dateutil/test/test_utils.py rename to libraries/dateutil/test/test_utils.py diff --git a/resources/lib/libraries/dateutil/tz/__init__.py b/libraries/dateutil/tz/__init__.py similarity index 100% rename from resources/lib/libraries/dateutil/tz/__init__.py rename to libraries/dateutil/tz/__init__.py diff --git a/resources/lib/libraries/dateutil/tz/_common.py b/libraries/dateutil/tz/_common.py similarity index 99% rename from resources/lib/libraries/dateutil/tz/_common.py rename to libraries/dateutil/tz/_common.py index e2e66e7b..ccabb7da 100644 --- a/resources/lib/libraries/dateutil/tz/_common.py +++ b/libraries/dateutil/tz/_common.py @@ -1,4 +1,4 @@ -from ..six import PY3 +from six import PY3 from functools import wraps diff --git a/resources/lib/libraries/dateutil/tz/_factories.py b/libraries/dateutil/tz/_factories.py similarity index 100% rename from resources/lib/libraries/dateutil/tz/_factories.py rename to libraries/dateutil/tz/_factories.py diff --git a/resources/lib/libraries/dateutil/tz/tz.py b/libraries/dateutil/tz/tz.py similarity index 99% rename from resources/lib/libraries/dateutil/tz/tz.py rename to libraries/dateutil/tz/tz.py index 4c23242a..6fcfce86 100644 --- a/resources/lib/libraries/dateutil/tz/tz.py +++ b/libraries/dateutil/tz/tz.py @@ -14,17 +14,17 @@ import sys import os import bisect -from .. import six -from ..six import string_types -from ..six.moves import _thread -from ._common import tzname_in_python2, _tzinfo -from ._common import tzrangebase, enfold -from ._common import _validate_fromutc_inputs +import six +from six import string_types +from six.moves import _thread +from _common import tzname_in_python2, _tzinfo +from _common import tzrangebase, enfold +from _common import _validate_fromutc_inputs -from ._factories import _TzSingleton, _TzOffsetFactory -from ._factories import _TzStrFactory +from _factories import _TzSingleton, _TzOffsetFactory +from _factories import _TzStrFactory try: - from .win import tzwin, tzwinlocal + from win import tzwin, tzwinlocal except ImportError: tzwin = tzwinlocal = None diff --git a/resources/lib/libraries/dateutil/tz/win.py b/libraries/dateutil/tz/win.py similarity index 100% rename from resources/lib/libraries/dateutil/tz/win.py rename to libraries/dateutil/tz/win.py diff --git a/resources/lib/libraries/dateutil/tzwin.py b/libraries/dateutil/tzwin.py similarity index 100% rename from resources/lib/libraries/dateutil/tzwin.py rename to libraries/dateutil/tzwin.py diff --git a/resources/lib/libraries/dateutil/utils.py b/libraries/dateutil/utils.py similarity index 100% rename from resources/lib/libraries/dateutil/utils.py rename to libraries/dateutil/utils.py diff --git a/resources/lib/libraries/dateutil/zoneinfo/__init__.py b/libraries/dateutil/zoneinfo/__init__.py similarity index 100% rename from resources/lib/libraries/dateutil/zoneinfo/__init__.py rename to libraries/dateutil/zoneinfo/__init__.py diff --git a/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz similarity index 100% rename from resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz rename to libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz diff --git a/resources/lib/libraries/dateutil/zoneinfo/rebuild.py b/libraries/dateutil/zoneinfo/rebuild.py similarity index 100% rename from resources/lib/libraries/dateutil/zoneinfo/rebuild.py rename to libraries/dateutil/zoneinfo/rebuild.py diff --git a/resources/lib/libraries/requests/__init__.py b/libraries/requests/__init__.py similarity index 100% rename from resources/lib/libraries/requests/__init__.py rename to libraries/requests/__init__.py diff --git a/resources/lib/libraries/requests/adapters.py b/libraries/requests/adapters.py similarity index 100% rename from resources/lib/libraries/requests/adapters.py rename to libraries/requests/adapters.py diff --git a/resources/lib/libraries/requests/api.py b/libraries/requests/api.py similarity index 100% rename from resources/lib/libraries/requests/api.py rename to libraries/requests/api.py diff --git a/resources/lib/libraries/requests/auth.py b/libraries/requests/auth.py similarity index 100% rename from resources/lib/libraries/requests/auth.py rename to libraries/requests/auth.py diff --git a/resources/lib/libraries/requests/cacert.pem b/libraries/requests/cacert.pem similarity index 100% rename from resources/lib/libraries/requests/cacert.pem rename to libraries/requests/cacert.pem diff --git a/resources/lib/libraries/requests/certs.py b/libraries/requests/certs.py similarity index 100% rename from resources/lib/libraries/requests/certs.py rename to libraries/requests/certs.py diff --git a/resources/lib/libraries/requests/compat.py b/libraries/requests/compat.py similarity index 100% rename from resources/lib/libraries/requests/compat.py rename to libraries/requests/compat.py diff --git a/resources/lib/libraries/requests/cookies.py b/libraries/requests/cookies.py similarity index 100% rename from resources/lib/libraries/requests/cookies.py rename to libraries/requests/cookies.py diff --git a/resources/lib/libraries/requests/exceptions.py b/libraries/requests/exceptions.py similarity index 100% rename from resources/lib/libraries/requests/exceptions.py rename to libraries/requests/exceptions.py diff --git a/resources/lib/libraries/requests/hooks.py b/libraries/requests/hooks.py similarity index 100% rename from resources/lib/libraries/requests/hooks.py rename to libraries/requests/hooks.py diff --git a/resources/lib/libraries/requests/models.py b/libraries/requests/models.py similarity index 100% rename from resources/lib/libraries/requests/models.py rename to libraries/requests/models.py diff --git a/resources/lib/libraries/requests/packages/README.rst b/libraries/requests/packages/README.rst similarity index 100% rename from resources/lib/libraries/requests/packages/README.rst rename to libraries/requests/packages/README.rst diff --git a/resources/lib/libraries/requests/packages/__init__.py b/libraries/requests/packages/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/__init__.py rename to libraries/requests/packages/__init__.py diff --git a/resources/lib/libraries/requests/packages/chardet/__init__.py b/libraries/requests/packages/chardet/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/__init__.py rename to libraries/requests/packages/chardet/__init__.py diff --git a/resources/lib/libraries/requests/packages/chardet/big5freq.py b/libraries/requests/packages/chardet/big5freq.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/big5freq.py rename to libraries/requests/packages/chardet/big5freq.py diff --git a/resources/lib/libraries/requests/packages/chardet/big5prober.py b/libraries/requests/packages/chardet/big5prober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/big5prober.py rename to libraries/requests/packages/chardet/big5prober.py diff --git a/resources/lib/libraries/requests/packages/chardet/chardetect.py b/libraries/requests/packages/chardet/chardetect.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/chardetect.py rename to libraries/requests/packages/chardet/chardetect.py diff --git a/resources/lib/libraries/requests/packages/chardet/chardistribution.py b/libraries/requests/packages/chardet/chardistribution.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/chardistribution.py rename to libraries/requests/packages/chardet/chardistribution.py diff --git a/resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py b/libraries/requests/packages/chardet/charsetgroupprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py rename to libraries/requests/packages/chardet/charsetgroupprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/charsetprober.py b/libraries/requests/packages/chardet/charsetprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/charsetprober.py rename to libraries/requests/packages/chardet/charsetprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/codingstatemachine.py b/libraries/requests/packages/chardet/codingstatemachine.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/codingstatemachine.py rename to libraries/requests/packages/chardet/codingstatemachine.py diff --git a/resources/lib/libraries/requests/packages/chardet/compat.py b/libraries/requests/packages/chardet/compat.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/compat.py rename to libraries/requests/packages/chardet/compat.py diff --git a/resources/lib/libraries/requests/packages/chardet/constants.py b/libraries/requests/packages/chardet/constants.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/constants.py rename to libraries/requests/packages/chardet/constants.py diff --git a/resources/lib/libraries/requests/packages/chardet/cp949prober.py b/libraries/requests/packages/chardet/cp949prober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/cp949prober.py rename to libraries/requests/packages/chardet/cp949prober.py diff --git a/resources/lib/libraries/requests/packages/chardet/escprober.py b/libraries/requests/packages/chardet/escprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/escprober.py rename to libraries/requests/packages/chardet/escprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/escsm.py b/libraries/requests/packages/chardet/escsm.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/escsm.py rename to libraries/requests/packages/chardet/escsm.py diff --git a/resources/lib/libraries/requests/packages/chardet/eucjpprober.py b/libraries/requests/packages/chardet/eucjpprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/eucjpprober.py rename to libraries/requests/packages/chardet/eucjpprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/euckrfreq.py b/libraries/requests/packages/chardet/euckrfreq.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/euckrfreq.py rename to libraries/requests/packages/chardet/euckrfreq.py diff --git a/resources/lib/libraries/requests/packages/chardet/euckrprober.py b/libraries/requests/packages/chardet/euckrprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/euckrprober.py rename to libraries/requests/packages/chardet/euckrprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/euctwfreq.py b/libraries/requests/packages/chardet/euctwfreq.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/euctwfreq.py rename to libraries/requests/packages/chardet/euctwfreq.py diff --git a/resources/lib/libraries/requests/packages/chardet/euctwprober.py b/libraries/requests/packages/chardet/euctwprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/euctwprober.py rename to libraries/requests/packages/chardet/euctwprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312freq.py b/libraries/requests/packages/chardet/gb2312freq.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/gb2312freq.py rename to libraries/requests/packages/chardet/gb2312freq.py diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312prober.py b/libraries/requests/packages/chardet/gb2312prober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/gb2312prober.py rename to libraries/requests/packages/chardet/gb2312prober.py diff --git a/resources/lib/libraries/requests/packages/chardet/hebrewprober.py b/libraries/requests/packages/chardet/hebrewprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/hebrewprober.py rename to libraries/requests/packages/chardet/hebrewprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/jisfreq.py b/libraries/requests/packages/chardet/jisfreq.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/jisfreq.py rename to libraries/requests/packages/chardet/jisfreq.py diff --git a/resources/lib/libraries/requests/packages/chardet/jpcntx.py b/libraries/requests/packages/chardet/jpcntx.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/jpcntx.py rename to libraries/requests/packages/chardet/jpcntx.py diff --git a/resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py b/libraries/requests/packages/chardet/langbulgarianmodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py rename to libraries/requests/packages/chardet/langbulgarianmodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py b/libraries/requests/packages/chardet/langcyrillicmodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py rename to libraries/requests/packages/chardet/langcyrillicmodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/langgreekmodel.py b/libraries/requests/packages/chardet/langgreekmodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langgreekmodel.py rename to libraries/requests/packages/chardet/langgreekmodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py b/libraries/requests/packages/chardet/langhebrewmodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py rename to libraries/requests/packages/chardet/langhebrewmodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py b/libraries/requests/packages/chardet/langhungarianmodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py rename to libraries/requests/packages/chardet/langhungarianmodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/langthaimodel.py b/libraries/requests/packages/chardet/langthaimodel.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/langthaimodel.py rename to libraries/requests/packages/chardet/langthaimodel.py diff --git a/resources/lib/libraries/requests/packages/chardet/latin1prober.py b/libraries/requests/packages/chardet/latin1prober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/latin1prober.py rename to libraries/requests/packages/chardet/latin1prober.py diff --git a/resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py b/libraries/requests/packages/chardet/mbcharsetprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py rename to libraries/requests/packages/chardet/mbcharsetprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py b/libraries/requests/packages/chardet/mbcsgroupprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py rename to libraries/requests/packages/chardet/mbcsgroupprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/mbcssm.py b/libraries/requests/packages/chardet/mbcssm.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/mbcssm.py rename to libraries/requests/packages/chardet/mbcssm.py diff --git a/resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py b/libraries/requests/packages/chardet/sbcharsetprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py rename to libraries/requests/packages/chardet/sbcharsetprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py b/libraries/requests/packages/chardet/sbcsgroupprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py rename to libraries/requests/packages/chardet/sbcsgroupprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/sjisprober.py b/libraries/requests/packages/chardet/sjisprober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/sjisprober.py rename to libraries/requests/packages/chardet/sjisprober.py diff --git a/resources/lib/libraries/requests/packages/chardet/universaldetector.py b/libraries/requests/packages/chardet/universaldetector.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/universaldetector.py rename to libraries/requests/packages/chardet/universaldetector.py diff --git a/resources/lib/libraries/requests/packages/chardet/utf8prober.py b/libraries/requests/packages/chardet/utf8prober.py similarity index 100% rename from resources/lib/libraries/requests/packages/chardet/utf8prober.py rename to libraries/requests/packages/chardet/utf8prober.py diff --git a/resources/lib/libraries/requests/packages/urllib3/__init__.py b/libraries/requests/packages/urllib3/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/__init__.py rename to libraries/requests/packages/urllib3/__init__.py diff --git a/resources/lib/libraries/requests/packages/urllib3/_collections.py b/libraries/requests/packages/urllib3/_collections.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/_collections.py rename to libraries/requests/packages/urllib3/_collections.py diff --git a/resources/lib/libraries/requests/packages/urllib3/connection.py b/libraries/requests/packages/urllib3/connection.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/connection.py rename to libraries/requests/packages/urllib3/connection.py diff --git a/resources/lib/libraries/requests/packages/urllib3/connectionpool.py b/libraries/requests/packages/urllib3/connectionpool.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/connectionpool.py rename to libraries/requests/packages/urllib3/connectionpool.py diff --git a/libraries/requests/packages/urllib3/contrib/__init__.py b/libraries/requests/packages/urllib3/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py b/libraries/requests/packages/urllib3/contrib/appengine.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py rename to libraries/requests/packages/urllib3/contrib/appengine.py diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py b/libraries/requests/packages/urllib3/contrib/ntlmpool.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py rename to libraries/requests/packages/urllib3/contrib/ntlmpool.py diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py b/libraries/requests/packages/urllib3/contrib/pyopenssl.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py rename to libraries/requests/packages/urllib3/contrib/pyopenssl.py diff --git a/resources/lib/libraries/requests/packages/urllib3/exceptions.py b/libraries/requests/packages/urllib3/exceptions.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/exceptions.py rename to libraries/requests/packages/urllib3/exceptions.py diff --git a/resources/lib/libraries/requests/packages/urllib3/fields.py b/libraries/requests/packages/urllib3/fields.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/fields.py rename to libraries/requests/packages/urllib3/fields.py diff --git a/resources/lib/libraries/requests/packages/urllib3/filepost.py b/libraries/requests/packages/urllib3/filepost.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/filepost.py rename to libraries/requests/packages/urllib3/filepost.py diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/__init__.py b/libraries/requests/packages/urllib3/packages/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/packages/__init__.py rename to libraries/requests/packages/urllib3/packages/__init__.py diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py b/libraries/requests/packages/urllib3/packages/ordered_dict.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py rename to libraries/requests/packages/urllib3/packages/ordered_dict.py diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/six.py b/libraries/requests/packages/urllib3/packages/six.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/packages/six.py rename to libraries/requests/packages/urllib3/packages/six.py diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py b/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py rename to libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py b/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py rename to libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py diff --git a/resources/lib/libraries/requests/packages/urllib3/poolmanager.py b/libraries/requests/packages/urllib3/poolmanager.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/poolmanager.py rename to libraries/requests/packages/urllib3/poolmanager.py diff --git a/resources/lib/libraries/requests/packages/urllib3/request.py b/libraries/requests/packages/urllib3/request.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/request.py rename to libraries/requests/packages/urllib3/request.py diff --git a/resources/lib/libraries/requests/packages/urllib3/response.py b/libraries/requests/packages/urllib3/response.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/response.py rename to libraries/requests/packages/urllib3/response.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/__init__.py b/libraries/requests/packages/urllib3/util/__init__.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/__init__.py rename to libraries/requests/packages/urllib3/util/__init__.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/connection.py b/libraries/requests/packages/urllib3/util/connection.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/connection.py rename to libraries/requests/packages/urllib3/util/connection.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/request.py b/libraries/requests/packages/urllib3/util/request.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/request.py rename to libraries/requests/packages/urllib3/util/request.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/response.py b/libraries/requests/packages/urllib3/util/response.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/response.py rename to libraries/requests/packages/urllib3/util/response.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/retry.py b/libraries/requests/packages/urllib3/util/retry.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/retry.py rename to libraries/requests/packages/urllib3/util/retry.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/ssl_.py b/libraries/requests/packages/urllib3/util/ssl_.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/ssl_.py rename to libraries/requests/packages/urllib3/util/ssl_.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/timeout.py b/libraries/requests/packages/urllib3/util/timeout.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/timeout.py rename to libraries/requests/packages/urllib3/util/timeout.py diff --git a/resources/lib/libraries/requests/packages/urllib3/util/url.py b/libraries/requests/packages/urllib3/util/url.py similarity index 100% rename from resources/lib/libraries/requests/packages/urllib3/util/url.py rename to libraries/requests/packages/urllib3/util/url.py diff --git a/resources/lib/libraries/requests/sessions.py b/libraries/requests/sessions.py similarity index 100% rename from resources/lib/libraries/requests/sessions.py rename to libraries/requests/sessions.py diff --git a/resources/lib/libraries/requests/status_codes.py b/libraries/requests/status_codes.py similarity index 100% rename from resources/lib/libraries/requests/status_codes.py rename to libraries/requests/status_codes.py diff --git a/resources/lib/libraries/requests/structures.py b/libraries/requests/structures.py similarity index 100% rename from resources/lib/libraries/requests/structures.py rename to libraries/requests/structures.py diff --git a/resources/lib/libraries/requests/utils.py b/libraries/requests/utils.py similarity index 100% rename from resources/lib/libraries/requests/utils.py rename to libraries/requests/utils.py diff --git a/resources/lib/libraries/dateutil/six.py b/libraries/six.py similarity index 100% rename from resources/lib/libraries/dateutil/six.py rename to libraries/six.py diff --git a/resources/lib/downloader.py b/resources/lib/downloader.py index 85081155..43c43d1c 100644 --- a/resources/lib/downloader.py +++ b/resources/lib/downloader.py @@ -13,7 +13,7 @@ import xbmc import xbmcvfs import xbmcaddon -from libraries import requests +import requests from helper.utils import should_stop, delete_folder from helper import settings, stop, event, window, kodi_version, unzip, create_id from emby import Emby diff --git a/resources/lib/emby/core/http.py b/resources/lib/emby/core/http.py index a96c80c7..e7d585ea 100644 --- a/resources/lib/emby/core/http.py +++ b/resources/lib/emby/core/http.py @@ -6,7 +6,7 @@ import json import logging import time -from libraries import requests +import requests from exceptions import HTTPException ################################################################################################# diff --git a/resources/lib/entrypoint/service.py b/resources/lib/entrypoint/service.py index 1153b8ab..2fe12f32 100644 --- a/resources/lib/entrypoint/service.py +++ b/resources/lib/entrypoint/service.py @@ -17,7 +17,7 @@ import client import library import setup import monitor -from libraries import requests +import requests from views import Views, verify_kodi_defaults from helper import _, window, settings, event, dialog, find, compare_version from downloader import get_objects diff --git a/resources/lib/helper/playutils.py b/resources/lib/helper/playutils.py index 9af3a72d..f3146cdc 100644 --- a/resources/lib/helper/playutils.py +++ b/resources/lib/helper/playutils.py @@ -14,8 +14,8 @@ import api import database import client import collections +import requests from . import _, settings, window, dialog -from libraries import requests from downloader import TheVoid from emby import Emby diff --git a/resources/lib/helper/utils.py b/resources/lib/helper/utils.py index 23cb5e11..71d5bbbb 100644 --- a/resources/lib/helper/utils.py +++ b/resources/lib/helper/utils.py @@ -17,6 +17,7 @@ import xbmcgui import xbmcvfs from . import _ +from dateutil import tz, parser ################################################################################################# @@ -449,8 +450,6 @@ def convert_to_local(date): ''' Convert the local datetime to local. ''' - from libraries.dateutil import tz, parser - try: date = parser.parse(date) if type(date) in (unicode, str) else date date = date.replace(tzinfo=tz.tzutc()) diff --git a/resources/lib/libraries/__init__.py b/resources/lib/libraries/__init__.py deleted file mode 100644 index a81e44db..00000000 --- a/resources/lib/libraries/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -import requests -import dateutil diff --git a/resources/lib/libraries/mutagen/__init__.py b/resources/lib/libraries/mutagen/__init__.py deleted file mode 100644 index 03ad7aee..00000000 --- a/resources/lib/libraries/mutagen/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Michael Urman -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - - -"""Mutagen aims to be an all purpose multimedia tagging library. - -:: - - import mutagen.[format] - metadata = mutagen.[format].Open(filename) - -`metadata` acts like a dictionary of tags in the file. Tags are generally a -list of string-like values, but may have additional methods available -depending on tag or format. They may also be entirely different objects -for certain keys, again depending on format. -""" - -from mutagen._util import MutagenError -from mutagen._file import FileType, StreamInfo, File -from mutagen._tags import Metadata, PaddingInfo - -version = (1, 31) -"""Version tuple.""" - -version_string = ".".join(map(str, version)) -"""Version string.""" - -MutagenError - -FileType - -StreamInfo - -File - -Metadata - -PaddingInfo diff --git a/resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index 0d767fdced0b459f7462e04b1308d99da0fe85f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 914 zcmZWn&2AGh5T5->vUQt+1XLW5@R3L~5<*-mKu8EABnp*UE=$nX$xaft_Il;@M%pWV z2i}3_;pQu+9=UR2oTh?`UCr2@8GrN5jQ55^c4zYI(~kt;C;aoJw0}j?d?i%^<fJ1g zV^9(381fjRa85uapi{_G&^^d|p!<;buY4(}0q6|!4D=S{TV$n$K{<f4ZNMGCAs~Z` z7%n2ncL50#Q<5GDVPHlQZe?&hV&Yo_aM(Jrz={j08}1CBA{TU0ir1~JO@myw%1Mo- z6r4gAvRZH@r&ic*%-F#JW1K#zYhxY7uF=My=f-N`zN~#3ohXG9{KbF%@y8mgM{}vL z5*kO0on8l@axrraWjZH9yp%I1O+`7mnFksTmlbzQ<l*j^p9G7r$f*>x#J=N#DcQIX z-Z)F7_t#J>p;~Mn^Jz<DMaP$7MfhbY*YJu{`lTreYbB%-Q-!QVqF9Ei4P_LxSbkL< zrCnmTh7qb+w+<^OEmHPU&gW?9$j$T&sS-wa_zbNRMEo2(;vg1be*=F5%r5DPz$o%N zoAJD{*4R<vv$s_4Nmrw1$IhbA@2a`+X|RrBpB-*G?YECYP1Pc}FKNnf`H+;4#vewp z&1h@4LV{a<P!y$^6@?#OkFIE)R5lLbe{G@5mL4?N@QQ8krZ>#P_Jd;(9D^0whNk7y zGb1aXXi@uwhTwNMj^b(}-@)DumxB8J<n_VisH5grPoGWR%ju+%4)<#@JE!N_Og8K$ nMIkHcieg-MK6|cB*(!W_j{urIQvLXL^dQ=c`^o(zjj#Vd+O!M@ diff --git a/resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc deleted file mode 100644 index 93f423d567c397fd76892bf3e94725f87947d426..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2754 zcmbtW-EQ1O6h32nf8)*OuOUCBq#z_Ll_;Wuizo_J!%d~Kjfh%_CCKvHlf=!gcNsfr z62T4O33v-0fD7J$XP8^AdV$1sulUY*y-Aidsf0H(bA0CboAW=JZ#3$^cK&(ZtP=f2 zmmUlBBaHM9K@{OnP?xApB#B-M<}5}Q9a_}3NLu8!ZC=#hM%JdTLp_Ju4he~+RB+3N zTOq-S4t1;4t5LfKsXDbMNV)`K51<W_lO&slo?@asO>&0XGbCrJJxg+q+IJ`daG8FA zUh@=HD5@IzJ&I}ty+BdDpcg5cDCi}U%M`hEE}%<;qDk}RT}u8WX_NZhC<rs&ziftM z%xD^JiLDY6ONG_WmERLX&6$AOlSb`(Y@z&sm5-(h1!gIlE@&8nt&t(v8W}QI&@cpI zIUk0=-ke^d2=*=*^;amuR$&Svw&N|m=9so#b2vSrt2>#Bll>>pKs)hSJP3J%A6|KE zVWbUAKO)YU1-od+6D|H)CQ<q-&JH|FJDEJmv<=R0q>w!HW^HSIcl$Iu=qC?9-+dZ) zcGEbMpALfXFxZ#rZtp0=n(X>vzc&c7)xoJM;j?>uo>4-W_Ymo39$tAEh3X)3h0dwa zrUrP0(MzG4h>Q~XRD-mxZghiQCki(2<1NJ?<zjkw{5s<w2i+ry1Jncqu$>o27`zX` zGU)FPTxH-xd3qD<N7urR0>u8vj7u(}<1j3{qH~>|9~XdUYmwYrQBnnQDnH2v4_1tg z4_2~{w_aZ{?3F9*E0&E^SO*|dERV`znbfkO4ac_#&EaC}3q#vH`$k~Lvs=32`)Q_p zU)OxUbDGK2_dQ2feLsrB%=gtaPH0c~{!tQ#{YZi{0}d>-!)`ieqn=D(1p`0pGbA$H zV{_R{B$~ptc&I!1*OkY7){|#fQs2g&$+S5TP!oB7=oAFe0#|@J5~(i&H3g|L4NNwC z3I2bxVdOKet2}J`m+Puztmn|3TnGAS1j<1C@H*1?JG|te89>p4*UYP+{55T{F)tAj zbL3Bgl6!@K&GJJ5G`B*416LZAzSYvDAPwW#E9-ik#z~q5Nhq}qPwR3dIW}qmO6amo z%nR4GXQ?t4RpDu@Zg~GOM#=?A2u@SXnEColV`t|E;^H}{QA4s+6Fa&z>;Z2r(Wj8Q zAt<-Nmv3zWU;i(B=ax>;x*45|1c5`j;TlBXzA_ELZjD@7<O+Kr<!%e1@8<}4z&;g} zXW|R-gUz=a=Q+9^hZC7@t*&WtJQlt;wGH%fc`{I&+l<Kvl<91_=(npI-F_H!)6Lb9 zoIwEW+sek^0ER-&YJPZS;4O@F5mR21Fpe)Ji*w*-^Iu4=u%6^=$<I^}$_{QE&(>Bp z&?OT)kmD^;SB4Gn8^-5@NGR?ZBez|-ejMyTfr};})Ero2jxi19Iru|B?>=heG}Z30 zRB7B#{5aX`YkT|K&v<&M*hV#vsa*?;ds$b*VSCc%e#`oO^g2FYJKcV=uS-1p+K!V< zH!#V4sr>A8AX9ahp(?{}solI0=6kxv*R`Q_`9uZDKI%8@4KN)j?O^iF?W?$DDsEG1 zlFu|cnC_$<c_Kq&qPAj~fqx!!$sifWUGjyg_Te4e>qkdjxyf%S``Wc<Ma3$MDZyh3 il#5>(D}I)1^P6kAR^4jW3Mt;L;g?I9{dyti+W!Ksf!+}S diff --git a/resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc deleted file mode 100644 index 368544dcafdaf72965a51adee92899dd834cab69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3099 zcmeHJXPX;W89laEeb=!GF%a6&3?{4-a6$<oMA9}pyKAx9#Bf<yu5_-YVMlY7J2Sh{ zPy(TM2)*|bdhf;XGxC)`z-K=3-jSS`U%(eu&yMcQbLTDRJ?C7VURr8A(*M_ettH@} zxbE+&tnZQkq<b77pyYZCJPww?1+WZOz$#b+>tF+Hf{Wl1cmliuJPDoxp9J0rJ{i0T zJPketd@A@f@MiGo;4{Eyg3kh<4F=$Iz~_R`1D_AR06YV}5PT7M7JM;y3-}Un89WEx z3f>036nq(Y9=sjA1AIC73h<TStH3+KSA%zfuL17{Ukly?w!k*n0aw6Pa1C4sH^2+v zMets56Wju~!AoEld>yz0_P|~6KJb3<GWdG%4WI(w2=0Mz0^bbogKq&f*art7K?7a^ zhu{bd!MB13U<8iA7_{I7d;oM{0;XUF9)d^UgWwd*!MA~L2j2m{6MP7K7x-@QVemcR zd%^dC?*~5seh~Z+_+juP;77rafgcAy0e%wv6!-}EY49`PXTi^bp9jAHei8f<_+{`b z;8($~fnNu|0e%zw7Wi%OJK%T0?}6V3e*iuT{t)~T_+#)V;7`Gyfj<X-0sa#F75HoL zH{fr<-+{je{{a3G{1f<R@GszB!M}l5F}w<O4E#HKXaAAk+_7<z>Nrh;EHMoF(_kx0 z_0YuURJk2xCdn5&kxr6u5bW5&fvR?F7N>5i$}2h^82Jj5fmI7@S$v=>>n@9jCa-RU zljRLNQ41G!o~!bfNp+N0x6M(|8X8r(6frc3Dql)R#;H=*PV&;unYJqJ=!vTAnBh<x zwNT`%?3%$SwyJR<W|pKb)RAhoqSUxpr{SSdrQU(gn=3}AqoBy6DqW}JgG4oYQYurY z2M2j`HFI`(9;@0K_d^q>d2QFt3oYMo2U23)*L%yXm$fVjZLBK05=|Pd&tZ`Kix&hl zmyIQa+%2>QAys+93V>>1U5`yZ%N%r~P}*6y$;3peylun8<aMe2+ziTvw&1E3q)G|W z_4t*06D_G4TZUm;w3nZ}K3Cw8I`v;qW+1Jmqc|M+T<J)@Yb)9v9O*-opPIG2d>wj? z)yND|XXS;wCIOe%Y;>UN9w5#~>Wm)eOH!DhPdux((y?R1saoh{QJOc`bvzW}1TwL_ zvTG;7?YHGioskQZG}Q8nJodqFond)VXc3wGhV7!Mpfxrw9Ozisr2E<#nS|%YWvN&- zyUBDgGBlAivasVH)q5j*lxDF|rYh42BjfV&rd?hO{dB^<ADkaT;6a=(XT<ADYg(0e zjP$FjTQX5U<)#NMI3s;U^5%wioDak!k=Olo*>B00e7!c-QBYu1?j42kFmJBuBwe1< zque%qA$oaJc1k}x<}f@IQdBx7N~M9i*U-@UK{r~5qO+l7GXh^Rs3L8Cvg_>78PV9G zx%T7<3}#PG^rqv<$j@wcU++csXtu1i!(q_YF(v89x-7|0tm<SMY|1E1ECs7tYarzG z<y1DZvCW*S^d?4hCHRkYoCwRymnOz(KS4e28z&|pP{}rFKGmu=&pyo?ZEMr1q?3k3 zXW>}VHM>?wvhJ*vSyg%hgfn@qDDa{l3t^(HugbeZ7%L2y_!7|L8CfU$+d7Ig^Tn<` zlDzY~rJd0<9gU^iLLK!rXH1e#BN5n2CUmL$Nzaj35hcpD$mWe!(9shSTcq<wA^1X+ zd84qZU_&yg(z^9_;omt^w2?P_ed1PLzBNyVK~dMPan3qb7UhISRogC1z542{D`F%C z(Hg?8<e(s(T4;*_<u^Scfa~?q+%Wo3jJ0sVW;+r?np;)9<7|=$ZZ1D9PMn(Y#5(P! z!Q+AQ6OS*8epPMDI1>*uvEH7J5}hyZj)Y&=%&**v!?D10QHu3*i__7>uUdVvEIkmV zwOl%jRBdZ#ACN%Rwy;1*n$M^tod|Em0}lFrS?i3#C=xGF^%Z+Gt5KC!WTvIn_Li!x z4!oZTTdH$oQH%4%Kn{+P>>68n^q>$-Dl06rvaX$+G2&N|74@ppg%t^OA&O?Uyfk+! zyX<9>*Mvngg4geLIuj|&IaJ>?SJEq?iG=o>;`4Iq)VKT@PzZO?+sTd{i{QlN>f70J z(N8YKv7+6XPZUePUDd^N1RZblHSroV)VVitZ^pBD^<5_xSA=d}wl3maAd<)WE(>2# z^;X8v_N^7?D3-zQNDve?+_-Hr6RemlO~l)R%R*6sy=yrTE6M%Y7S1zO*2P)mD5wg+ zqTh*kT8~wcT-Bt|0;BD-#~-Wi@6!(U_XWn)?VZ)$qj2&gad!Fl`OcktUD1$@&)>1P z8TR**Fg454(SZa@_QtbAes6zp?fgGCnLbv#YiyZC=I)aJ$@%axpi(Lw`~T;Ehd}-K Fe*pD4cI^NF diff --git a/resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc deleted file mode 100644 index 2e8bbe118279fb735d7106993b1282ab526e22c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7763 zcmb_hOLH4nc0LV&00@Ev-y;3&nb<Mmkto};lL<#w%!rbru|+ASCz>d7K~3}}*-}5C z+YM4=5vf${iam?T)MS;K?QD|mtY$O+AnPo;vdbpkWs#~>%_jNIx!nzb;%LfM0ofP# zp1#lTo;Q}KryKvc`(HmwUM2bu8u?UE{|HYUqHytVkx%r%)Jx=B6qM*ei7b9sCcjKU zg&tJMy5&~LouDs4o*-4CFUs^_f^NB0a%)1Zk*W%{#*?~rawo}c(3hAFb&Gb|Q{P}s z+HPCv<d)Y>?s(6<5RHm=;`O@xD*u#G`3O(^?<f?uhxt&Jc$x=gaxo<+*Z{U9+u#x# zsn9+;V14wNAZ+bRG_g1+b>8Cx?8s~i^#-|9<Tizl$~3t%Qg4#mB6n8mAY!UHa_42L z8EGz%yC_X`TOxN^dd`x2f!r0T&ykuZca_|ixcLS4us~{&+?UCHC8uCu8IZi1)^B>g z`s}!;oZnbPo1yLO#))>ii5=}eR`@#7POs;M`*z~&$F>u?w$pdL$VM;a1hyCMMLKX2 zFACQi4I7`$B+=e(KT+|9A-pY}>`Rx<8Dp>cD%?*FF50^umQB<}J9dJeuWW9#Wx}`3 z(ik78#BrU(5yC~=+rxg2%_29P?t#6Bv)E=o_JI@IuD7?Rv<efu7bW=e9DjHWa{yW4 z`9tKW#J0z~33v`C#6c3J_nfY>50yT)V=wT0N83qc-^sT213NiTGN}t58xG)e9pZF; z<hU0FMpyYNQFhny{n(BGyqzy9G?@c)*??b1J?#Mx2H+rarMs8Jd={JWSeMReE$D-y z!tIk44D!9MT+8LCo2cZHIc~QEmgY|-d+y>p{v#Bp!sJitq+h3m=rNiWB@i?SW4J)4 zAeFh(X^BqD^beMZ=uW$|eX=&(cNAioem9A<t<}@MrxjR6t|fE=)vl!VBS(kerZ}x| zEuG&|J*~_}@0(X?g)u_kO04|7w9c2xqtl9`F|PUq1NY;9Aw7d#|91WQ#>0EZKqb8L z!NX6y-G{N4s7pPk`vgeD4>Lp_c6gTc-f>#*biB|@I-NK9;B^#aHLXdjQ97B+&#|8O z8AA)=L4sBT6(IC?0@zO5VvVw}9VNGOPEqa+ts~v8Xzr?~xM*@Q%>{4U6fLezqPU1B zmYcCori)!L48;!kEkD<}>m?L?A)-I|%3h-cH)FA|V_V@=_7;@Y9npQIlkO)`^u+G> za37;$WIs{Inc%YU-ri&WHG;$v6h@iAfc<?RjB9_aj^%W=7ehw9Sh<G#c=UF=lvY?z zr1iX8yCfi_Wvp}N=;>6av#%1|SJ3HP=5tk1P_<>XN+*j23?rSoW$u6vfAe!4Py9BD zF*H`C2{drG%&vkIu(+B*@zWiJ<BY-Z`ypVV9h1dO&j1F;b(L$cLFb7aG7@3Ln$t_5 zb?iPAk1yiSn6bPow><`$p2rc=I_BvxdhN1doJFQi96EkqodaE*L$|>o2z0fU)v(5( zD>|LeNX&R+(4AqV#t^&@Gw^n|jNTf@@Bf>P1QES}?PUz48H{sKfEG0jbfZAwc?K#v zby<k{!{3ECy@AFU3JbJPv|~t6gy5rGijF>2Nnf+F_PrQ3a*q!VBN2+&j0)E8=P29B zg(|dF&`XZZoGe$e$Qj8pOj6<X+LmFEGpghzoZ}vzLXIk>5p}`?$GD)3uGhKy$0&y9 zpCo-ttbs+WTHcaZ77d_RX4xRX48Q|QDq<Njj3zSWV_+&f{|S6QRY7#9jQZHqQ2@+D z2^)LOLUKEVG{i6wM+dOdf*{y`pPh}NaCxDtdI|+BFyJn<Qv#o*5Vtk-x79P_IXudg z{`&2$E05Z&w`}%kuueAq%pymg?-w)e!t;=gkEpft_sPQCbM*7Of~MdlGk2+&xd-v) zqw#X~=rTo*&!BnDu-t_zI(L2LXeb$|<RgXgaD9LTK*J`YP>nF#R)m8>{T%RGrrUGs ze5Ct^=<wLiF}q`22vPAHJYL)s*|!siZ?Pv8$KI|##D4qaiu6%ye7$U`KU`m5Z}ZM@ zb7I^TTpq31o)!1kt{M>+<H*WO*SKJVoL0?zx#UZiGpznH`u#PYnCUdzf(>t$s#e*m zStqT+beMSxre}udhxt9;Mgf}ut;Srwk_kY@<rY1Lw+}8a7hDcsA6+3FKa{Q&dMr#2 z`|?u+7or@?C!g=Y$s02)@wdSmF5-)kewUrdZ;SIe5NEoCXUmO^^g7Q$c?Ceh-N~S? z7C@I;qs|IXRcKcRndiIC!wygYo>6Veb-{OauSCnh{5W?ojkX<>=zh3N6)2e}4bmTz zWm&kZoxciK>Bp!5blu?y29*oFU5>mi>>%XYX;)7^xToQzpBHLy?9z<_@#UcmKNQ0X z5%zSveo}igw%6W_+nOyyYBf>>iW7(p5#FR#c;J{btpr}640JO^lw({yW3kh!@v;qT zpJ7=W6C)m@m|a0C+hEoOYucJDjWDcPsKAl;!jIrM;&yU0TRQn_(?uu{sw1N`5QOB) z@!Rm6Q8+$=IrkVURIHk2mqzr5sFFfKvC_`vf7F4o^c;5iJFL<&Ui}P@ad2xRZg%eP z6zAojWCD+z7fAs&!{Cl*cbEaeG+#h2E!z70DuF|uU14hk4ghnZ9})svpTAB6Fonb^ zPimwuvxIZl0x?Fw^7Jy@f4WTfnU#Rw;$~sQ`C@IhD2OjQ+=w~;U_x5!bOIf9I%y5Q zssqKGRuNpmAZ6}-IwAej%Ez!&`a9V4881a>{{*_Z_)67UwpO99mPW;g=NR*D&%1WG zArfsEw967$q&3#Mh}F~T0j^N#GpnZ)aW~THm%P427^Rb$LO;r@_A&=WthaH<22LBf zeV^;iPUmUg@v}F;!Hv3|4#GnmksVeO*s$}YXSv`=MlW%(!o?~V?2>3!)|%Cbwz*)2 zNb4NbDxDk)JZUoqwqDo7x0>Udk#mS3NwCBW!mXHBZO&9n__O$Lvec?I%grhC*II4O zqBc1{rFldbPw`<8KEe~Pp?E=Z2Rh|Tq-iAU1ig|rb|m3(8w%m%=bL$w#K^lOII=8A zmN&^n@SctiJr@?4Imcv0*tfA?l=ZMx(#?kNDB~6tNeGGPW0}GQJ9<9s3rnSDu_@%} z#d>4qcWFojvM&;38%z8OkKv>8$=nc;b)&eTGW9xoK=4q!!^OK?*jx<N;qX4+$BTc) zBOYtB%J-?6cC(NL<|1acGWY7?@*uoyh+(t;)q;hF5%7ru_@`xLB+#yqU!tH)10)T( zJwbkjf(aT-NHC~>#C9FIIloFljRsX@H;_C7sYZUCf=MB<f%zMu*2!;BFhzq}(Q#5p zO(F5X2Kmzz%+R22R-7WgMZqi$CQZ9Z{u~AKG-#OiH2Dh@EYe`gv}ed)qF|W@O|!%Q zk%e32U!Y)x2GhmDvqD-G5-&VQ{!0|ROoJIS@EtkuJo&Fs@G1>j#lQvfU!wrZ5hNbC zNWM+McW5wc+Dqhrmx4c_!JKI?lm7+<Z_;4iv@ekVhZMX;g9X!GA%Bg6HVqcBaE(r< z=@gOTB%LDhP^VKkMzH4<I&INul}=~qbe2x%=yYCGbCXUNX&-P%#v=YVi+!i$ZTcE+ z!YaF4Kl^}wV$s*o+~4CICN%d?O7s=cR~CI$qCJEklF9sg;e{={9xTy)O!60>{y4cv z$t5~nl5hmMD|v_J<8_k^K-%*)T76g|{11Tm1scHfTV}wYR7f9`PFHvz7$+Vb@2vg0 z^9XErtFK}t+}J~hRmdX=*1eL<OOOqO0ftILk}%1nm;HI^Aw$F;?m`*3_AXL~3K>LB zN3x$DBRkkf{u183G2t4q<-6~FUqpNN7^zfy?dZTmkA4h|>>tCA2_gMqo?SDW!rVwT z?0HJJFY-h(oHxKK9JU?Fh!}ZVlYZod;GM@RL^czFk(hqA`Rov62S7Fm382vgq@1K1 z^?e+IQ?30FuKhM55O(*6VXLgXf#?Vs;;6^T;dsN|K}IlNZKLZualG*;H${(zc~+z1 zE+Tl?bNW7KIU|{l)!gWZgDfxX`h7+eJIFVkEvuaPcq7keal>S?m5%NBAU}ZFu`8M6 zLugY6fz8mdDOS5V5?$syzkw<5+`D354SHABv&J<X0~t`*;pcY@i1VSvFjTHRGV5)N z<B3dSd2{C`|3tBQ@5bSK3=jWAar2YS>-cW*I*4j>)IgdVZ`QJ%;7u8`<EwUee?J>u zM|G#C)H9GLQ3roU+Q}M!8R=aQnFSeex7X+DG|%>#?yF5s`E$q7M(w_Hcn^tI9%Gh$ zA5|>NU-divSaBK__2!)@<m^K+%U!TUaP!l<xA3yEdHW_GxV*V}JuT_oZ~hjVzRMPj zKWwczI^;}H%%%mN9vs2X-^kPlQlOAm=!&o9Cn0wCEJ9H6^AbI+(vBD%#F64q@Ngt3 zOY84wP8lNbk(lRusB2Dy84JaJ#%~)}IlY5mp?B@S(H4t0X`QSv@atFo2uZy78Y+3G zZ+O4o{4YN3%jr}eOFQwN{s1r1>4MTzhwo`lx2MgVw&y!t{l`pd<s=2zJ@<<KK2wY7 znDQrM6tJG=?4SO@*zjn7UnafE)Y*JeP-DJ){UMX*M#xYF-~)Z_Ed5YeLjUR6$s=%w z{%EW>vi?eb7t))c>pvSCa^xH`Cunx-#}66w-q%}9Ee^>PPUQ{v9b}B8Z!&o?-*;em z$}D<|Uk#7M?1hxmzW#Hj7f3t!IY{4TQn8!^?0?9V3ajq4!eJ18fWW<K_BikxM5L4U zYUXNZsw}PW1<BRQUCQmr)yPF?#$8YrNCiKEOJETc<cM-0<l4}g_bEFwo2fxBv+5JR zO)IRvg=GypKgYMsvd|IW&0nJye};nQn%1oRRjq~6EIec`hktoH|IXjcTl3ZmeCkSR z-fC5rthv$>Qp(E+R~jW9VBYqmW_cX4bf=H(wq`kfQ1EYuCC4!kiU*!n&98~#+{rvf e*oHo#oS}Zz1RvMxm@ejkx5>LMFaKhB_Wu9?z+Ei> diff --git a/resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc deleted file mode 100644 index e242cafa64776176be78a6c7ef2f18a25b8a3e70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8626 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc deleted file mode 100644 index b49b1dfcbd2a2fce928e9ac92ebfbdbf514f2518..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2970 zcma)8OK;mo5MGM3Br9?aC#jRBuRSz?d5938=tW4<7;aFcP1GQ28q_e#tjLu_n-6ul zvLPV_iYDj&kN%sUdF`b+_S#dwSyD7(yG3d5aJ4%-JCAQ>xV^E_*?a!iv*(u>`-9aU z2lM+F*=>+71I7Z2y*5-b_R41a7FHW9u+6H?%2lj5Z0N4}I8{9j%<f}kPeH(lvk<rs zUW@J93}W^jL)S<i1mL3mCJXShY33~!;J<C=Z5FJvpkwBkT*7VL{L&K<1Yt6Hn2b~L z2>0!dGOdKK`K;LBA$C+ObeJYQ9UV$v_c|Sp=i6zR@slu$_(*~~%VfZH%Jo!oUqsPJ z_(yy^PkeJyPld*ELcDWu8VL~0GjWXTxfYYGl-utY^68F1j(NQBR`Pa8nLe-S?DmZ$ z;-yDPJy(g*xi^v8!+9}}v{xFl<E~V%>6lH^c@(UUlKLV}=ZWU&xN-vQg4TTJ-d%p% z<s(71rir=Q*qbI=;zSlMaXNt?AZ6u1h@qe3Fv8wg%azSNcn~YTo{Q+zC+F{u!^u?h zW1$vs$4n*^E?j;pw1^Mi3FZ<3FiMFioFp-+B(>kGSzS7Whw&_u#83pbjEwMoY2rA~ zOi-rMNbAQ^Px~(}aOew8TWJ7~zZLkNIrj#rWxh=m54{P{dl=a^$V-d8WX!{)VI~IL zL)RL(jl5NQFL%hNu8p^h#)@Pp2jn-Vnt1))+v^`ZSwMy4)6Whbg`<Nk)biF0*b@_( z9mI10OC|@N!Pjh&w>>XRLhX4saXKSO%v!gWS8I^!typ}Ew(-!dG>qm$OpsL2{16kH z-MID>6gQ^Of<p_&o<l6Jv7<GnzBb%ovDlHr)E8v%kOWwGVTxoOy6xrfgol3#(8)CP zr(CO0W|U-b1`?4ApjKpQB1TaWDawKK2qHul0tzhoQ;|$0#cVuBzDRy7RfhatWgtoE z7pc_eyXTUj4q_AMf?qFYvJauUS_>H*{H6nky$L|VKjB105Ez7PhcUNdLTQ5P+G-6O zxibaYb2~~;OneK{qMa)jz|X}-_6`VZZ&_Q7P5e2Hmc9JA9?lES`J@Cr^sX@Z34S0% zQ1z(I)OH!-R}4Wi;eCa0VbdVgQXAlg1xD+26bI!|u{*1E*Q4u8Fn^9C8NrrWU8~br zUazZJXRMG%2y`$E@;D{{nueC>Bo@JkKvaOuW@DRe58cLKdF{^4%#@<LH-l9b!t|c& z<PHUik{}VW^t_t?En~@9^}(s+OnKe|++C)NeJ))JmVw(*TVTsOo)@RVJi-Ne*Ykcv zeJifZTb>uBK4bz2MINg*&B&K(lgMQv<f0m(@GJ7fI)({%_j0q*Tx;50r@P&4nmr$T z)kEU%WBf<oM4YRRi5^;YOiWD2M6Y~}CW~f@pB-wK^wacH>YMGyQj0)nu{_*OxL7sK zAoTUEC_IvHx5^bWN5`aIKn;ZZ5qc)49vF*cLEVP7P^ikY871iBqJS0kG;fvltilHw zO5+<cmV*GPS(oaI`k=%wVYk741?DR3=1!D~ph`KRCRyG*I-&Pjw^MlN<D-my*II6^ zjJdc@Qk;x7l=gt?FD9XioXM{YQvGn%d1UZFsZ^C}sQ19HZV)*Qzlr>HtbK=(nWVLr z8<hfN#o(=6rrY<3w{|<S)@>}W4aG5hZYrNCp~7_J-oUlg`#7;m<u>(cg6kPI(10M| z`$`YHrU&i(Pc?piD&rK2P(R{hmBzf(<Mf;7VZ!4yqpzimiamX0#Zn|0J}egK00J$# z4wlsym@<Nk4=XCjMmQmg3@Y$L%V+EbOwM^cZw6>DTK-TO+gff`PA~S{w#jvMg^ph( z@-7jR%@i>8A(4-WoQBiPOiZc$e}w_8GS+pF%?;$Ty}5?6i}rwi-z>g0O>UdTi{#|( PJ4L0rXG#MBWPA4?BbT<w diff --git a/resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc deleted file mode 100644 index cabceb1e10e6403e5472a8689571666cd647a182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6102 zcmaJ_OK%*<5$>7UoqcdAN~B0hmLC&24{sz>j%3G<U?)<jr$bYWmNKP*gw1fKNzQV1 zX4Nw*iUfrKwgTiF1Oa>td<$~OA%|S^JNgm?1_BuHA%Sx;U?5*r?=F|LoLNq@r}|md zRrOW%+~j2acWeK?`^hOGJ`-b43H8f(^RG}u__IVJgg>fV!ncNWTln^{?g-x*)=R=K z4eMp$mxpy%_^yaB#1TnFbgROzif&E#HPNjLzb?8H!k-WpmW(hS!(8zUYc#}-s3M}O zc!ph6g`5;0W3?k9s)?vhM1M*|6C!GG{iuj0MTFKV@eG?iCL&OdanrPj@HfNt8BBUy zM6>jPQkF&MIL`~BC0fn7&%wV~`rJa<Ea_SypA_w4rzf?ueE$L!7j-Qzq*~i4Wd4=) zAS}@UJaxPd-UYn*1B@?(*t5jP;K~wPw#ZM4!V(`@On1iUZw%?S*uy9^*+Q+00-ug3 zO8ohd>K%y2rh!jEcg53^NL^7>sIte>-Kq-pjOQ2JjVFZEqmuZIElxcxiwz;3x?-;^ zo<g!HtK!M0Vh`haz}1Jp7f)+q57Yct?4ja_Ek~$-ave;VdWGn3V}d>W)wpJ+di2-f zATUs~aEzFLKx7f9_bd{MOPj-vw}b!%pPeer1)^nkMq@^E<74$;9~b9T+5KcO-jZHF zjl(RG=kr3H=OdZVdr4e)YyH9tk|f*Cz4c6a#isOf*$Y%q&{yWINl)fs(34TU?%}z$ z?)8E^m(e$8^tR(9@v=18@suq3D)o{$FT8C1n_cI5E@eXnbUDbwI9^huN!M4JHSHua z)km&i5mm%ldQGWJ>3W>UX<h_rD0P_`P^3iKl_~M1T>2=H!f7|?^<)|~>)Hjkak`-$ zu%xTq;0g2WPZ*Bc;bx%xGL32Lvh4PX9qlTa_me_b!oE^6MP<OfE>Z7433U<+eRXl+ z(u3ulVlzuGzV%=+UVD&Zme+ehxD{;3{6V*0;D7p{U1V93_lr20@9pRYuiiEj9>+BK zk5LHsq;=AsvglW{UAtj5?4y>;zZsM#tvY@+&}-Jq)+B02P^uR&GNj?dhoo^CZ+;7f zgweAbA_s(u%{C1MXB{V(FmfQMmxY9GLD7d^1R4g{1OL&25GPylMj9kHgEUH{3h9{W zq5pJ#Y{svkf^}dW?NNp8nJDZ7TTOfEdehO3cAP@ss^2SQq}_I!<?)6tgBll2OP6w) zth0<w0rx9(wav}#_RsK@(@6^mqh{^S9oX9Zu-kz>l3aL?cTs^=bxg^d?Z8Xef^2O- zb%=!_wZx9ZnDZ~khQcq5Iz<99vX``-7R?IujYXy1)~y@2mR7LwO(Qj30cGXdoe#8~ z<+_vxUFlP&n|GI+mLlmL=D9=yxr&Coib7a(_)S^6(+4@GP9bqf5B(pV$QgW$PK5Sr ziTq{qmBur|z4F$brIp?Km9<RaH0I~$n~r)B1Jz3?biK8c7qWZp2~Ni0P5R7WzpKmZ z3M?Gjv-&PZy^r_*9nnA_2abp~9n!crP>eQ>jeclBVX=8Am0>}hV;11+86nW6)hzG+ zadk6JBz#w#dtnCkdQy1ZAcYAk52PS0;>Xep(jCv7k8yPYaSiXlu83S4`M#dYLYe2r zmBIHt>dQWS9Zu1cki)Rg2H1zE3u#d~Tyr!L)=cDjVVoSzys~utkd#TlM$}EzzC{Ph zYe^Pv-6qKpXo^bBDjUV0Ij9EQZwamwmO{GKZtHrx-OZwYLiI+w{iq)#rbT&}Nxen| zotLf;B$mW$`pBR@5S}_qy~yE>6=ad>G>Z4|vT`=;M!DfO%1otm<cD@lrn-PPf7`%3 zln}=Yc+BVM92?Lxz<8%5S~y9HdVsXh0MtuV?*bSt(FJhZ6b%8*Ed&akn&{LC%Yo5A z<n{zZeuL|i1ecv7qBA7`;uH#i4&^_}<uOv54S;=_M;SW$OcR}mW;o`bVf07Saj}6W zceu)NuA>{qVm1T`fL|;Gg)}NUI{;|76<|67g#2q_tc6-JN1Oq#J10be7>60h!Y9$! zP&xKMGyw-wr7c;A8c4sd-eEX}CJ&L+3)IGAd;s{o)i{j^zDY+3AfrH`vXjqqp9~7B zwqs!FcW@94vP*Eor7%ho?QUZ%{#WszVz<0`N`0TY{eX%R3P9%$92QNb-H`jJA5r54 zDlSrw>XuZgOfFu~Q#WL>kaeLw(PEs+RlkaHJjSmqE-$RMR_^!}04$OGO1>!*8Z|vO zWF@<Tl~(=A@@@FfYj^x|NO<A9i_0`x*A}xdNUkdh_jz;i_QI7#AHjf<>)HXj>+<6u z=}T3^wwiTC=z3_g>K&r}go;Zjd}1tAZ=tFkZ0wMl>$weCv>}^|7utjPsZEl2i2gZQ zkZ|U#3Xu6lMrYW8{jxQS`V=s{VVwqgm+W(P-Bv$E-;f3jpOK$_6%@iBV;P7CXGlw0 zu#OG_Tp@aASbv(O-FH{uYB<_~)AIThf>FfQ3yRJB94o{*G(V12mY#zmL>WbPtP@e3 z^66`_TkaR@ufNkQ?H{vt;bekD>f>Ox2T=l?Wi(u8Od{+D7?11g`LO-h*uHW1&J>tc zKgYKsm=4F|1P6o#>Uj+XxP|#3#E0JJv*ZUT)@N%V=K$glMNt-Np1QTj(l~gBEFc(+ zAyTX9?A{w%1^aCild|iF6MKx%5%NL&%8f&=SXdsZANy(&pA5oHua^V`KDvXjlq^L< zQ9%x6v#KWW&C#YJr?xLTRi@8LDu{0iUuXkkGBe@JdY%RZLBB;O<H^gj_AJCTV=0oz z^JPWG1zC}Z;Pl`VO=@t4WJLnxgr_z5D6?cpb0C?&AV?n*3L6ChAtLc_5eIA1b40O} z6$EhsqWIJs1iWXHIPE_<XB5v1gOqXvj>tX86A?G(6b!~D4aiy8*MlUNypHLMohUgb z@TI0}<hk#F_T}C%;&otshow6a=k1|@|3Cuf_|ToTCT(?tgo~FSn(YK$#?v=pfP^I+ zZy3D-$P6e5l3CLLI0Fht%jDCM0dk!($X^T&21Ls1%0ne~Nr~0pD4a$P2~~yyBf3WN z1|6nuBMK69cJzp}X~^G2Q6C<5o#y=55xqYT<}pj8HBn6PJg(UNzyjK-e~5xX4uC;c zG)ahc0x(J`Y(+fzh?M2(!(GCo;s_y2F-6Lv?pwuCp>9|^b3_E30e5gsKwMVE>ZAKs zshAd9b)gn4Jo{Z5gD&^c<z6~HHh9=VXf`v%ut#4YEk#27*uuR-3ef_c0i&G8B8T1q zG$YWSWuEF{HT6XaNLesOt|PC;2L3}E*ue(T@f4-bgpI4|S@e8?JAA=A9JY2&Gb_9s z>Ulr4#J&6JX>pGxc#&4JNK%-p&a)u}ScP~2<S4glLY=qA&j@sy<{lbb&8Yz1N?bvp z2wd{EH)FVuyx;2~mdpoMV4SWxn<ItHHUeCK43N5KGRAq6NiRvbSs-=hZ6kz)8JQ^& zrowjM;Rk6pd{{q+4XUiav1xFh78rPXV?cKg1Ol9akM3hQ0;U8X0lrxn5Zmt}x6Ln& zBGzoJBSR2i=8~+qo2h|KpQnjN{b2eMxwQA%8WP*)CGaoFj=cb>K1S}}9ORo>KZ(dQ zm}DH+2NVd?^Stqc^}YRJEDRElxr{(<4dFX7Y-!wyt_=3FqK?6+b!i<bz!JmU0OaD& zzlLKb7}C`tT<NLNp_nvWEuj5N>g0WiLNs5}PA}`JH&LhS147X;htDAehdytkgFnpy zPnu_myxlK){bFI+pKehQbbzKSfYzWW6vKor(Y1)Wi3!G#WdtR9`mVXExyBb8eEXrM zXgj4&7N^=JM$-%zB$zP9^^G(aB$eAOQ>lMQL6E7Jw-qiXbZrHYXDDtY>AnORT++`s zn-hLzAbag@ZNt6hW@98%zf4QDeK$*@cGy$z(<aKe8!B>jl{XXxMW9wtJ?xZSKEC#+ zXv!B+h{m)<KU`wit~FJ1Q9q4e&92!F!lXG+ClNNemV-QZ(t5$3wvO4S@pTk!j&%&Q zSM9oW+-X<}+2Focr^y|>m+|J`Lm{zoPHxTB2i>}IZh@5I8Ll<pt2t@ZjoUrCU0Mzl zV&mO~@h=$kT9_cBIBeg{71`$Chz>^JOCdmoANo(xGbvY2H#NX<3S<!Tm;{%uJ1r|R zmj{Y&Y??Onr6(v335qs?;~r?Qpo!ZLOSo<%Ox^xPtMJm+?ul_e=J)$F%SfFu)$3Hu zqtKOhJIca#Tdfi4c}d={(C``FoLFgCuC0jDoHh197NX8jahZzqDEw;HE1(|`C>aWt z2(vEYQxiMUEC%5Tq4@$!yGHpqbL1e|1dtRIF?h2~c-iOeP*>kIIqpv>;>Yy{ZFB5r UpK+#}b6(?2qc(M<(P&)xALiS7%>V!Z diff --git a/resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc deleted file mode 100644 index 199c081ba21657517d7297d2389319f50067db88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17420 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc deleted file mode 100644 index 5a667067893e573ea9145da73a1180c5996a0eb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10304 zcmcgy&2Jn>cCViA84gK_6e)47SY^pHIRZJ7EicybT8?DOa<sN6N2DZsEP1v$QzV=0 z=_adt_%R`aWMyPGKoTT5<+!H=drU65CO|&^g`f|~VNV7EBtd`xQ7-wtSKU3s5w(CX z8M}+!)%De{-uu1Zd!^~o(aN8#{l}xfeM^X6ih*Aq_qT8b|Bj+9L<6NI+8N<mqJbN8 zpAn6WXlF$`C)}*)<U}JUI(gB^i%vl_3ZhdKjiTt3M581+Wzi@Li+bjTTM?ZR(HIe( zs%TVETM+G{_y+Tg3N<PkW8#x`NwmvUg;I@+=ULG>gW8H{k8tgXP-nRIENZKwJ<7G{ zZfYk`J0{xWTstmQMr=(Abxu6TBB$`~jA)<bcV~r~5^98BOw$*x)FyiW`%u|!X?tzC zuboiI$G%>30@?CA9p#0xZHIQfQaM>0NLwqpu9aH%bw_Ti!)trC+f{PMc69JVs^BJ- zmK~@?#|xAfIH9wr<n5KEd-r%Y?qCLb<T)+Btz-~t$J+?%@-~eiJNBX6wD(k{({)2< z$5qnw!E#nN(RxR<oOP#VhmP+_EZGj_W*F|gf9=}-{(k+y+1af7dgIz2f6%qI-@3N9 z7=KoMGwita%D32YvHY!tvQ~(ST4~z}R8;)vFjT?42T?gx2VpZj+)+{CK-=Diin5=7 zd4q~~YMH2f$7zN4odd^<@+}fni$<bf23Ho>60TqqMJR-RLxh$%!hJ?O&+sGE$r8^k z@e@lN+@e=^p4G)M8nMo8qwhn$&x-9_a-S31=sZuVwX!=eo`gB-h(T7MSu3?-?_VCO zu&X^8YHWzJF6~&ZEFu!2(;nVrNE<?x_&t8bYqL>1*q_i|!%moeTvOq`Ql7ki@XmEP zAMDsIRpVhuovt7J;EtT1JD#g8NOjP1yKQW)oVz&}Pqk`qtAN&atsC#Z<H^qt!%ghw zI}7sV${Rd%Z^l!q9mvp^YpgQT^Fz5tJNCkoFRbGi*3G-iwY)Au>`}!Dpnjq4wN#Yn zMp1bc`}wifzK(KhzVBiqvk*XJwSFP=Idpn)ed+zDW+6Ae|MW9w?J1O3E#m98?F|(? z?Q}!@^`173X4Q8NqpA}$S+Y%x^gY`2Dhg4avZ|R;>rJba(bITtQDyQYUAcuTID<j~ z53uPdfil=jEGn;Pjmx!cRBARoyQ7-TsM2h9{C3x+`)aef+qGTuh8I3iOkqrY9>sN< zfz+{T@$+|D!jv)lM=;><uEr8rY*>=MK;zF)L0>yCF$qxf?62YOU0liR^!%M>pER?7 z>;W90I^SjPi<s-Zfw}4V#N0Vl+@c2iD29aJfLUgQ%89MKI1gwys1JMl1`eSp41mX& z4YvUBPLLX?pW<@^m1t~$868JN3dC>&7!%+dl^!n{oVEW6>TrA5E_ZAlD7_%<R!aqe zPj@gZt-Q7ZR>gL7+jUd4_BGy+nE?)Wv_j`S)h0Ym<}o1PIzcG?b+#uc>>zlXDFPoe z9VXVu%{vN~*_JKaBfIMcsx1k2!vr`^xD5jrw!3e`5$tPwX9vw;-hofU&;ng&TVW3Q zU}GcxYF^En69N&G-I2bQw!Hh<?InpWHleFrbN$x#0tS&7Bap6d0|$)LU_Zruc$js^ zRf{}@zqX}X;R1t%EvcRiCJ|i-kF(YVbi8li@2cE`a544bBIaOtp|#AJ+BrGz)K%R$ zN?rS*Y^!w}zDdsAH;^!A5ULbE%H<YW9{q@9kj_286~NPzkps4yg7IC5?Xu9n%l^Si z{KE|qX6Y@U9WKFpO%T$vV}SPvmPME+Ei&E%^;-q;T!>@#23tjHd6W@HS@Fk~u+rB@ zIdQ>E3#eJC<(4&}P?Uqj+r76^CDCfP>p*=BteUMzMOrIG`DWnslpaH0Jx&FET2$P$ z0~-?61ZYt%RIVH4Xp?IBC>JPqJ<8EGMmbw=1W{popa158&N>`G5CRW^JjZjwX7fIt zf(aDDD(9-!IR96*s@9~{JDV)Hp0p+(zT?6Tt+r@M^dlgi$F+njn8$t`zOdkpx7f>W zG1@|gBxDvZY9J&$ZZQf#hLARiEJr^4lwNsp(6z7^M|tXX=h=pJTzEy(qBy)n8$)vx zhvzshi6`E?$c5-#6zdtWT@m^pgk@PF5C)o16w{ahRN+g{5?>r%6h|eo1;EJ>6~H2& zX2fw>99J;RE{3V_0FNx|ctlve-y&@QauIhZTl-%N?|rT*a!t(woE;U!G=|KWLA>vY za72VvafEVIY>kn2fuGKRomx-6B*?1>4E;MJ;tO8Sv^Xk@t#RV4Mrau4iFX+*Im0XY z6MTt<@c@u$n0(6a*W!!ttk8d9QE!MG>=av37U2Xn!@d5R#1BS`$D;}f29*~2J%MuT z|FObzu}Db@ypiC?GkCaFrMFWA!KjDao_KRu4aO>oa5|Cjl}s#Q@b12J@og)$GxlG@ zIzA^11{4`*lp^iA_B*IetwI{0iGhWv)TiojA9xI^ywN2ponQ>TyDHhaql%uA-yC6C zE1X-9bFmuZ7GN~zY<Rv_b9K2&xM#1g1JVMS(B`%>4)VlU<R~`%edz<nG^n&oOdTNH z^1YCFJ;ni-QyN|(go{Y~UIneUJA}}HK(I$*kqdI&c3i_HleYkB8iTkOI~p%E2uj~V zRDnhyFy}}i#*;Sigkg0^mM56}Lb?<HGxw%B*x$f`utON?&Tb*-f5Nw9`n`NNTwi>z zcL@%Q1|Wo!v$O)B{Q}sdFJ0`6(xYgs38tfh5wosQ_j1?U_WXTsw*4)=T#oW~&~lvK zq~jS14Gt*DmB?AmAljQ66wSUJfWd+`^)8c&B%-oU<=?gQx<U&mcCjT*SLxSzIlQlk zJdE?v1*#l<u&4C8>+drgibn3Mj;{~-f$3e84|ZH9j4B}VT|?J&79I6P>YWV^gQx^D z4sc!fqk<x8rJ{*glbC76orpa%XN?Nvy}fo+Cehhi+lQK{9HV@#s;|(%Mfyls0EQ&e z8lb}HKx5Rb9$)2cgqw(y{H_;9mJ=1?nu2lQQ5FJ?@{ny8K^8_C@(B7e=J^U&@Bs=j zmbETeuj87;m9;M6?-c%Cv?kyh3b@W?7V$i7japOL0-Q$GI-hydI)j?tXzDh|Ikm_G z(vQSMWOfNx@B#&(f{-V1$iWIZWc?=j;WTVJ1p06ra7B<tK#29kyTB+;`2CRaJC~yN z!OyAroo9c}Xbqsth{I{Zdw2!(4St&jXQ(3JEHQBYJRXC5fRzF#VJ<B&6VrvD!K?d6 zULAX~7-A34Qlo+8?KYAD;j}X8fT;cg*Ao*(+HXTF&>;sNTgCA_5QVn~rk2|1(xz%{ zQ!Lg&G$(^D(gF$^ddLC1F<L;tNv#LtWR{r@5^f@NH{E!G*xLIY6QlzcK3cuI_+Cu< z>hhl9umn29R}<UB3;_YBS*}S|K=0a^mLlBPFvuu5;0OGrYq^)kpf{uqX3awpn8ssK zLL!bm?PM8g=iYQoABRHJ1dOI;^)M1({N4nrhkKlVxhbz;wN^H~ivl}Mw2}i|{Vv^I zLy=nZ9dZk?HB<0u3^vOKtBFx*()1xSi%OV<H~kOsGPsIDR3^xxGFj^!tZ5`O3A?IV z6|$_%INqGWo3p5!$qW#(WR%pp2t>2EXwHE6*a6ZZY+%s)JI^LL?ZK90Ou^b6g)VX= z+>=W#fTzD5?0boxC)u23gv&XKQlf$eAnvJJ#(d^)V4~qK%(rN?zoK<8N6dmrj-`;m zFH_oHLMxInulm1nL&lD53)m>u27~_wlp7`p>%|QWl({1K9X^WqWR^G$Od93~UT26U zUg3YsyfantL%$&>bmE2wErS_fF=pfi=p{|VZ0DFw7|f04Lp_HC=ka|07??6NJ5$b+ zvvZZT+XI;b)B3l#3n*o>LNuU2A#Yp2gE+C2=skRD?D|a#&Ien-rVz&;FsR2ypOPG% z2?=-L#F&q8G}nTm5nte_ZoTWZIHTu#sa+*@%~2qw1=(!npfL~3`01}CUWu0;q&LRH zVZ7uev77loDULBsN{cY2mG}z!o5i()E08FRDw7@{HUl+>nk1pc3LcQpS&l&f$&-HE z^aFIQS*IFev1z>X-_sQ2`vtt%sMWiasw(Ge>mMK}w%y>vdffiMXl*wGDy>>c-^E`J z&olu>l;=){Zd5|Q88eP33vV2iHdM$UgML@2-%TohME$IF?yo<hnkA|!#>O{fUive< z{2N>>7BN!ERP$x5{36zV5nh@?w=vwE1#;kP`i92~O9h<31qnHL2SsXbe;~w9g;<C6 zAzH|Z-)Eb=1ARr14Au|pGyt+vD=eqOo!Bq>v=jS<pe)%h^5Mvf^Oj|kEn`NQ>>EQa z0qraIE#_^s@?_u6SiLj_Q{Uk<>cGaC4(Vd)o2la@jX>YN!Qc37zE2w3Li{ZGP`Y8j zL1;Anrx~$JX<)0DpABx#2E02+A!h2=BWpXd_OR-Q7}D$-h1xGF9}se4H+Me#SDKMT zUlzS9sh$n5x8FXpDm5$0ZTXHDBQC;zdn2ATo{8i6f*ItWX#y4s)MCc!jSo#y8y(6l zM#W|mnR}!=i*edD8Uw!w04GgD9F1O*M`4O+6#gDPk|yc~73WSCoSytO)O~=9)34Rh zLS{5GJ~r`ob+%d<e?!yT7OwP1Qn`gI_!@;enXNSmL+B&C0gi>Bl~6>w2OOuTJ-A7Z zc_@jPrVLS;9O+Q{a15I_9#s>$&<TT02Nbtc{Q*pZF1fzHjT9JX(PSH~AQ4+9X?%_o zO+<>9h!=GLY~zTv-<A?`;l3{^NmrM5adH!y;0stVA6s`#?mJ*XK}aP+MLt*n$a~$+ z8sbVoJ_80KDhjdy|BTuFw?v=nas^bxYrCL*NTh3c3kG}bcQ-cKWyC9`skpysr6fGL zmh%{6!AwTW$NcxVTyeQBROFe0oTIy6GaNBKMo*m%W!x<t4SVv#ltoeq*-`<n=6 z2G)Vq#@=OxCmRYsX&p>4TSJFkV4Ep{)UjaT^xzMB2*kme5ymM^z07fniC|OiK`@Y9 zL4XHf7Hr-47=4L&Kgb1;3YCOeU=MVMDB^9(4I+xbqsa>PF_THl0A_+nz+r7u2x2;; zn+M{8@u1Jh5cZC4L(4c_oKRqRZWbm!e8B6n%29R#tZWAxhAeHYLto8>PUxz+XLaLe z=jUu`YGUnz>ZGn7DU<DfD1QPIbC3aJVjVXzCcfFnM8LkGX7~_Fb1_Yd@zB7a73Epv zfO2x&dX<VL6ad8ux@VkVba;$M3uc#~;liLnBHaj22_@|`n|&DX!{RdhC;4~;<F3<E z3?|XYKqn5Y4P{3L7}?HGm5Jf8vMaO@S`7k!3RO}t<rVVcY`pzXTqZAC8V3bO(3`1d zX5duEGrbG1T5PIYB+68`{s<5MZ@TrmICzcJizz>=FVb;fs?b|&SA+5!t-E?hxkya0 z1sO3&8z&$w2jM7As-4gP2dQAHBLvZ@jx69I){)D}k*W|g!y00V)MIiM2bw@T0&hkd z5?KeQtH#8yP>r@i0_N4K5r=O-#_*|Pq+Ny-L$6V*-$YT%^%bL02600d$S2CtRIzp& z9cB%IgAzqrBWbkAr5a)x4FT~Q){%4xFX;#w2y!2+BZgW*?|?!;3NkrR4}?BAz^<Dx z<uY3&a?u<Onb&AX7CB2SZe@2CyWGc@w}y_Z38t~FIQa*bAf`9FUth(UZgxmKgF@*W z^MJP}S<pEtB|UAN12>y9Bowkuk!6zsBb~tMY;R`BWcoc?WGD0^)vDrRdHe!5uo-+< zjK`Pzc#KLSWKd*4G9Cz$-;E)e`XF9Jzy{d;jP5r~76@5$9`5E7Zvv?w<mHh*+NMkm zr~8o$(!a;@21no_N#t)BOPXO}1MDl*0>+di$$w@fUCZ|VZbc!PN`jpWVh5P!M+QI~ zt=Ktp#s>d<fE+LrbL<6tneWn}pMpnU#V+;zC=LYe6u}d3@rtAkr~+oTCC=qHkQg0q zl@4RrUEdYxMTmU4He#ZuALAi4AEa50#SQ4kC)9w=r$l!YXz5=^aYAQWR7?9gq%(RG zb#IZXT*Qqik6V+Bu;W%4iH;EjF8K^0?J0JYt|f&f48yuZ^8~~zp#xBwG@!x0Q}|;3 zU%_5oO5*H<JrGZWykmFT2fS=NG8t!Z8AS|u7fx}CCmo>yxj{Gh2;GOJKu+*uk^&(s zLW0>7Mgm<5PV;`iP7*$n%_;<XUU_-oC`WxiN=6phe&UGuZk^t~BtH7sfChv3erf{1 zgv`L<BTM@{l_Lr9?L>%7M~J8nh|ruCL##HP3%Jam`(mZ9r6_NbW|Symu};T+1k=IH zW?*E4LS^B_euI(t#J4jllRSTvd^O}be?p#i&?Ihtx=3+!igz0Bi^vLz!OR!mGtd>| z5k5(#fMCy^HpOzSG8ATM%B*Xf3UVD9Uo6-Nct-8g<GGUse-Q;kzr@9mTCIYEOjjqW zqt%PmJkIT~*7IiM?^EBWC=kz4q-lrdRDv+oBrz!{G?`4Y*Jw13_Ug?pLJwW0w-qX; zQ8e-(q&O`o-h0qL`7nnPWUo=_e*7;6SP>Z}cr~YeLR9*|{EM0o&!THU!xNLLT6JqW PTb(Rqr*rh*#lQSNYB63I diff --git a/resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc deleted file mode 100644 index c976f6f4650780c5745425975ff709a31937ca5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10050 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 SUi`)48;ka0Z}9@N^#28zz&&mN diff --git a/resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc deleted file mode 100644 index f9a42314d9662e22c2aa38fff07809f81db17f50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10216 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc deleted file mode 100644 index 943ab32c895bf467e60af7587acf090672c40d08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20902 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc deleted file mode 100644 index a1031ba9cedbd41184ef7bbc08feb167385d2f96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17989 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 R_9DM4v$yzceW8!s{|97&yEgy; diff --git a/resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc deleted file mode 100644 index f970678fbfeb6d5c63c1baf98504447dddb60746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10727 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 QqHwsdw}#2fO7JZH7m7(;rT_o{ diff --git a/resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc deleted file mode 100644 index 703d588251c17e036a3bfde0b1b41be9202d3afd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28214 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 Cy}z6Q diff --git a/resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc deleted file mode 100644 index 43ede4e2c474159647db96d95d9be8b501954a47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3561 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc deleted file mode 100644 index 8c719f0ede6e8b0bf573377a20e0fd3b1884bb06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2904 zcmZuz&2Jn_5wD)vo&E6G-h|-9F9b#p!pnwuBl3tutblAq@5Pp0OpwK8WN9?s>9xn6 zkEOde#w%MwyvGd*i9dlGM<h7FUxMb!$v4iN_*KttytZg*YO1QMtE=m)du@5S`=`VI z{HnV|^lw@i4d7e&^gjR+q7YC};!xt!kxL=)1|?0(916koC}~mVQr@7jL4xPnlr<^$ zDD-N&Ls^URHid1<I}~;(?^4*U=em?FQSMXdlX#mfQR34#*w820p|7#qGQCZfDOs`H z6_TFiu7JDBX0faw*|ywOlB*Q<*tq?EZ~D)7WpOIcKhweXBuUFa8ztjh2WP1{4z}Na z@!>^a;v+ric5ex$I*5Z0Qk@5KG)jl5EKHD<T4z#Y#_AfD`B;H0RglOJGAqY2=^ma3 z?_y(uZGD`@MFPcNVm>dG4ANp$sysGnSv(80l)<qv<IU&K&(6*UxiwQ;#RDjQKIm3{ zZIZT@SB+ncWl?#F%%qW52n+393BbOn+OkMAp9pw&-=1(6d-dI_V~_Y{n#rmul`55T z>m|)$5}SC)YUYELZQ;{B0Ew6*$bbm1fGeHq{-QZMGESuWf>rKepLP@#aW12%>PAss zCX<YDKZ<@miL-j<FQitX*n<DQe6xA*{@fgw#pf>%zDf@dbZX?rI3AwHM^Yc;69h#T z2RFPjK37OnddMa^0KTyFjWaNRiv<P%x6Tl{vjj%IAWJz(o18mglM{Hq->Q0xCu9%C zFX5D$c`Jh!gNYai7~96E^l)ON-mDo<gW+*p6f)Dn#zs&~@<XYDax~AL*_>VbG{|Ie zgp`Ef2+^98X@Nu;mI&{&Am)r5Wo2w;OLUx%;b#>a$?NK*w9K?P3M?DzccwG(!_??# zELBvm<0Z@rzzS;COb}VGk)tf||Ia=XSTrekB9AtM&j+tw^{tj6M}kKipNH=aKK*yN z^Fq*xE!eLGT{>h0T{r|BS$67@{xOp$NIJ{()z8QvZZ1pwPLOHPNs}e&=>6WIbxd3k zMIOTwG$M~qo233srbQ=h?n^8UM39ws=)j{(mo6K02~Akpv9N355*2@2*aVr;sh7c4 zZ>|!eP-)xLT%~hE$876mE4M=EQrB|W6kE@1?O9tNiDaEkDqhX`cbTu>ikaBAtk`>T zf#Wsk)FJg=BzNo*aPBbz$p)YP+w{nRFOoHDkZ%>j();XN^;dC$69I3`@dxza!lio` z4Z45XA_I$C>}lS2i5}t?`P>Bd`=9JxeZ>)M0KD6N6HM2O(jXsiB?#y470X<4%+o)h z;-{_GySuwPJG)muWj^P_)O`)I%Ov+2kh{BA510%9(gLee3IwIE8T;1?lRqFj!X(Q3 zk6SeYT=7<_36Ron8`NK^8ffoH<)WIZt~RkUwlCU7p?JfpGttMTG7&mrYnI2Ahg)GX zG?h0gxCMo)w@M{!&kMO)S}3~8tBba3+N$igtA>`@sA{nn%EOb&JGNai>>Xz1@U$+z zkwTvg?~c+U<@h^=#;y(#&}}yfeRDP>C=W*UMa>7Qb$$ihal(I4x2WMLz;%+aHNl z@u~2|cf_i@>8uM6l*g3li0^{maXgDZ6icEdrt7zx-(aqB$TtHI{<FtzEq?+8Ma8Lt zq6VyShej@~;l9|LKDm_~j|VSC&jN<mP3>`l2Gw`f_n=LEpTVa9eW4yR{_(=7&*+WE zDlN<orfedTaK-e-wZu((P)qIgJ>|2KWd_J0s#;N$l*1@ePngwbaJ_@Y*p=!I0NbNE zhyAYS_>U}a2=e1_fo<W_KLJ?0X(S#jxLWLM=?KrGCcjtEz)-p05Eob2svjqwVt-CM zwr@X=$8x$<qrcezG2{(+@c*OJ0}$LF=M9v*CtQzS2@aq`_xJnm-gTKa2G9lnKEgu9 z=c$@mc_tO#*_@_)H>)4=<Yx@7-C>iesfT#D{1P+v#wBNZcQH!_c6zVhzU64^t*RMu zG%7F3(_HGxM>(2wm`mLHiQ2fi+qLuAC+~Pe>wF}v@AU7er{Hk-;w(e>f|}wAiA{Z~ z*6hikT0FNrqxkS2UMTK4s>6Vtc|99VOq$uYP&Wt;ybX%t+iSrYAI+=Uuj|*%&zYs~ W0eH@;<2yakLuvKgwZ__Dt@A&tI-YX? diff --git a/resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc deleted file mode 100644 index 8a0be64316ee96d34ca96f91a8414a6ead76495b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9514 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 RoN{S$GBf_4<41Fu{{bT;EF=H` diff --git a/resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc deleted file mode 100644 index cf4ca9e5582986eebe0a4d9b7941a217ba4bd059..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8017 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc deleted file mode 100644 index 0eb57b0c6c9310509e2fb8e83ed55f48342796ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16965 zcmcIsTWlQHc|NoET`on6qDWB}Yb2YZ){<z)m#DC1)v|0k3LMH2Rm)yW*bH}PmrL!= zl4oW`a%(zi<RlHyHcfyOeMui$<e>rjkQPmW6uks!(DtbheH-MZ4NyQp(F^)!G*G|q zKQpsSN_OG^U26`{oH>_&&iViEznn9k86PkG?A!nKYrp?lrGBbLep%#S#^wJ#l7>=M zq?T%=RL4?PWK2G#s;M}iR@JoR)2fqE-K?r+mBo4))yb)DURCp|TTs=4>K0YCh-X>V z$f-_Ab;ne7Om)kuT2|e0RUKE|eX6=obthDHLRqgj@~Tl#AAo{_awnBrRQEEfx?jEC zD5^$D9+Z?jrS5@*11KF+jk1)Mm7A5)X;Qvi8FyZ=l)COZ4cqY=_NHzJu6?c5vRB%H zA86O<`to4iX}SJlsdQtt?c3d6qu+6DJZ+e#(hOPK@2~i7VE3AKu)3!oyV=uSCn()@ zwcqY}_S1{@)xd7_T;G1u>jh6rcXm&Y(C}7IuVA=lyW{#N>??Q{tUAHj(sr+J`>VZv zr(ySf*Y5TMQ0Fc7TCKa%BBrz5UGKPE*9)A0BciJn$9Eex@+9y2mCN?C&p-RD?FV>w zM>jxXACmwgSA$^vg{Pj{+}vE;YOk*@_H^qhbpKSNSAR-3>uj|cYz2#@```t&RI$Rs z6;OF&d)=+1!)(3b1Ws7iuHPQGwUzC_^~3V#44JQK-P89m>8g?q3-wp{@anZNuU+l5 zeK*Y1S9LASv<L0=Ft?>0ujSS`Nc>~VmvQ+sNL;WX_z9^n9odbm3KConGdRqpH0x-u zJD7N{Wy=$PajB9C3$>c(blqAlEY)geRgo{(Y8!p06Fte-YIy5f?T?k-j}ayL@a*Lm zZoRo3toFQTpTG4+d*zl7zB#w<)bEf_Zbd6{3sbhZzOBn>ca$V&kd&=lN=kOURSr*H zW|cnP?~!j+&G6kIK}>U!LI{vAs0O$kd=CjIF>j>k14t#<J{A-Rk~^-}M2Zq2fh;AW zQH2=oSB-Ji*hfjZhvrkNfeMqng%t5A{!NJ-9f&iKql4;R%BoIFg_*c~HqM~JA*pas zD$K>@hvN(?9FYn$%0;WAs&OnP;E<F&rW(gp!&Z1BQZ=Vsv^}93C*x{Iq#8Kjan*Q2 zHRe_0l=>hom5)m01=TnmS3V|{E2{BiT!Q}7V45>w9^CRK7SNf&LS3@SEf90E5XIj< z4KcMnx9<ABquZTr%DbuJdi7odf<*>!g9_x&#_y782C;MORbYjy2@KDC+7^L03u)_g zdYfprvMuu1alKZs8i9xOKrwo_9!sMkZa|FdJr5k-uLr;fjMZx2ZF?AzsBx~-zT?`x zl{L2>*z3Kv7f=9#o(u=6*MSTJsvIwQ&qe!EGXSlGh&_Eb+g{sW1;y=vL&ZHxC!@W_ z0O+|}7#DLF&qxsH_DUZT|ALvKC+!#x&z+0q)VxezyUDZD^}g1CF;01B)7kdzrz<k% zlOO?-M{q-XXT|Sz(0SZZ^6YfOZ94r<V4wR`+|GA(+v%`Xr`KxNk&RFydf@fDE0}|% zWip`eZuGI-E}IHhBn45E&33Sg1=!Q=!yWaSO{xlX6XnnhQ%XkLbbO-?n!48=(y-L? z+?f7)kJyfPMf<kVHXX<3WRn98!pGg=Z&E#><A&}hotZ|y%tjlXfFHE88TUnY8xryE z?TT5tr5;A7KH~Tb@c;|ti>A}@-Lt|#2?s3rtV9OvyNv}|y>;DNcXhB01%^Ra&>Lv7 zPy0Qa6SLBTENrg2-jE5&yqq9EmawZ}+=m$*6!`u1^-dd%8gb<a9~-)ymEK)fsBjXV zv^U$G4(WjCbjal1P+zv8>nPKj>SyUjJ%vB~m2mCDmB!`&8y>cldQYjEQokui(w&0R zfIOv^alZxtGmlezye7sGAZ;h5f{c1EMZ^N4-$^NbO$Ax?WlL?pq=KAU1B$1mLQ35# zs2$`nMnTe|q$NqmBrU6*jM~Yno8G+2u=TjQQ&RfRlx0}~@Lh(Q!C2`39nVs4zb885 zPFm@oDptrTQ<9Jjo9$3Dt)ey<dksnlgrY@Oy3BK66h<}hY$wO5*vUsP`W~z<yl6pQ zw7@({*!~r1k5|dYjkoa1`_y|<>DAj$sU5r_C@QM+=o5dSz3xB{U$frJtF;L}LvNND za^sqH69xJY_B=GxGpW9&R1<S7=HOSc6nXV3Ns$Gk>JuZ8ucFL@ywLcsLgPM;gDK-; zRwkD2AHk9fI}Az}p7URMW#j7A_A7&vea~5e)&S$tKD3*C@RULS7wr}l$h;Oa09s{v za7?Pq>$A4IwGNC1)A0r}_4L;K)?g+E$9Zk{J@Rx<qt4*5J!9Z;$Ei~~M6=v~0Lre^ z$*KI2RiV$+0bKrZB!)$AA5=S*tPbWuH0w80Y9ptX#lnF_mmzVLRG3<PI<z_{$Wx_2 znHW203O#*Ty3he&Q1~x)**(W9s=*VHB3-Q2pfQ44ZSg{<2axqIA`>@wh`?p`a1=>I z;1mfAK7zV5v}GlwXHh-0)`zB<CSCe45)W5Q(4jpuCaXeVtLzW+5&VUj!0mLxoPlFa z^HOt-!@_IVu87qd=8SF%bA6AE!hEzq;dCu}<613%4P0w>oR%LJVhx0bqHn@NtS!U> z42xVZM{8$0%sK1puGfG?93gw96lQ$4(*%5ghMZxcYNpph$LYGRSJ79C#=ReAgI?ft zs(Hf%ezj~ek>PFCqZO5&WPWJ)nuU3dlk_qYWffCpE0dZ?O;}SfQPb&{tV!!2%5v5* zlpRRrkmIvS>ws0V%GNm29MWRysCCpDOzvIt1S825W4JEk^3NmLJ}htnBU2U^!iEe` zZ~3M-1H`ffuAU)W0eF@Lq9EQd>j&p=Z1xgE1$GsT1ez&6jQB_mZb$x8QY_Zel_fB8 zR@}63-=!GwWl_OIXFyY!g}#Ankl6TUqH?vGy8(ZIq!*B=RN6X>mmcgJzAh^y%$!D@ zA};>*e;o;40mK8ofxE^sH#Rw7tW_k3N0<cDeM}LPEP)>A1fUVPA0Q0ef$9K0t)(I@ z5u^#EP!WV^0HCOj;jN{b88x$9*|(%02MKzf$tflaOb7z{NhD!0QORMp+17p#W@ttv zZ^Na_;}YzsAQOq^aS{$71hIxhB1lR#Q)(tck%~3Q&igOTYXV2hg$o+!SY9)7W68V^ z!9;|12V=xSJAxwGUDuCBk-_l`^S(XrpS9;{NW-+G-bOx>>Wd#*b|nwlhFI!x^d4r` zU<ik4zuyhhU1wcnTpz=eN;={MlIMrHyH2O?`mykkNfO~P^bANWojWChF=wT%bGrn= zR7+-)GfCdKjLUBUpD}k_Sm6+A9V{<W(e7)|(#U5eg}awl_X-MaFz;9caL3l`@(3F2 z_61r50dy-gCF-HO3|He8w6GWguq@WVUg#^MrUMuWqKs!z(b~A8ZbIL$l_SPN#~gNC zmGxh<2o59I6*bLm1A2d4eI=#lSP^VlQs|H43J0PdJ2ekXk=#H_qf_a}ibj)~#P><H zwqLrMQo#YWHZAl+*QV42A~`QZ=d<`=r03U0YSA3PyMm-KAqk3qmIY;&k;Yl{j{<1x z!S7vngTAI;1PG_56qk*eC{`necjTop5+tx;EM4Qvy+v*i#Gr{Qu?m=o#`I=omIl2Z z0!QLuHR&Q@Jh2+tL*y!Q9Aiqda8OK4hr}unn?fHXk5Gj__#Y%DtClq51RY|_gD*b# zVp`p&mby=EcmDx?ns8p3(I@aH%uoW1{6B&Fy&yo24|9$7UBo?%3{bu``BPtDA^=f< zPV(u}ggN+fR8W~gr@ev@kFG!aOjryMpz%8n{6!KK<{38Ibk@UC(5v;~Z`M}aO5TW7 zhO<;1i*yrj!YopVVP>P>3&ON^yZRhQ-i^WfQz-i~F8?GFRXAilmd-&79kGr>?B<Z0 z10YXY6Pd@;N34_9U_6Fq!TWk_2W8Qimq_0|WFU87kgXSlIbrb8)`TSvo{V7@z#Yj^ znxX_=qG6m?v**!pV^ZBDTc;r_b&3|Gj;1^EV8XQe6~lc7r`aXy0?S2E*j_j@JJ^4N z%G+*g>M+L37WFiU69$mw7JjJY1fulod?c19mq)+E<RX$vHkxGipx<Qi=b2n*vS&VN zXZ;Z_e-eqxn7N#?X3{0<@p3$S?5WOG<&Op=^Ote?pGJbo!wX!bD}r}E$+yek^)Nrz zQt*Df$&r$Y&#?&VV!4tsEL^cPd9sobs;g<RO_&DWhMBJ8-`PWO6~Q@pgkn_vDaRK@ zEY`<VYA_YkJFJu}IhQ`6nmWt?E>}Y0g;)$BiS>Z~tQr=eLcq4|z)(1<rDPsyUxtMX z9jCj}a4!B3cRnsPw1479bt+H(_&1?~b8a}n%-}8(tZ-^*NXD-m8z|P@J@cY(qOZTi zB?N?NgeeEl-+(zJnmkf~gr!8ugUb+5plP(O-Mj5x-vk^MKk<YShN216Zz2CNb0t*% zO-$t{n-m$w(r<G#W2WhMINBbD<~;l@F0&Ae5~C*?k4!}iVK4>@;;>8--v7k!exJ+v zN#6bMV#+?*yT8j}ezAA|aXiA{+~~VU0iln1b#f(tgvG={{|;^-(~#_~-&Na_lo>EA z^g0mmrgtDQ&=+Z-zxj^0fB-^9Y!?{r@aQk84T(w6jzM`wje*m_<`%F41-00u;X|0N z(zQjAF!d2ikXEw>eAx=^&x0IvdlR_2T*)}6LHTtzinvKg2yvTE&udYm!}WG-AO3W^ z$q-Py<R-uuMIxzuBX>3lKOq|Bp+2=8w|KC@b7n<0WX`ONv|47^l*7PF7o#DfVnO1m z3uoc>M<otC|2?!N)&OYSO_#l3T*QS%`x+gQsAU|IYPY<eb{ivf?4n*Gv2n0V2r=vq zNNqO~hv`LxdAAc1dUb?V<wYdi3wwoj4|YkC{i~0f!=#ItI`hIH&tAgZH`Wl(qi31~ z)Vc_M$J7~H$xHeWG-6QC#v6*vwqR3me=YW&Uqm<)q2IWoZAL?w#A_tIAZUp2f(dM2 zzJ7URo){za%%+bmuk72W-Nn`-hV#xrcZj7YQA=)qRS<4BQ4S$K=^s(Sj@x`Vh`tVU z2Ck>sP>VvyBov+`-bTiA8)t{z^aFX1wr5sGCL^9j82~nugJAt>n2YFGV#3Y7S7(en z=|wgTxfxMk<@T8LC?No$U5p4sNXg>-4ULGUN<mW#YMaR^Bn&aQcl0vv7nppC3H?JD z6bw9=T3KPKFlT0-mSxM`1Hxe;=KFs@GoL1@I+%k;l!j?Gj)1}hLJHGX5y6AQBR*2x z=t=^P#C%?tartjTGXoj`0CO-djB_G4K%=xcLSmKacavH$Mt)VnJtlfcxQ@-o(LAI4 z_vHb6BYEIG&PMwE(WbZo_+XDMOJ581tSz`fv<-j2h$>(az7r@R&i#q>1|KAeTzY3| z|FDKZ!hez)0JLEZ1R}+PB%ZM%$o(0|#HQOEojnx6*~9B{Y;QIqXILCjMJi=y4^e~< zFh!;rdM=6M!=NTR*!T(%Gv50~8<x`>`r+kx&j?7#fE>}nILt;I#7-DKqL>Jh?UpOL zQs7mpT14(mU_G`yz?Kr$H(=yo-z?rx7)k^2hQcybRMG@q#%M7&G)B0Yw%TAW9!7RM zMok>y_QA2y&5)tEM4>hP3i~(`3*qi9A4U)KU~UDm+c;PkA&vwVbe#n7M>~KQtq%VX z67b3GeRL^c7-OB@vNo_FG<vNi5tb<Yp^u{O9_jh8cmX^32m<Si6X;(eAUQRdk8N9c zQ&Bu)B$67pSlUHW2E9|B9}4w?6dLHl8A*^vrUGWh1+@4W`Xn~9Kc);3-cuh-SrX$F zhtznbg^>W%^B65G4Lxe{HMRJkbW8PBQj-g_!gCUJ1)q(ymr8Ic_D-X?ozX=7Cm#Dx zB2k5L0U*XI8MQ<#)0#|~zl>{^Qbp@XYSub4;!6zYJMn-@=qbUa!<Y&zH|JAu2GR_n z1?Er~O#p9rpBA|zWV|7bgh9{_w#*y=0H)D$1M&<&khOuC#RyMUZGD%$zk2&S+)RNR zc~ACJ5amE4>|E54A`j3&9x3twBjk}H57<FIueO%twt$W<a|4E8la-K-?65FSf+B`+ z<Q~shl)|nHJzi|F6d2{fzKa0L_u=ybY*1jLIUB1e$f&YEV<8ZP-4Q%puH>9IFhOr{ z^O;Bz^RA7pUkXtjr%*(~>0*l25m`)0EQVt3CmJlW<t56Ct<gx^Vxt?IgT}xzD1vh_ ziS1&_salh6pu_9Eb((~y6I)_01)_^_OsBpY?=ez^!iF#{gf1M~@fHGGYM~s|t+=bs zU05*OZhs-#+GaccVnG?5h|>{|o=qU;rCz6jX@VK_?$fsyW8g8nz3k=DB2FX$uYeP8 zf81t?1Pp$jblW4+{ppzK+9=hvKyTOJI07&4x+6F=ly-E19X%vV6&(J-->^K|fiW=% zHO35~bZTOOiA{-AOKgKkfH=L6k!bjsIDHA*9vxr8AYn0?uIiY{%F2WVsF|9CmWs?7 z{79oln8v+9-fJ9iw+9&>{^%Dyz<D`|Hfn4lbt*NBy%5>;0G62lCK2?Vw)R`&slmyg zKW}Y?2tZpQgi$9_^T&`|5}1MrQR;ynhjC3OU;r<S@-HDzh<6Qvt6!zp^y=;N7Is-F z@rc#}h6InSt;e_n3T1^9I0F$CK^Nx=_eR9AlWbZ-(6O6~Y56Yq6ii<bcMxC-O$j2m z**T!Z`Sb<vTTv|phv)Q9B(@Bq08^vQO+4)18IC2%d5H=cLJ_nzh(a_0eP$DP_>wTe zN<hb)DzhcP2?b*)08tr4d1V5Sns+wHWpQjlC74*G`$|k>nRMq#H)aQ~yn&$i#yJ3& zbfDk2b`c>6Htx))VS_%xOu`aJphMPvwEZ+<Cf$I2<8)d*94uVz8i$B_TO(u|b;G#C zI?h6DwSg#ZXXv0sr&el$Q2;&pds@>?0fsetoA5e}$KzoW5m75xT%Dkeb0IM264ZO< z`R6N(_GfU=2%D8>d2&ZEPP`>}s6Cox^uEv)Tn_B{5}SKT^IfFbyGS~No?18|Rnt98 zL>sTc?!*IMHD@b`T&9jKbvEV9qB%u`nTssW)8gLYa7$beIYbE!!(&JEvw*ia17u@6 zM<{=hBNLsYqb6bz#siu2O(X+;m6@v%dSo{IfTuphO!awgCN@=D?x+TkxJuW>iN6LM z{6-&#z+!3<xs`obbI4LotVA!y1(Qw6<W2leyS2)5OdUtVg#z91O-X!wLWyf33w;)6 zU*L#>uu&D};uL`R?A;!g+bkS6K_kTpB?MxkF!FHo;!Rm#){7l=^S<CB1TheJFh`me z;An#uoFengSygjrG$Fq4anBgXrtn&bL~vI5Gm&aReKEeT42`~z%vDN81BlNlxG%<^ z_@Tq-_B?<wOt1IWMM+eOCIrql)$%AOR>wvdF)Y<r``#Uq8pPs(12hz?IkP4>So0A2 z>n6&cBg{XCj5@e4m4iuGgo!u_fAxg*gf*SVHn(-qDyQhn76JCtFd?V#cOPm3_@`sw zA60h=z*E`eaiwwjKZei1kec9h8ICe<(zLG1uyC>a0Mx||O)01Wm}0;<Xo0P(bS6=e zCxzr+(ry+OB>ug6dtSEuvZ5uRD6(QAMx6i~ai3KiZvpqBGSm@;F-MazDuY6Z%8-l7 z0N+s=awsbZGPA6x;<iL<(-trdsAs)0$^hM{2M3%WjvW2mB3|{3W$4EV(>a)v)HXnE zxWi{GoH%98{b(~5?~aSn`c;`JI<WxpWDc1T*gr(;)*MXAD7_;`fUp;cgGiXmf|^D< z%foJusRB0m3)zeuXLA;&)gh=esz%ggn?1#%J#hXguXeEQ882ZnJc#%I#lq1gEZx=z zvRY!N%hLF=G-GN98?9q<Y^kVbcgCfgGT(k@AGbo`0q`+4L~#OYT;0o<&D1I0P_r|N zie1+O>OE;>p%^u1K@SXcwvon%&ck}&_;2gxE+U1;=!N_!#Xis{_JpH8G2K&G2e1yd zYH@P_H&{UIbnQ$ko-vpf((rJSr{^uq!E$BV`5uJkx@%%!iA4s)M*uZ3zvLF$D+;TP zsK#Lnar<dP^Pr;f8RYa7bx9nKg9b%(7b<3Z2tC6=2yTv#J$+yrfL{6{t}f7NSTI2i zxDLsXyMqu>;4lgVFf0*cVuYAbIn)w~2Z7+DDN0a~gUP8Mp7R$-Hz5jkmc%#s1%Uz0 zVe~of*c_HOpCK4vk7HwmX+%e(gU8!ApF4Do{C<5^)L49mG74gkhOXlg2S?rRI*wbK z-X-kAAfjk(w_&!O>EYI~s`3L;{W2%VB#1cbZXmv}imr^&ot)4577>?2Ae``cjGs6$ z!HDW3y$hpaUSm(cTv9n`woX^rg{U4n^_n{$Cb+<jP0bTZ#w!v3M6?q%kwhiT)_9Az zjFM{F@FskEi#Z9OegV14e)Gx_Lv8aBEK;Rq0O}*0KXErS;s-dhFY8fjw42T9xH)`Q zGcojPnG8;MriPeUwGeZ!QDPjwT9l_WG)BTB)Hm^*%XTF`u8?Wn;FNzBPu2KgtDL4P zJB4dHH4TLprPcxz+FWJ^-pwqoMeMcbtjDl3%;?FC1anK)!OS7a?Y9OKdzP-wCFPGA z?{Qq0arxUAc^e=iF%u||2zYLK$CLe}=eeI0RS>MQ%;8it0ZK{qcN@TGyc`MQBI<I} zdkh`G(ZB)MK0AODfDi@%6(}t2aR3%=2x_?)+o4Wzg9r<Ew*c~{vm82`LT5Oaiq3w( z&fY;p_icI!P%Q9;pq+jqK1EUKe`aPG7rOBfvX#s(>bKvP&dt_YPHkO^n);V44w{r7 zmvS=#(E0qXryFx}pcR8`SdzmTxibloHPA@s1!v@VaIPej2g5>~?FC`Wm7?<u=Ko52 zcVHEUVq`Z)8yCqI)DWej$Ol1~abDs@>||^aYH5wf`xCN>g41L8u&HUj5E!B?+zvq~ zIq*uX++6LUOXeQhmlInXHa|YD;`0PO<G>I0NAP)$+3PUhHk{=)U0gRoSjtu0CVIou zqMO_vPKe_>4Y9H7t8|P+@$geCoPitam-`CShRp>e$e8bU!0yX~<HPf$BoytSnPi{M z93fh2D;&px+&_3w8Pm7WHq2kWCUFcoSQ_SF#W&n7El|WGWSZh)jN((w2@2WB;rL9l z5hH>hW~FJhAgA3h0oCFPk1X(Y#n#;$+$fU78mIC5cnhB}s>*3d{5VY5GB#)qK(_Zo zwz*wH12>BpN72fpim4nhB8@uyk0(D%Rsqj>4fe-!pY&ciE(h#1&%^5pCOqu$(I=1u zKm}u;X1(W-;PkD0til7TtB$`~t6@7C&xoryP8)qNU;vk=0t`+kupO4+#|7=Wan$q| zSi@!0u{vJgkBWIPSZsc+ncHMS$Q(&TNgY7hNnHMCkd)_gasrlUDF3A<&X)5yGg~O1 zn3^c()0nX`oQA!Brxw!I)Z<ep(r7jH;?&Es#C3F-{HQ`+#^v(_HK={~gSNe&egVr6 zBgUs+i0uIGatcvreg+1MO*Vx-z*kaoPTqV9$*;oj2>;Ah5E>%Gqc7v(7*_ELgqvUP z4W=)7wge~Ib>U@7{o>MK0x^INzL8-lj$Z)|_Ky~9okLLq`g|Ggg$U^XBQmsT<#;_j z8|3NSq|`UQ$GyDmi=qzz&cJt54IU2QUcyn#Kn>t2n!|gMTSmC0vUSs20pj6&KCt>b z%7QAe1}Aw4k`=|(035E3QE}i^=V-Lvd<O<C{-N&sY4x^u0v(j4gTIPZ4`xl2L99G@ z-r)1s-$uGz$qr7v(c_7J!NL(2If?_ZoZk#ChAF)wps_SK;$dH!!JTz{ZZ!NdNxrkh z>=)uTVTN{Fn5|t!zxeo(X|{HQhsZPdxYNfGDC~v@HRk2$xJCdG(oz#hfiba-)8_70 z9jCf!e&Shlv`T*wd2@u^dT7fw)4}`v2Qgt9wN$zY83(sohky-J)>D*np6b35@y8;o zKYFEqgyv>-`N0bgYhn?gv1L&avoi|Zq5}&S5TD~4Ul$fQCRI^G)|~5;Ava^$qZKyV z_}jx31_z*XC{Q`_ilycXXOk5c78_k*qZI!<USSA`#2SYyY{?39V>CZ#Uw6CsFv#XS zo5ND(`=SxM9l8yR8ZR7mo@_*z-A@LwBs8O8t;+{%oHHm&RKusbBT<v3L0YezNla}l z0(FhqxuXz4P?Do*0!%nZZEfv^D4u*OrLchbYd*V37OUa%`>;_%+MHMu&ir`B95Y;l zb-=OsdWT;qe2Muy*o&VM4mnVN1?3ukxzb-_Vk3Eo^E4dDz2J(Ir$XQVCft`JdXA^b zfyZL*<JrDFFZ9ri3DWRjx8XUgYs2ih!U86B_;cnF;}@JpL&Ry4(}xT5FJYWyytqQL z1DHjfSzI#W50H5z$459POb$LAd`WF%8y_qyhAX(10G6<~v0T3l_J*rZ05eDjCeP4% zgyBi{#ztWTV1~Uj>itXvVE7mc2>6FFd&3gl17MKBG&k(fkM*{2Rd&!^rmyXg&FL-w z|MK`R&TwDCoN98N{w*eA77^zj#bsp7p6^EIUWH-#B~PP)n1L~v9>sMTA1{7@D?WpM z6?qQ(!91ig8}Zzs)NagU18ioI!S)(9zriw0tDz5I+--O9y};L5^4m;)hsigYJj%S- z!2^CV=+oHXSK-F6D5uK%CZ@}2&FAn~#fCb;PDIS;y@mPc19MHj4aejE0YI&fV6mKH zHGU}>7DCG}ik!e0m@=J3_Eusdt}miQ(PVEiN3%x;e-za?`D(tCzsOe-y^+H=Lii7% Q(WBPs^jvP{f%@_P0W}0OjQ{`u diff --git a/resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc deleted file mode 100644 index ab7dadf0f0e45087906650fa6558056a002dc4ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4862 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 F>%R{d_Jsfd diff --git a/resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc deleted file mode 100644 index fa6e46fa18b4658ac155f038b62d93a06acf2aee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4763 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc deleted file mode 100644 index 9cb1566580dc286aebb8d1aaf8e69c4600458c69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4600 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 N;`i3-hpQ`=`9J>^?#=)J diff --git a/resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc deleted file mode 100644 index a7a4e55736d3e214718eae3c2d039db2a76f609c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4781 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 Wc_iH{cs6n?dHmg2xw*3J<o*kMv;RQ= diff --git a/resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc deleted file mode 100644 index 9acf6dc536fafd567cf5ae3c705815bf35d5b8ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4602 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 diff --git a/resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc deleted file mode 100644 index 0bcbbb85864a5bceedba00e8f3b89b42776baef1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2545 zcmZuyTW=dh6h6DQ*Y?J)LurFbp$vs0T&1o^sVbGKN?VaOQk7JV5Rw&XwegPaP4{AE z#tpHO7gGKJZ~TP*3H}6c`^p2#8_zuPo$)1U#Y%H_&UMb5+pIL3{%?E#{P1Uu=r20= zm9byP(r*DGqAs9AkxNmD1|{lZU#6%+iA!A&J&LN7lqfAzw@eQ6YLrwc^{DGj@j4|{ zN^8`uQCg>Nol>8=J~>Y#pQ0stg9DaG*69S+m*{EKpr~o7O_H9aHbA|=Zm_IFvSz7Z zH>nGHg?8Gl@OOu3dt~Bt<K^~K5ssrc7uqNprdk}tW+)!L`2O{pFyTOVeE&=`)*=*1 zu5}_cwpOo~ry~V4&NDGm`9OtfM{F7`(lE}%AP*A(HIw^FM&q6op#kUTI31_L45iTV zM2fsWGuDq`c~u+?@fRbh(%6_d8;HHw_;DmN6Zc>b$C+G(hL7y$N`wi{$VOws2lHt< zPU9q0)4T8~-OU&NIY(E;-WY;bb{G$a$)V6A*^9#j-b8R#%!OxuMWG2f5*Z|4OU0Mf z;@i$$5oVEK3ld%^+-5u)<;uti!W-}`%|VPaIGI{^SA~{R42>DxzkU1Q;Gi=Prj2DM zSA*MrQC|eSC~uEsR(O$2q>-nnGTJ&t?x^h)HJL@4lZCy%W0SXugW9E{ZnL+65Ed1w zRIXGBDm~Voe{5qNOD_XRR4z&t5Jl>sK-}We!i8rtjHLRW&1}JTEeNtOl|fMWL6GLr zIN^OG2!0xe$&~Y!)CE|z;NP_;_jg|$nqi*Zxx4#3-rLo&k+()+?^QUEdN&=T#$>je zb20W&J)O~^@}Yf=opS*ij>X^cp6Tl>&;fYQ`)+C0fT(|T88=yld!bw52Ha^^i{|2P z9IweA3v|{3=ktX$3Ve@@Q87wJ>ibi|P0<^MStb)LZi#H1?jiq+BA&CX>P?ZzY=D-! zCAhff7+OQiUXB#5ijcdipX8yLY3MK=A+D%!3AI^!GL2ioQlb8AVx@Z=4E{W<3f2=W z{WrjoL;H3HoH%ssl5yzBCG3&nS0&OvGP;k9TVeyaDbYT{Rbs|8Zoj&8<-|^|D^ZJH zRY~>8Adnb94*lZHHGHc9`V!4Fb}MvTqT@0hSLlc+@GPv-5sc%A20gq7J~-ulOvNVM zbEhKvIHDj^V?i7N0q_}C>C#m;RHv*?7;w`)YIdi+ymk7V?_37h-gqe{?t{%!MouxW zN!eY4(H6kua+Vu(tvJg%r)A<j*nIZ*@#g6z78C%AV>!UGU)xp8QI`SQeo@A#jEWL6 zTlm_9%GgFMJj~E>&lKJ`;|jNh+pW#Zp;$VX*P>z*UU<{$XnRFj%cNga*_(WCzwm~( z@w!cun=lEcs=9me9W>z4)1BKjhnSd))>C|4?$<brO%SXC(<=bvw4CeCS5CwI#<_rf z9qR*kva$#WcSUEe*5gURAOA~Ow!AnNC?ie`>I~4KzDp~Z99xrX3qz~oMrW;mR_8Y& z6fGk88MM`>0BuJJ-oNLwHyWupGv9$_lS=S$ayF5ye^ZgIwx^cZ0_RsX0g7r6M0qa= z)E7+plEImIyW_Yho_p7^G`CHo=DCfQrS*U=KECfdmi`*x{G5?^e#<0L@%FiYLtLzQ z!_hlC?Xu!n^;nrd{&NjJ!P0jCrcpZ~I>sI?Xgd=6#$^__qe~yn5Oe_$*xb@=2jy)e z(#iX3zR{S<l_WpFr2iNqiZ8^2nhS{svih8<eAjb<u(eUqJv>j=FIFPA;a+hjtwsJi zmcP}mz87N~sG<^ZB8mp;AFpyMG2<h}fjW20S-d$U>sZ$7hU2;Imbw9)^M&Ud`MNsA z_~8wWy+u)-JrAnNyygt+gd_QSP1)wIgJEAc+iTV_8-3+6<9`xg)E-P<t%n~%LSF>% U+!k)W>?}K<lvc_s*H--h0H%^zDF6Tf diff --git a/resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc deleted file mode 100644 index 629b654b9c5709ab5e164296d91a430a126fcd4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2924 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 F_AgA^o~Hl+ diff --git a/resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc deleted file mode 100644 index 2b9be21492c00f302d9b204dced56a8dc8852b9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3738 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 DmOdQc diff --git a/resources/lib/libraries/mutagen/_compat.py b/resources/lib/libraries/mutagen/_compat.py deleted file mode 100644 index 77c465f1..00000000 --- a/resources/lib/libraries/mutagen/_compat.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2013 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -import sys - - -PY2 = sys.version_info[0] == 2 -PY3 = not PY2 - -if PY2: - from StringIO import StringIO - BytesIO = StringIO - from cStringIO import StringIO as cBytesIO - from itertools import izip - - long_ = long - integer_types = (int, long) - string_types = (str, unicode) - text_type = unicode - - xrange = xrange - cmp = cmp - chr_ = chr - - def endswith(text, end): - return text.endswith(end) - - iteritems = lambda d: d.iteritems() - itervalues = lambda d: d.itervalues() - iterkeys = lambda d: d.iterkeys() - - iterbytes = lambda b: iter(b) - - exec("def reraise(tp, value, tb):\n raise tp, value, tb") - - def swap_to_string(cls): - if "__str__" in cls.__dict__: - cls.__unicode__ = cls.__str__ - - if "__bytes__" in cls.__dict__: - cls.__str__ = cls.__bytes__ - - return cls - -elif PY3: - from io import StringIO - StringIO = StringIO - from io import BytesIO - cBytesIO = BytesIO - - long_ = int - integer_types = (int,) - string_types = (str,) - text_type = str - - izip = zip - xrange = range - cmp = lambda a, b: (a > b) - (a < b) - chr_ = lambda x: bytes([x]) - - def endswith(text, end): - # usefull for paths which can be both, str and bytes - if isinstance(text, str): - if not isinstance(end, str): - end = end.decode("ascii") - else: - if not isinstance(end, bytes): - end = end.encode("ascii") - return text.endswith(end) - - iteritems = lambda d: iter(d.items()) - itervalues = lambda d: iter(d.values()) - iterkeys = lambda d: iter(d.keys()) - - iterbytes = lambda b: (bytes([v]) for v in b) - - def reraise(tp, value, tb): - raise tp(value).with_traceback(tb) - - def swap_to_string(cls): - return cls diff --git a/resources/lib/libraries/mutagen/_constants.py b/resources/lib/libraries/mutagen/_constants.py deleted file mode 100644 index 62c1ce02..00000000 --- a/resources/lib/libraries/mutagen/_constants.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Constants used by Mutagen.""" - -GENRES = [ - u"Blues", - u"Classic Rock", - u"Country", - u"Dance", - u"Disco", - u"Funk", - u"Grunge", - u"Hip-Hop", - u"Jazz", - u"Metal", - u"New Age", - u"Oldies", - u"Other", - u"Pop", - u"R&B", - u"Rap", - u"Reggae", - u"Rock", - u"Techno", - u"Industrial", - u"Alternative", - u"Ska", - u"Death Metal", - u"Pranks", - u"Soundtrack", - u"Euro-Techno", - u"Ambient", - u"Trip-Hop", - u"Vocal", - u"Jazz+Funk", - u"Fusion", - u"Trance", - u"Classical", - u"Instrumental", - u"Acid", - u"House", - u"Game", - u"Sound Clip", - u"Gospel", - u"Noise", - u"Alt. Rock", - u"Bass", - u"Soul", - u"Punk", - u"Space", - u"Meditative", - u"Instrumental Pop", - u"Instrumental Rock", - u"Ethnic", - u"Gothic", - u"Darkwave", - u"Techno-Industrial", - u"Electronic", - u"Pop-Folk", - u"Eurodance", - u"Dream", - u"Southern Rock", - u"Comedy", - u"Cult", - u"Gangsta Rap", - u"Top 40", - u"Christian Rap", - u"Pop/Funk", - u"Jungle", - u"Native American", - u"Cabaret", - u"New Wave", - u"Psychedelic", - u"Rave", - u"Showtunes", - u"Trailer", - u"Lo-Fi", - u"Tribal", - u"Acid Punk", - u"Acid Jazz", - u"Polka", - u"Retro", - u"Musical", - u"Rock & Roll", - u"Hard Rock", - u"Folk", - u"Folk-Rock", - u"National Folk", - u"Swing", - u"Fast-Fusion", - u"Bebop", - u"Latin", - u"Revival", - u"Celtic", - u"Bluegrass", - u"Avantgarde", - u"Gothic Rock", - u"Progressive Rock", - u"Psychedelic Rock", - u"Symphonic Rock", - u"Slow Rock", - u"Big Band", - u"Chorus", - u"Easy Listening", - u"Acoustic", - u"Humour", - u"Speech", - u"Chanson", - u"Opera", - u"Chamber Music", - u"Sonata", - u"Symphony", - u"Booty Bass", - u"Primus", - u"Porn Groove", - u"Satire", - u"Slow Jam", - u"Club", - u"Tango", - u"Samba", - u"Folklore", - u"Ballad", - u"Power Ballad", - u"Rhythmic Soul", - u"Freestyle", - u"Duet", - u"Punk Rock", - u"Drum Solo", - u"A Cappella", - u"Euro-House", - u"Dance Hall", - u"Goa", - u"Drum & Bass", - u"Club-House", - u"Hardcore", - u"Terror", - u"Indie", - u"BritPop", - u"Afro-Punk", - u"Polsk Punk", - u"Beat", - u"Christian Gangsta Rap", - u"Heavy Metal", - u"Black Metal", - u"Crossover", - u"Contemporary Christian", - u"Christian Rock", - u"Merengue", - u"Salsa", - u"Thrash Metal", - u"Anime", - u"JPop", - u"Synthpop", - u"Abstract", - u"Art Rock", - u"Baroque", - u"Bhangra", - u"Big Beat", - u"Breakbeat", - u"Chillout", - u"Downtempo", - u"Dub", - u"EBM", - u"Eclectic", - u"Electro", - u"Electroclash", - u"Emo", - u"Experimental", - u"Garage", - u"Global", - u"IDM", - u"Illbient", - u"Industro-Goth", - u"Jam Band", - u"Krautrock", - u"Leftfield", - u"Lounge", - u"Math Rock", - u"New Romantic", - u"Nu-Breakz", - u"Post-Punk", - u"Post-Rock", - u"Psytrance", - u"Shoegaze", - u"Space Rock", - u"Trop Rock", - u"World Music", - u"Neoclassical", - u"Audiobook", - u"Audio Theatre", - u"Neue Deutsche Welle", - u"Podcast", - u"Indie Rock", - u"G-Funk", - u"Dubstep", - u"Garage Rock", - u"Psybient", -] -"""The ID3v1 genre list.""" diff --git a/resources/lib/libraries/mutagen/_file.py b/resources/lib/libraries/mutagen/_file.py deleted file mode 100644 index 5daa2521..00000000 --- a/resources/lib/libraries/mutagen/_file.py +++ /dev/null @@ -1,253 +0,0 @@ -# Copyright (C) 2005 Michael Urman -# -*- coding: utf-8 -*- -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -import warnings - -from mutagen._util import DictMixin -from mutagen._compat import izip - - -class FileType(DictMixin): - """An abstract object wrapping tags and audio stream information. - - Attributes: - - * info -- stream information (length, bitrate, sample rate) - * tags -- metadata tags, if any - - Each file format has different potential tags and stream - information. - - FileTypes implement an interface very similar to Metadata; the - dict interface, save, load, and delete calls on a FileType call - the appropriate methods on its tag data. - """ - - __module__ = "mutagen" - - info = None - tags = None - filename = None - _mimes = ["application/octet-stream"] - - def __init__(self, filename=None, *args, **kwargs): - if filename is None: - warnings.warn("FileType constructor requires a filename", - DeprecationWarning) - else: - self.load(filename, *args, **kwargs) - - def load(self, filename, *args, **kwargs): - raise NotImplementedError - - def __getitem__(self, key): - """Look up a metadata tag key. - - If the file has no tags at all, a KeyError is raised. - """ - - if self.tags is None: - raise KeyError(key) - else: - return self.tags[key] - - def __setitem__(self, key, value): - """Set a metadata tag. - - If the file has no tags, an appropriate format is added (but - not written until save is called). - """ - - if self.tags is None: - self.add_tags() - self.tags[key] = value - - def __delitem__(self, key): - """Delete a metadata tag key. - - If the file has no tags at all, a KeyError is raised. - """ - - if self.tags is None: - raise KeyError(key) - else: - del(self.tags[key]) - - def keys(self): - """Return a list of keys in the metadata tag. - - If the file has no tags at all, an empty list is returned. - """ - - if self.tags is None: - return [] - else: - return self.tags.keys() - - def delete(self, filename=None): - """Remove tags from a file. - - In cases where the tagging format is independent of the file type - (for example `mutagen.ID3`) all traces of the tagging format will - be removed. - In cases where the tag is part of the file type, all tags and - padding will be removed. - - The tags attribute will be cleared as well if there is one. - - Does nothing if the file has no tags. - - :raises mutagen.MutagenError: if deleting wasn't possible - """ - - if self.tags is not None: - if filename is None: - filename = self.filename - else: - warnings.warn( - "delete(filename=...) is deprecated, reload the file", - DeprecationWarning) - return self.tags.delete(filename) - - def save(self, filename=None, **kwargs): - """Save metadata tags. - - :raises mutagen.MutagenError: if saving wasn't possible - """ - - if filename is None: - filename = self.filename - else: - warnings.warn( - "save(filename=...) is deprecated, reload the file", - DeprecationWarning) - - if self.tags is not None: - return self.tags.save(filename, **kwargs) - - def pprint(self): - """Print stream information and comment key=value pairs.""" - - stream = "%s (%s)" % (self.info.pprint(), self.mime[0]) - try: - tags = self.tags.pprint() - except AttributeError: - return stream - else: - return stream + ((tags and "\n" + tags) or "") - - def add_tags(self): - """Adds new tags to the file. - - :raises mutagen.MutagenError: if tags already exist or adding is not - possible. - """ - - raise NotImplementedError - - @property - def mime(self): - """A list of mime types""" - - mimes = [] - for Kind in type(self).__mro__: - for mime in getattr(Kind, '_mimes', []): - if mime not in mimes: - mimes.append(mime) - return mimes - - @staticmethod - def score(filename, fileobj, header): - raise NotImplementedError - - -class StreamInfo(object): - """Abstract stream information object. - - Provides attributes for length, bitrate, sample rate etc. - - See the implementations for details. - """ - - __module__ = "mutagen" - - def pprint(self): - """Print stream information""" - - raise NotImplementedError - - -def File(filename, options=None, easy=False): - """Guess the type of the file and try to open it. - - The file type is decided by several things, such as the first 128 - bytes (which usually contains a file type identifier), the - filename extension, and the presence of existing tags. - - If no appropriate type could be found, None is returned. - - :param options: Sequence of :class:`FileType` implementations, defaults to - all included ones. - - :param easy: If the easy wrappers should be returnd if available. - For example :class:`EasyMP3 <mp3.EasyMP3>` instead - of :class:`MP3 <mp3.MP3>`. - """ - - if options is None: - from mutagen.asf import ASF - from mutagen.apev2 import APEv2File - from mutagen.flac import FLAC - if easy: - from mutagen.easyid3 import EasyID3FileType as ID3FileType - else: - from mutagen.id3 import ID3FileType - if easy: - from mutagen.mp3 import EasyMP3 as MP3 - else: - from mutagen.mp3 import MP3 - from mutagen.oggflac import OggFLAC - from mutagen.oggspeex import OggSpeex - from mutagen.oggtheora import OggTheora - from mutagen.oggvorbis import OggVorbis - from mutagen.oggopus import OggOpus - if easy: - from mutagen.trueaudio import EasyTrueAudio as TrueAudio - else: - from mutagen.trueaudio import TrueAudio - from mutagen.wavpack import WavPack - if easy: - from mutagen.easymp4 import EasyMP4 as MP4 - else: - from mutagen.mp4 import MP4 - from mutagen.musepack import Musepack - from mutagen.monkeysaudio import MonkeysAudio - from mutagen.optimfrog import OptimFROG - from mutagen.aiff import AIFF - from mutagen.aac import AAC - options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, - FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, - Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC] - - if not options: - return None - - with open(filename, "rb") as fileobj: - header = fileobj.read(128) - # Sort by name after score. Otherwise import order affects - # Kind sort order, which affects treatment of things with - # equals scores. - results = [(Kind.score(filename, fileobj, header), Kind.__name__) - for Kind in options] - - results = list(izip(results, options)) - results.sort() - (score, name), Kind = results[-1] - if score > 0: - return Kind(filename) - else: - return None diff --git a/resources/lib/libraries/mutagen/_mp3util.py b/resources/lib/libraries/mutagen/_mp3util.py deleted file mode 100644 index 409cadcb..00000000 --- a/resources/lib/libraries/mutagen/_mp3util.py +++ /dev/null @@ -1,420 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -""" -http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header -http://wiki.hydrogenaud.io/index.php?title=MP3 -""" - -from functools import partial - -from ._util import cdata, BitReader -from ._compat import xrange, iterbytes, cBytesIO - - -class LAMEError(Exception): - pass - - -class LAMEHeader(object): - """http://gabriel.mp3-tech.org/mp3infotag.html""" - - vbr_method = 0 - """0: unknown, 1: CBR, 2: ABR, 3/4/5: VBR, others: see the docs""" - - lowpass_filter = 0 - """lowpass filter value in Hz. 0 means unknown""" - - quality = -1 - """Encoding quality: 0..9""" - - vbr_quality = -1 - """VBR quality: 0..9""" - - track_peak = None - """Peak signal amplitude as float. None if unknown.""" - - track_gain_origin = 0 - """see the docs""" - - track_gain_adjustment = None - """Track gain adjustment as float (for 89db replay gain) or None""" - - album_gain_origin = 0 - """see the docs""" - - album_gain_adjustment = None - """Album gain adjustment as float (for 89db replay gain) or None""" - - encoding_flags = 0 - """see docs""" - - ath_type = -1 - """see docs""" - - bitrate = -1 - """Bitrate in kbps. For VBR the minimum bitrate, for anything else - (CBR, ABR, ..) the target bitrate. - """ - - encoder_delay_start = 0 - """Encoder delay in samples""" - - encoder_padding_end = 0 - """Padding in samples added at the end""" - - source_sample_frequency_enum = -1 - """see docs""" - - unwise_setting_used = False - """see docs""" - - stereo_mode = 0 - """see docs""" - - noise_shaping = 0 - """see docs""" - - mp3_gain = 0 - """Applied MP3 gain -127..127. Factor is 2 ** (mp3_gain / 4)""" - - surround_info = 0 - """see docs""" - - preset_used = 0 - """lame preset""" - - music_length = 0 - """Length in bytes excluding any ID3 tags""" - - music_crc = -1 - """CRC16 of the data specified by music_length""" - - header_crc = -1 - """CRC16 of this header and everything before (not checked)""" - - def __init__(self, xing, fileobj): - """Raises LAMEError if parsing fails""" - - payload = fileobj.read(27) - if len(payload) != 27: - raise LAMEError("Not enough data") - - # extended lame header - r = BitReader(cBytesIO(payload)) - revision = r.bits(4) - if revision != 0: - raise LAMEError("unsupported header revision %d" % revision) - - self.vbr_method = r.bits(4) - self.lowpass_filter = r.bits(8) * 100 - - # these have a different meaning for lame; expose them again here - self.quality = (100 - xing.vbr_scale) % 10 - self.vbr_quality = (100 - xing.vbr_scale) // 10 - - track_peak_data = r.bytes(4) - if track_peak_data == b"\x00\x00\x00\x00": - self.track_peak = None - else: - # see PutLameVBR() in LAME's VbrTag.c - self.track_peak = ( - cdata.uint32_be(track_peak_data) - 0.5) / 2 ** 23 - track_gain_type = r.bits(3) - self.track_gain_origin = r.bits(3) - sign = r.bits(1) - gain_adj = r.bits(9) / 10.0 - if sign: - gain_adj *= -1 - if track_gain_type == 1: - self.track_gain_adjustment = gain_adj - else: - self.track_gain_adjustment = None - assert r.is_aligned() - - album_gain_type = r.bits(3) - self.album_gain_origin = r.bits(3) - sign = r.bits(1) - album_gain_adj = r.bits(9) / 10.0 - if album_gain_type == 2: - self.album_gain_adjustment = album_gain_adj - else: - self.album_gain_adjustment = None - - self.encoding_flags = r.bits(4) - self.ath_type = r.bits(4) - - self.bitrate = r.bits(8) - - self.encoder_delay_start = r.bits(12) - self.encoder_padding_end = r.bits(12) - - self.source_sample_frequency_enum = r.bits(2) - self.unwise_setting_used = r.bits(1) - self.stereo_mode = r.bits(3) - self.noise_shaping = r.bits(2) - - sign = r.bits(1) - mp3_gain = r.bits(7) - if sign: - mp3_gain *= -1 - self.mp3_gain = mp3_gain - - r.skip(2) - self.surround_info = r.bits(3) - self.preset_used = r.bits(11) - self.music_length = r.bits(32) - self.music_crc = r.bits(16) - - self.header_crc = r.bits(16) - assert r.is_aligned() - - @classmethod - def parse_version(cls, fileobj): - """Returns a version string and True if a LAMEHeader follows. - The passed file object will be positioned right before the - lame header if True. - - Raises LAMEError if there is no lame version info. - """ - - # http://wiki.hydrogenaud.io/index.php?title=LAME_version_string - - data = fileobj.read(20) - if len(data) != 20: - raise LAMEError("Not a lame header") - if not data.startswith((b"LAME", b"L3.99")): - raise LAMEError("Not a lame header") - - data = data.lstrip(b"EMAL") - major, data = data[0:1], data[1:].lstrip(b".") - minor = b"" - for c in iterbytes(data): - if not c.isdigit(): - break - minor += c - data = data[len(minor):] - - try: - major = int(major.decode("ascii")) - minor = int(minor.decode("ascii")) - except ValueError: - raise LAMEError - - # the extended header was added sometimes in the 3.90 cycle - # e.g. "LAME3.90 (alpha)" should still stop here. - # (I have seen such a file) - if (major, minor) < (3, 90) or ( - (major, minor) == (3, 90) and data[-11:-10] == b"("): - flag = data.strip(b"\x00").rstrip().decode("ascii") - return u"%d.%d%s" % (major, minor, flag), False - - if len(data) <= 11: - raise LAMEError("Invalid version: too long") - - flag = data[:-11].rstrip(b"\x00") - - flag_string = u"" - patch = u"" - if flag == b"a": - flag_string = u" (alpha)" - elif flag == b"b": - flag_string = u" (beta)" - elif flag == b"r": - patch = u".1+" - elif flag == b" ": - if (major, minor) > (3, 96): - patch = u".0" - else: - patch = u".0+" - elif flag == b"" or flag == b".": - patch = u".0+" - else: - flag_string = u" (?)" - - # extended header, seek back to 9 bytes for the caller - fileobj.seek(-11, 1) - - return u"%d.%d%s%s" % (major, minor, patch, flag_string), True - - -class XingHeaderError(Exception): - pass - - -class XingHeaderFlags(object): - FRAMES = 0x1 - BYTES = 0x2 - TOC = 0x4 - VBR_SCALE = 0x8 - - -class XingHeader(object): - - frames = -1 - """Number of frames, -1 if unknown""" - - bytes = -1 - """Number of bytes, -1 if unknown""" - - toc = [] - """List of 100 file offsets in percent encoded as 0-255. E.g. entry - 50 contains the file offset in percent at 50% play time. - Empty if unknown. - """ - - vbr_scale = -1 - """VBR quality indicator 0-100. -1 if unknown""" - - lame_header = None - """A LAMEHeader instance or None""" - - lame_version = u"" - """The version of the LAME encoder e.g. '3.99.0'. Empty if unknown""" - - is_info = False - """If the header started with 'Info' and not 'Xing'""" - - def __init__(self, fileobj): - """Parses the Xing header or raises XingHeaderError. - - The file position after this returns is undefined. - """ - - data = fileobj.read(8) - if len(data) != 8 or data[:4] not in (b"Xing", b"Info"): - raise XingHeaderError("Not a Xing header") - - self.is_info = (data[:4] == b"Info") - - flags = cdata.uint32_be_from(data, 4)[0] - - if flags & XingHeaderFlags.FRAMES: - data = fileobj.read(4) - if len(data) != 4: - raise XingHeaderError("Xing header truncated") - self.frames = cdata.uint32_be(data) - - if flags & XingHeaderFlags.BYTES: - data = fileobj.read(4) - if len(data) != 4: - raise XingHeaderError("Xing header truncated") - self.bytes = cdata.uint32_be(data) - - if flags & XingHeaderFlags.TOC: - data = fileobj.read(100) - if len(data) != 100: - raise XingHeaderError("Xing header truncated") - self.toc = list(bytearray(data)) - - if flags & XingHeaderFlags.VBR_SCALE: - data = fileobj.read(4) - if len(data) != 4: - raise XingHeaderError("Xing header truncated") - self.vbr_scale = cdata.uint32_be(data) - - try: - self.lame_version, has_header = LAMEHeader.parse_version(fileobj) - if has_header: - self.lame_header = LAMEHeader(self, fileobj) - except LAMEError: - pass - - @classmethod - def get_offset(cls, info): - """Calculate the offset to the Xing header from the start of the - MPEG header including sync based on the MPEG header's content. - """ - - assert info.layer == 3 - - if info.version == 1: - if info.mode != 3: - return 36 - else: - return 21 - else: - if info.mode != 3: - return 21 - else: - return 13 - - -class VBRIHeaderError(Exception): - pass - - -class VBRIHeader(object): - - version = 0 - """VBRI header version""" - - quality = 0 - """Quality indicator""" - - bytes = 0 - """Number of bytes""" - - frames = 0 - """Number of frames""" - - toc_scale_factor = 0 - """Scale factor of TOC entries""" - - toc_frames = 0 - """Number of frames per table entry""" - - toc = [] - """TOC""" - - def __init__(self, fileobj): - """Reads the VBRI header or raises VBRIHeaderError. - - The file position is undefined after this returns - """ - - data = fileobj.read(26) - if len(data) != 26 or not data.startswith(b"VBRI"): - raise VBRIHeaderError("Not a VBRI header") - - offset = 4 - self.version, offset = cdata.uint16_be_from(data, offset) - if self.version != 1: - raise VBRIHeaderError( - "Unsupported header version: %r" % self.version) - - offset += 2 # float16.. can't do - self.quality, offset = cdata.uint16_be_from(data, offset) - self.bytes, offset = cdata.uint32_be_from(data, offset) - self.frames, offset = cdata.uint32_be_from(data, offset) - - toc_num_entries, offset = cdata.uint16_be_from(data, offset) - self.toc_scale_factor, offset = cdata.uint16_be_from(data, offset) - toc_entry_size, offset = cdata.uint16_be_from(data, offset) - self.toc_frames, offset = cdata.uint16_be_from(data, offset) - toc_size = toc_entry_size * toc_num_entries - toc_data = fileobj.read(toc_size) - if len(toc_data) != toc_size: - raise VBRIHeaderError("VBRI header truncated") - - self.toc = [] - if toc_entry_size == 2: - unpack = partial(cdata.uint16_be_from, toc_data) - elif toc_entry_size == 4: - unpack = partial(cdata.uint32_be_from, toc_data) - else: - raise VBRIHeaderError("Invalid TOC entry size") - - self.toc = [unpack(i)[0] for i in xrange(0, toc_size, toc_entry_size)] - - @classmethod - def get_offset(cls, info): - """Offset in bytes from the start of the MPEG header including sync""" - - assert info.layer == 3 - - return 36 diff --git a/resources/lib/libraries/mutagen/_tags.py b/resources/lib/libraries/mutagen/_tags.py deleted file mode 100644 index ce250adf..00000000 --- a/resources/lib/libraries/mutagen/_tags.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2005 Michael Urman -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - - -class PaddingInfo(object): - """Abstract padding information object. - - This will be passed to the callback function that can be used - for saving tags. - - :: - - def my_callback(info: PaddingInfo): - return info.get_default_padding() - - The callback should return the amount of padding to use (>= 0) based on - the content size and the padding of the file after saving. The actual used - amount of padding might vary depending on the file format (due to - alignment etc.) - - The default implementation can be accessed using the - :meth:`get_default_padding` method in the callback. - """ - - padding = 0 - """The amount of padding left after saving in bytes (can be negative if - more data needs to be added as padding is available) - """ - - size = 0 - """The amount of data following the padding""" - - def __init__(self, padding, size): - self.padding = padding - self.size = size - - def get_default_padding(self): - """The default implementation which tries to select a reasonable - amount of padding and which might change in future versions. - - :return: Amount of padding after saving - :rtype: int - """ - - high = 1024 * 10 + self.size // 100 # 10 KiB + 1% of trailing data - low = 1024 + self.size // 1000 # 1 KiB + 0.1% of trailing data - - if self.padding >= 0: - # enough padding left - if self.padding > high: - # padding too large, reduce - return low - # just use existing padding as is - return self.padding - else: - # not enough padding, add some - return low - - def _get_padding(self, user_func): - if user_func is None: - return self.get_default_padding() - else: - return user_func(self) - - def __repr__(self): - return "<%s size=%d padding=%d>" % ( - type(self).__name__, self.size, self.padding) - - -class Metadata(object): - """An abstract dict-like object. - - Metadata is the base class for many of the tag objects in Mutagen. - """ - - __module__ = "mutagen" - - def __init__(self, *args, **kwargs): - if args or kwargs: - self.load(*args, **kwargs) - - def load(self, *args, **kwargs): - raise NotImplementedError - - def save(self, filename=None): - """Save changes to a file.""" - - raise NotImplementedError - - def delete(self, filename=None): - """Remove tags from a file. - - In most cases this means any traces of the tag will be removed - from the file. - """ - - raise NotImplementedError diff --git a/resources/lib/libraries/mutagen/_toolsutil.py b/resources/lib/libraries/mutagen/_toolsutil.py deleted file mode 100644 index e9074b71..00000000 --- a/resources/lib/libraries/mutagen/_toolsutil.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2015 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -import os -import sys -import signal -import locale -import contextlib -import optparse -import ctypes - -from ._compat import text_type, PY2, PY3, iterbytes - - -def split_escape(string, sep, maxsplit=None, escape_char="\\"): - """Like unicode/str/bytes.split but allows for the separator to be escaped - - If passed unicode/str/bytes will only return list of unicode/str/bytes. - """ - - assert len(sep) == 1 - assert len(escape_char) == 1 - - if isinstance(string, bytes): - if isinstance(escape_char, text_type): - escape_char = escape_char.encode("ascii") - iter_ = iterbytes - else: - iter_ = iter - - if maxsplit is None: - maxsplit = len(string) - - empty = string[:0] - result = [] - current = empty - escaped = False - for char in iter_(string): - if escaped: - if char != escape_char and char != sep: - current += escape_char - current += char - escaped = False - else: - if char == escape_char: - escaped = True - elif char == sep and len(result) < maxsplit: - result.append(current) - current = empty - else: - current += char - result.append(current) - return result - - -class SignalHandler(object): - - def __init__(self): - self._interrupted = False - self._nosig = False - self._init = False - - def init(self): - signal.signal(signal.SIGINT, self._handler) - signal.signal(signal.SIGTERM, self._handler) - if os.name != "nt": - signal.signal(signal.SIGHUP, self._handler) - - def _handler(self, signum, frame): - self._interrupted = True - if not self._nosig: - raise SystemExit("Aborted...") - - @contextlib.contextmanager - def block(self): - """While this context manager is active any signals for aborting - the process will be queued and exit the program once the context - is left. - """ - - self._nosig = True - yield - self._nosig = False - if self._interrupted: - raise SystemExit("Aborted...") - - -def get_win32_unicode_argv(): - """Returns a unicode argv under Windows and standard sys.argv otherwise""" - - if os.name != "nt" or not PY2: - return sys.argv - - import ctypes - from ctypes import cdll, windll, wintypes - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = wintypes.LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [ - wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)] - CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) - - LocalFree = windll.kernel32.LocalFree - LocalFree.argtypes = [wintypes.HLOCAL] - LocalFree.restype = wintypes.HLOCAL - - argc = ctypes.c_int() - argv = CommandLineToArgvW(GetCommandLineW(), ctypes.byref(argc)) - if not argv: - return - - res = argv[max(0, argc.value - len(sys.argv)):argc.value] - - LocalFree(argv) - - return res - - -def fsencoding(): - """The encoding used for paths, argv, environ, stdout and stdin""" - - if os.name == "nt": - return "" - - return locale.getpreferredencoding() or "utf-8" - - -def fsnative(text=u""): - """Returns the passed text converted to the preferred path type - for each platform. - """ - - assert isinstance(text, text_type) - - if os.name == "nt" or PY3: - return text - else: - return text.encode(fsencoding(), "replace") - return text - - -def is_fsnative(arg): - """If the passed value is of the preferred path type for each platform. - Note that on Python3+linux, paths can be bytes or str but this returns - False for bytes there. - """ - - if PY3 or os.name == "nt": - return isinstance(arg, text_type) - else: - return isinstance(arg, bytes) - - -def print_(*objects, **kwargs): - """A print which supports bytes and str+surrogates under python3. - - Needed so we can print anything passed to us through argv and environ. - Under Windows only text_type is allowed. - - Arguments: - objects: one or more bytes/text - linesep (bool): whether a line separator should be appended - sep (bool): whether objects should be printed separated by spaces - """ - - linesep = kwargs.pop("linesep", True) - sep = kwargs.pop("sep", True) - file_ = kwargs.pop("file", None) - if file_ is None: - file_ = sys.stdout - - old_cp = None - if os.name == "nt": - # Try to force the output to cp65001 aka utf-8. - # If that fails use the current one (most likely cp850, so - # most of unicode will be replaced with '?') - encoding = "utf-8" - old_cp = ctypes.windll.kernel32.GetConsoleOutputCP() - if ctypes.windll.kernel32.SetConsoleOutputCP(65001) == 0: - encoding = getattr(sys.stdout, "encoding", None) or "utf-8" - old_cp = None - else: - encoding = fsencoding() - - try: - if linesep: - objects = list(objects) + [os.linesep] - - parts = [] - for text in objects: - if isinstance(text, text_type): - if PY3: - try: - text = text.encode(encoding, 'surrogateescape') - except UnicodeEncodeError: - text = text.encode(encoding, 'replace') - else: - text = text.encode(encoding, 'replace') - parts.append(text) - - data = (b" " if sep else b"").join(parts) - try: - fileno = file_.fileno() - except (AttributeError, OSError, ValueError): - # for tests when stdout is replaced - try: - file_.write(data) - except TypeError: - file_.write(data.decode(encoding, "replace")) - else: - file_.flush() - os.write(fileno, data) - finally: - # reset the code page to what we had before - if old_cp is not None: - ctypes.windll.kernel32.SetConsoleOutputCP(old_cp) - - -class OptionParser(optparse.OptionParser): - """OptionParser subclass which supports printing Unicode under Windows""" - - def print_help(self, file=None): - print_(self.format_help(), file=file) diff --git a/resources/lib/libraries/mutagen/_util.py b/resources/lib/libraries/mutagen/_util.py deleted file mode 100644 index f05ff454..00000000 --- a/resources/lib/libraries/mutagen/_util.py +++ /dev/null @@ -1,550 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Utility classes for Mutagen. - -You should not rely on the interfaces here being stable. They are -intended for internal use in Mutagen only. -""" - -import struct -import codecs - -from fnmatch import fnmatchcase - -from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \ - izip - - -class MutagenError(Exception): - """Base class for all custom exceptions in mutagen - - .. versionadded:: 1.25 - """ - - __module__ = "mutagen" - - -def total_ordering(cls): - assert "__eq__" in cls.__dict__ - assert "__lt__" in cls.__dict__ - - cls.__le__ = lambda self, other: self == other or self < other - cls.__gt__ = lambda self, other: not (self == other or self < other) - cls.__ge__ = lambda self, other: not self < other - cls.__ne__ = lambda self, other: not self.__eq__(other) - - return cls - - -def hashable(cls): - """Makes sure the class is hashable. - - Needs a working __eq__ and __hash__ and will add a __ne__. - """ - - # py2 - assert "__hash__" in cls.__dict__ - # py3 - assert cls.__dict__["__hash__"] is not None - assert "__eq__" in cls.__dict__ - - cls.__ne__ = lambda self, other: not self.__eq__(other) - - return cls - - -def enum(cls): - assert cls.__bases__ == (object,) - - d = dict(cls.__dict__) - new_type = type(cls.__name__, (int,), d) - new_type.__module__ = cls.__module__ - - map_ = {} - for key, value in iteritems(d): - if key.upper() == key and isinstance(value, integer_types): - value_instance = new_type(value) - setattr(new_type, key, value_instance) - map_[value] = key - - def str_(self): - if self in map_: - return "%s.%s" % (type(self).__name__, map_[self]) - return "%d" % int(self) - - def repr_(self): - if self in map_: - return "<%s.%s: %d>" % (type(self).__name__, map_[self], int(self)) - return "%d" % int(self) - - setattr(new_type, "__repr__", repr_) - setattr(new_type, "__str__", str_) - - return new_type - - -@total_ordering -class DictMixin(object): - """Implement the dict API using keys() and __*item__ methods. - - Similar to UserDict.DictMixin, this takes a class that defines - __getitem__, __setitem__, __delitem__, and keys(), and turns it - into a full dict-like object. - - UserDict.DictMixin is not suitable for this purpose because it's - an old-style class. - - This class is not optimized for very large dictionaries; many - functions have linear memory requirements. I recommend you - override some of these functions if speed is required. - """ - - def __iter__(self): - return iter(self.keys()) - - def __has_key(self, key): - try: - self[key] - except KeyError: - return False - else: - return True - - if PY2: - has_key = __has_key - - __contains__ = __has_key - - if PY2: - iterkeys = lambda self: iter(self.keys()) - - def values(self): - return [self[k] for k in self.keys()] - - if PY2: - itervalues = lambda self: iter(self.values()) - - def items(self): - return list(izip(self.keys(), self.values())) - - if PY2: - iteritems = lambda s: iter(s.items()) - - def clear(self): - for key in list(self.keys()): - self.__delitem__(key) - - def pop(self, key, *args): - if len(args) > 1: - raise TypeError("pop takes at most two arguments") - try: - value = self[key] - except KeyError: - if args: - return args[0] - else: - raise - del(self[key]) - return value - - def popitem(self): - for key in self.keys(): - break - else: - raise KeyError("dictionary is empty") - return key, self.pop(key) - - def update(self, other=None, **kwargs): - if other is None: - self.update(kwargs) - other = {} - - try: - for key, value in other.items(): - self.__setitem__(key, value) - except AttributeError: - for key, value in other: - self[key] = value - - def setdefault(self, key, default=None): - try: - return self[key] - except KeyError: - self[key] = default - return default - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - def __repr__(self): - return repr(dict(self.items())) - - def __eq__(self, other): - return dict(self.items()) == other - - def __lt__(self, other): - return dict(self.items()) < other - - __hash__ = object.__hash__ - - def __len__(self): - return len(self.keys()) - - -class DictProxy(DictMixin): - def __init__(self, *args, **kwargs): - self.__dict = {} - super(DictProxy, self).__init__(*args, **kwargs) - - def __getitem__(self, key): - return self.__dict[key] - - def __setitem__(self, key, value): - self.__dict[key] = value - - def __delitem__(self, key): - del(self.__dict[key]) - - def keys(self): - return self.__dict.keys() - - -def _fill_cdata(cls): - """Add struct pack/unpack functions""" - - funcs = {} - for key, name in [("b", "char"), ("h", "short"), - ("i", "int"), ("q", "longlong")]: - for echar, esuffix in [("<", "le"), (">", "be")]: - esuffix = "_" + esuffix - for unsigned in [True, False]: - s = struct.Struct(echar + (key.upper() if unsigned else key)) - get_wrapper = lambda f: lambda *a, **k: f(*a, **k)[0] - unpack = get_wrapper(s.unpack) - unpack_from = get_wrapper(s.unpack_from) - - def get_unpack_from(s): - def unpack_from(data, offset=0): - return s.unpack_from(data, offset)[0], offset + s.size - return unpack_from - - unpack_from = get_unpack_from(s) - pack = s.pack - - prefix = "u" if unsigned else "" - if s.size == 1: - esuffix = "" - bits = str(s.size * 8) - funcs["%s%s%s" % (prefix, name, esuffix)] = unpack - funcs["%sint%s%s" % (prefix, bits, esuffix)] = unpack - funcs["%s%s%s_from" % (prefix, name, esuffix)] = unpack_from - funcs["%sint%s%s_from" % (prefix, bits, esuffix)] = unpack_from - funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack - funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack - - for key, func in iteritems(funcs): - setattr(cls, key, staticmethod(func)) - - -class cdata(object): - """C character buffer to Python numeric type conversions. - - For each size/sign/endianness: - uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0) - """ - - from struct import error - error = error - - bitswap = b''.join( - chr_(sum(((val >> i) & 1) << (7 - i) for i in xrange(8))) - for val in xrange(256)) - - test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) - - -_fill_cdata(cdata) - - -def get_size(fileobj): - """Returns the size of the file object. The position when passed in will - be preserved if no error occurs. - - In case of an error raises IOError. - """ - - old_pos = fileobj.tell() - try: - fileobj.seek(0, 2) - return fileobj.tell() - finally: - fileobj.seek(old_pos, 0) - - -def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): - """Insert size bytes of empty space starting at offset. - - fobj must be an open file object, open rb+ or - equivalent. Mutagen tries to use mmap to resize the file, but - falls back to a significantly slower method if mmap fails. - """ - - assert 0 < size - assert 0 <= offset - - fobj.seek(0, 2) - filesize = fobj.tell() - movesize = filesize - offset - fobj.write(b'\x00' * size) - fobj.flush() - - try: - import mmap - file_map = mmap.mmap(fobj.fileno(), filesize + size) - try: - file_map.move(offset + size, offset, movesize) - finally: - file_map.close() - except (ValueError, EnvironmentError, ImportError, AttributeError): - # handle broken mmap scenarios, BytesIO() - fobj.truncate(filesize) - - fobj.seek(0, 2) - padsize = size - # Don't generate an enormous string if we need to pad - # the file out several megs. - while padsize: - addsize = min(BUFFER_SIZE, padsize) - fobj.write(b"\x00" * addsize) - padsize -= addsize - - fobj.seek(filesize, 0) - while movesize: - # At the start of this loop, fobj is pointing at the end - # of the data we need to move, which is of movesize length. - thismove = min(BUFFER_SIZE, movesize) - # Seek back however much we're going to read this frame. - fobj.seek(-thismove, 1) - nextpos = fobj.tell() - # Read it, so we're back at the end. - data = fobj.read(thismove) - # Seek back to where we need to write it. - fobj.seek(-thismove + size, 1) - # Write it. - fobj.write(data) - # And seek back to the end of the unmoved data. - fobj.seek(nextpos) - movesize -= thismove - - fobj.flush() - - -def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): - """Delete size bytes of empty space starting at offset. - - fobj must be an open file object, open rb+ or - equivalent. Mutagen tries to use mmap to resize the file, but - falls back to a significantly slower method if mmap fails. - """ - - assert 0 < size - assert 0 <= offset - - fobj.seek(0, 2) - filesize = fobj.tell() - movesize = filesize - offset - size - assert 0 <= movesize - - if movesize > 0: - fobj.flush() - try: - import mmap - file_map = mmap.mmap(fobj.fileno(), filesize) - try: - file_map.move(offset, offset + size, movesize) - finally: - file_map.close() - except (ValueError, EnvironmentError, ImportError, AttributeError): - # handle broken mmap scenarios, BytesIO() - fobj.seek(offset + size) - buf = fobj.read(BUFFER_SIZE) - while buf: - fobj.seek(offset) - fobj.write(buf) - offset += len(buf) - fobj.seek(offset + size) - buf = fobj.read(BUFFER_SIZE) - fobj.truncate(filesize - size) - fobj.flush() - - -def resize_bytes(fobj, old_size, new_size, offset): - """Resize an area in a file adding and deleting at the end of it. - Does nothing if no resizing is needed. - """ - - if new_size < old_size: - delete_size = old_size - new_size - delete_at = offset + new_size - delete_bytes(fobj, delete_size, delete_at) - elif new_size > old_size: - insert_size = new_size - old_size - insert_at = offset + old_size - insert_bytes(fobj, insert_size, insert_at) - - -def dict_match(d, key, default=None): - """Like __getitem__ but works as if the keys() are all filename patterns. - Returns the value of any dict key that matches the passed key. - """ - - if key in d and "[" not in key: - return d[key] - else: - for pattern, value in iteritems(d): - if fnmatchcase(key, pattern): - return value - return default - - -def decode_terminated(data, encoding, strict=True): - """Returns the decoded data until the first NULL terminator - and all data after it. - - In case the data can't be decoded raises UnicodeError. - In case the encoding is not found raises LookupError. - In case the data isn't null terminated (even if it is encoded correctly) - raises ValueError except if strict is False, then the decoded string - will be returned anyway. - """ - - codec_info = codecs.lookup(encoding) - - # normalize encoding name so we can compare by name - encoding = codec_info.name - - # fast path - if encoding in ("utf-8", "iso8859-1"): - index = data.find(b"\x00") - if index == -1: - # make sure we raise UnicodeError first, like in the slow path - res = data.decode(encoding), b"" - if strict: - raise ValueError("not null terminated") - else: - return res - return data[:index].decode(encoding), data[index + 1:] - - # slow path - decoder = codec_info.incrementaldecoder() - r = [] - for i, b in enumerate(iterbytes(data)): - c = decoder.decode(b) - if c == u"\x00": - return u"".join(r), data[i + 1:] - r.append(c) - else: - # make sure the decoder is finished - r.append(decoder.decode(b"", True)) - if strict: - raise ValueError("not null terminated") - return u"".join(r), b"" - - -class BitReaderError(Exception): - pass - - -class BitReader(object): - - def __init__(self, fileobj): - self._fileobj = fileobj - self._buffer = 0 - self._bits = 0 - self._pos = fileobj.tell() - - def bits(self, count): - """Reads `count` bits and returns an uint, MSB read first. - - May raise BitReaderError if not enough data could be read or - IOError by the underlying file object. - """ - - if count < 0: - raise ValueError - - if count > self._bits: - n_bytes = (count - self._bits + 7) // 8 - data = self._fileobj.read(n_bytes) - if len(data) != n_bytes: - raise BitReaderError("not enough data") - for b in bytearray(data): - self._buffer = (self._buffer << 8) | b - self._bits += n_bytes * 8 - - self._bits -= count - value = self._buffer >> self._bits - self._buffer &= (1 << self._bits) - 1 - assert self._bits < 8 - return value - - def bytes(self, count): - """Returns a bytearray of length `count`. Works unaligned.""" - - if count < 0: - raise ValueError - - # fast path - if self._bits == 0: - data = self._fileobj.read(count) - if len(data) != count: - raise BitReaderError("not enough data") - return data - - return bytes(bytearray(self.bits(8) for _ in xrange(count))) - - def skip(self, count): - """Skip `count` bits. - - Might raise BitReaderError if there wasn't enough data to skip, - but might also fail on the next bits() instead. - """ - - if count < 0: - raise ValueError - - if count <= self._bits: - self.bits(count) - else: - count -= self.align() - n_bytes = count // 8 - self._fileobj.seek(n_bytes, 1) - count -= n_bytes * 8 - self.bits(count) - - def get_position(self): - """Returns the amount of bits read or skipped so far""" - - return (self._fileobj.tell() - self._pos) * 8 - self._bits - - def align(self): - """Align to the next byte, returns the amount of bits skipped""" - - bits = self._bits - self._buffer = 0 - self._bits = 0 - return bits - - def is_aligned(self): - """If we are currently aligned to bytes and nothing is buffered""" - - return self._bits == 0 diff --git a/resources/lib/libraries/mutagen/_vorbis.py b/resources/lib/libraries/mutagen/_vorbis.py deleted file mode 100644 index da202400..00000000 --- a/resources/lib/libraries/mutagen/_vorbis.py +++ /dev/null @@ -1,330 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005-2006 Joe Wreschnig -# 2013 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""Read and write Vorbis comment data. - -Vorbis comments are freeform key/value pairs; keys are -case-insensitive ASCII and values are Unicode strings. A key may have -multiple values. - -The specification is at http://www.xiph.org/vorbis/doc/v-comment.html. -""" - -import sys - -import mutagen -from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2 -from mutagen._util import DictMixin, cdata - - -def is_valid_key(key): - """Return true if a string is a valid Vorbis comment key. - - Valid Vorbis comment keys are printable ASCII between 0x20 (space) - and 0x7D ('}'), excluding '='. - - Takes str/unicode in Python 2, unicode in Python 3 - """ - - if PY3 and isinstance(key, bytes): - raise TypeError("needs to be str not bytes") - - for c in key: - if c < " " or c > "}" or c == "=": - return False - else: - return bool(key) - - -istag = is_valid_key - - -class error(IOError): - pass - - -class VorbisUnsetFrameError(error): - pass - - -class VorbisEncodingError(error): - pass - - -class VComment(mutagen.Metadata, list): - """A Vorbis comment parser, accessor, and renderer. - - All comment ordering is preserved. A VComment is a list of - key/value pairs, and so any Python list method can be used on it. - - Vorbis comments are always wrapped in something like an Ogg Vorbis - bitstream or a FLAC metadata block, so this loads string data or a - file-like object, not a filename. - - Attributes: - - * vendor -- the stream 'vendor' (i.e. writer); default 'Mutagen' - """ - - vendor = u"Mutagen " + mutagen.version_string - - def __init__(self, data=None, *args, **kwargs): - self._size = 0 - # Collect the args to pass to load, this lets child classes - # override just load and get equivalent magic for the - # constructor. - if data is not None: - if isinstance(data, bytes): - data = BytesIO(data) - elif not hasattr(data, 'read'): - raise TypeError("VComment requires bytes or a file-like") - start = data.tell() - self.load(data, *args, **kwargs) - self._size = data.tell() - start - - def load(self, fileobj, errors='replace', framing=True): - """Parse a Vorbis comment from a file-like object. - - Keyword arguments: - - * errors: - 'strict', 'replace', or 'ignore'. This affects Unicode decoding - and how other malformed content is interpreted. - * framing -- if true, fail if a framing bit is not present - - Framing bits are required by the Vorbis comment specification, - but are not used in FLAC Vorbis comment blocks. - """ - - try: - vendor_length = cdata.uint_le(fileobj.read(4)) - self.vendor = fileobj.read(vendor_length).decode('utf-8', errors) - count = cdata.uint_le(fileobj.read(4)) - for i in xrange(count): - length = cdata.uint_le(fileobj.read(4)) - try: - string = fileobj.read(length).decode('utf-8', errors) - except (OverflowError, MemoryError): - raise error("cannot read %d bytes, too large" % length) - try: - tag, value = string.split('=', 1) - except ValueError as err: - if errors == "ignore": - continue - elif errors == "replace": - tag, value = u"unknown%d" % i, string - else: - reraise(VorbisEncodingError, err, sys.exc_info()[2]) - try: - tag = tag.encode('ascii', errors) - except UnicodeEncodeError: - raise VorbisEncodingError("invalid tag name %r" % tag) - else: - # string keys in py3k - if PY3: - tag = tag.decode("ascii") - if is_valid_key(tag): - self.append((tag, value)) - - if framing and not bytearray(fileobj.read(1))[0] & 0x01: - raise VorbisUnsetFrameError("framing bit was unset") - except (cdata.error, TypeError): - raise error("file is not a valid Vorbis comment") - - def validate(self): - """Validate keys and values. - - Check to make sure every key used is a valid Vorbis key, and - that every value used is a valid Unicode or UTF-8 string. If - any invalid keys or values are found, a ValueError is raised. - - In Python 3 all keys and values have to be a string. - """ - - if not isinstance(self.vendor, text_type): - if PY3: - raise ValueError("vendor needs to be str") - - try: - self.vendor.decode('utf-8') - except UnicodeDecodeError: - raise ValueError - - for key, value in self: - try: - if not is_valid_key(key): - raise ValueError - except TypeError: - raise ValueError("%r is not a valid key" % key) - - if not isinstance(value, text_type): - if PY3: - raise ValueError("%r needs to be str" % key) - - try: - value.decode("utf-8") - except: - raise ValueError("%r is not a valid value" % value) - - return True - - def clear(self): - """Clear all keys from the comment.""" - - for i in list(self): - self.remove(i) - - def write(self, framing=True): - """Return a string representation of the data. - - Validation is always performed, so calling this function on - invalid data may raise a ValueError. - - Keyword arguments: - - * framing -- if true, append a framing bit (see load) - """ - - self.validate() - - def _encode(value): - if not isinstance(value, bytes): - return value.encode('utf-8') - return value - - f = BytesIO() - vendor = _encode(self.vendor) - f.write(cdata.to_uint_le(len(vendor))) - f.write(vendor) - f.write(cdata.to_uint_le(len(self))) - for tag, value in self: - tag = _encode(tag) - value = _encode(value) - comment = tag + b"=" + value - f.write(cdata.to_uint_le(len(comment))) - f.write(comment) - if framing: - f.write(b"\x01") - return f.getvalue() - - def pprint(self): - - def _decode(value): - if not isinstance(value, text_type): - return value.decode('utf-8', 'replace') - return value - - tags = [u"%s=%s" % (_decode(k), _decode(v)) for k, v in self] - return u"\n".join(tags) - - -class VCommentDict(VComment, DictMixin): - """A VComment that looks like a dictionary. - - This object differs from a dictionary in two ways. First, - len(comment) will still return the number of values, not the - number of keys. Secondly, iterating through the object will - iterate over (key, value) pairs, not keys. Since a key may have - multiple values, the same value may appear multiple times while - iterating. - - Since Vorbis comment keys are case-insensitive, all keys are - normalized to lowercase ASCII. - """ - - def __getitem__(self, key): - """A list of values for the key. - - This is a copy, so comment['title'].append('a title') will not - work. - """ - - # PY3 only - if isinstance(key, slice): - return VComment.__getitem__(self, key) - - if not is_valid_key(key): - raise ValueError - - key = key.lower() - - values = [value for (k, value) in self if k.lower() == key] - if not values: - raise KeyError(key) - else: - return values - - def __delitem__(self, key): - """Delete all values associated with the key.""" - - # PY3 only - if isinstance(key, slice): - return VComment.__delitem__(self, key) - - if not is_valid_key(key): - raise ValueError - - key = key.lower() - to_delete = [x for x in self if x[0].lower() == key] - if not to_delete: - raise KeyError(key) - else: - for item in to_delete: - self.remove(item) - - def __contains__(self, key): - """Return true if the key has any values.""" - - if not is_valid_key(key): - raise ValueError - - key = key.lower() - for k, value in self: - if k.lower() == key: - return True - else: - return False - - def __setitem__(self, key, values): - """Set a key's value or values. - - Setting a value overwrites all old ones. The value may be a - list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 - string. - """ - - # PY3 only - if isinstance(key, slice): - return VComment.__setitem__(self, key, values) - - if not is_valid_key(key): - raise ValueError - - if not isinstance(values, list): - values = [values] - try: - del(self[key]) - except KeyError: - pass - - if PY2: - key = key.encode('ascii') - - for value in values: - self.append((key, value)) - - def keys(self): - """Return all keys in the comment.""" - - return list(set([k.lower() for k, v in self])) - - def as_dict(self): - """Return a copy of the comment data in a real dict.""" - - return dict([(key, self[key]) for key in self.keys()]) diff --git a/resources/lib/libraries/mutagen/aac.py b/resources/lib/libraries/mutagen/aac.py deleted file mode 100644 index 83968a05..00000000 --- a/resources/lib/libraries/mutagen/aac.py +++ /dev/null @@ -1,410 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -""" -* ADTS - Audio Data Transport Stream -* ADIF - Audio Data Interchange Format -* See ISO/IEC 13818-7 / 14496-03 -""" - -from mutagen import StreamInfo -from mutagen._file import FileType -from mutagen._util import BitReader, BitReaderError, MutagenError -from mutagen._compat import endswith, xrange - - -_FREQS = [ - 96000, 88200, 64000, 48000, - 44100, 32000, 24000, 22050, - 16000, 12000, 11025, 8000, - 7350, -] - - -class _ADTSStream(object): - """Represents a series of frames belonging to the same stream""" - - parsed_frames = 0 - """Number of successfully parsed frames""" - - offset = 0 - """offset in bytes at which the stream starts (the first sync word)""" - - @classmethod - def find_stream(cls, fileobj, max_bytes): - """Returns a possibly valid _ADTSStream or None. - - Args: - max_bytes (int): maximum bytes to read - """ - - r = BitReader(fileobj) - stream = cls(r) - if stream.sync(max_bytes): - stream.offset = (r.get_position() - 12) // 8 - return stream - - def sync(self, max_bytes): - """Find the next sync. - Returns True if found.""" - - # at least 2 bytes for the sync - max_bytes = max(max_bytes, 2) - - r = self._r - r.align() - while max_bytes > 0: - try: - b = r.bytes(1) - if b == b"\xff": - if r.bits(4) == 0xf: - return True - r.align() - max_bytes -= 2 - else: - max_bytes -= 1 - except BitReaderError: - return False - return False - - def __init__(self, r): - """Use _ADTSStream.find_stream to create a stream""" - - self._fixed_header_key = None - self._r = r - self.offset = -1 - self.parsed_frames = 0 - - self._samples = 0 - self._payload = 0 - self._start = r.get_position() / 8 - self._last = self._start - - @property - def bitrate(self): - """Bitrate of the raw aac blocks, excluding framing/crc""" - - assert self.parsed_frames, "no frame parsed yet" - - if self._samples == 0: - return 0 - - return (8 * self._payload * self.frequency) // self._samples - - @property - def samples(self): - """samples so far""" - - assert self.parsed_frames, "no frame parsed yet" - - return self._samples - - @property - def size(self): - """bytes read in the stream so far (including framing)""" - - assert self.parsed_frames, "no frame parsed yet" - - return self._last - self._start - - @property - def channels(self): - """0 means unknown""" - - assert self.parsed_frames, "no frame parsed yet" - - b_index = self._fixed_header_key[6] - if b_index == 7: - return 8 - elif b_index > 7: - return 0 - else: - return b_index - - @property - def frequency(self): - """0 means unknown""" - - assert self.parsed_frames, "no frame parsed yet" - - f_index = self._fixed_header_key[4] - try: - return _FREQS[f_index] - except IndexError: - return 0 - - def parse_frame(self): - """True if parsing was successful. - Fails either because the frame wasn't valid or the stream ended. - """ - - try: - return self._parse_frame() - except BitReaderError: - return False - - def _parse_frame(self): - r = self._r - # start == position of sync word - start = r.get_position() - 12 - - # adts_fixed_header - id_ = r.bits(1) - layer = r.bits(2) - protection_absent = r.bits(1) - - profile = r.bits(2) - sampling_frequency_index = r.bits(4) - private_bit = r.bits(1) - # TODO: if 0 we could parse program_config_element() - channel_configuration = r.bits(3) - original_copy = r.bits(1) - home = r.bits(1) - - # the fixed header has to be the same for every frame in the stream - fixed_header_key = ( - id_, layer, protection_absent, profile, sampling_frequency_index, - private_bit, channel_configuration, original_copy, home, - ) - - if self._fixed_header_key is None: - self._fixed_header_key = fixed_header_key - else: - if self._fixed_header_key != fixed_header_key: - return False - - # adts_variable_header - r.skip(2) # copyright_identification_bit/start - frame_length = r.bits(13) - r.skip(11) # adts_buffer_fullness - nordbif = r.bits(2) - # adts_variable_header end - - crc_overhead = 0 - if not protection_absent: - crc_overhead += (nordbif + 1) * 16 - if nordbif != 0: - crc_overhead *= 2 - - left = (frame_length * 8) - (r.get_position() - start) - if left < 0: - return False - r.skip(left) - assert r.is_aligned() - - self._payload += (left - crc_overhead) / 8 - self._samples += (nordbif + 1) * 1024 - self._last = r.get_position() / 8 - - self.parsed_frames += 1 - return True - - -class ProgramConfigElement(object): - - element_instance_tag = None - object_type = None - sampling_frequency_index = None - channels = None - - def __init__(self, r): - """Reads the program_config_element() - - Raises BitReaderError - """ - - self.element_instance_tag = r.bits(4) - self.object_type = r.bits(2) - self.sampling_frequency_index = r.bits(4) - num_front_channel_elements = r.bits(4) - num_side_channel_elements = r.bits(4) - num_back_channel_elements = r.bits(4) - num_lfe_channel_elements = r.bits(2) - num_assoc_data_elements = r.bits(3) - num_valid_cc_elements = r.bits(4) - - mono_mixdown_present = r.bits(1) - if mono_mixdown_present == 1: - r.skip(4) - stereo_mixdown_present = r.bits(1) - if stereo_mixdown_present == 1: - r.skip(4) - matrix_mixdown_idx_present = r.bits(1) - if matrix_mixdown_idx_present == 1: - r.skip(3) - - elms = num_front_channel_elements + num_side_channel_elements + \ - num_back_channel_elements - channels = 0 - for i in xrange(elms): - channels += 1 - element_is_cpe = r.bits(1) - if element_is_cpe: - channels += 1 - r.skip(4) - channels += num_lfe_channel_elements - self.channels = channels - - r.skip(4 * num_lfe_channel_elements) - r.skip(4 * num_assoc_data_elements) - r.skip(5 * num_valid_cc_elements) - r.align() - comment_field_bytes = r.bits(8) - r.skip(8 * comment_field_bytes) - - -class AACError(MutagenError): - pass - - -class AACInfo(StreamInfo): - """AAC stream information. - - Attributes: - - * channels -- number of audio channels - * length -- file length in seconds, as a float - * sample_rate -- audio sampling rate in Hz - * bitrate -- audio bitrate, in bits per second - - The length of the stream is just a guess and might not be correct. - """ - - channels = 0 - length = 0 - sample_rate = 0 - bitrate = 0 - - def __init__(self, fileobj): - # skip id3v2 header - start_offset = 0 - header = fileobj.read(10) - from mutagen.id3 import BitPaddedInt - if header.startswith(b"ID3"): - size = BitPaddedInt(header[6:]) - start_offset = size + 10 - - fileobj.seek(start_offset) - adif = fileobj.read(4) - if adif == b"ADIF": - self._parse_adif(fileobj) - self._type = "ADIF" - else: - self._parse_adts(fileobj, start_offset) - self._type = "ADTS" - - def _parse_adif(self, fileobj): - r = BitReader(fileobj) - try: - copyright_id_present = r.bits(1) - if copyright_id_present: - r.skip(72) # copyright_id - r.skip(1 + 1) # original_copy, home - bitstream_type = r.bits(1) - self.bitrate = r.bits(23) - npce = r.bits(4) - if bitstream_type == 0: - r.skip(20) # adif_buffer_fullness - - pce = ProgramConfigElement(r) - try: - self.sample_rate = _FREQS[pce.sampling_frequency_index] - except IndexError: - pass - self.channels = pce.channels - - # other pces.. - for i in xrange(npce): - ProgramConfigElement(r) - r.align() - except BitReaderError as e: - raise AACError(e) - - # use bitrate + data size to guess length - start = fileobj.tell() - fileobj.seek(0, 2) - length = fileobj.tell() - start - if self.bitrate != 0: - self.length = (8.0 * length) / self.bitrate - - def _parse_adts(self, fileobj, start_offset): - max_initial_read = 512 - max_resync_read = 10 - max_sync_tries = 10 - - frames_max = 100 - frames_needed = 3 - - # Try up to X times to find a sync word and read up to Y frames. - # If more than Z frames are valid we assume a valid stream - offset = start_offset - for i in xrange(max_sync_tries): - fileobj.seek(offset) - s = _ADTSStream.find_stream(fileobj, max_initial_read) - if s is None: - raise AACError("sync not found") - # start right after the last found offset - offset += s.offset + 1 - - for i in xrange(frames_max): - if not s.parse_frame(): - break - if not s.sync(max_resync_read): - break - - if s.parsed_frames >= frames_needed: - break - else: - raise AACError( - "no valid stream found (only %d frames)" % s.parsed_frames) - - self.sample_rate = s.frequency - self.channels = s.channels - self.bitrate = s.bitrate - - # size from stream start to end of file - fileobj.seek(0, 2) - stream_size = fileobj.tell() - (offset + s.offset) - # approx - self.length = float(s.samples * stream_size) / (s.size * s.frequency) - - def pprint(self): - return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % ( - self._type, self.sample_rate, self.length, self.channels, - self.bitrate) - - -class AAC(FileType): - """Load ADTS or ADIF streams containing AAC. - - Tagging is not supported. - Use the ID3/APEv2 classes directly instead. - """ - - _mimes = ["audio/x-aac"] - - def load(self, filename): - self.filename = filename - with open(filename, "rb") as h: - self.info = AACInfo(h) - - def add_tags(self): - raise AACError("doesn't support tags") - - @staticmethod - def score(filename, fileobj, header): - filename = filename.lower() - s = endswith(filename, ".aac") or endswith(filename, ".adts") or \ - endswith(filename, ".adif") - s += b"ADIF" in header - return s - - -Open = AAC -error = AACError - -__all__ = ["AAC", "Open"] diff --git a/resources/lib/libraries/mutagen/aiff.py b/resources/lib/libraries/mutagen/aiff.py deleted file mode 100644 index dc580063..00000000 --- a/resources/lib/libraries/mutagen/aiff.py +++ /dev/null @@ -1,357 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2014 Evan Purkhiser -# 2014 Ben Ockmore -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""AIFF audio stream information and tags.""" - -import sys -import struct -from struct import pack - -from ._compat import endswith, text_type, reraise -from mutagen import StreamInfo, FileType - -from mutagen.id3 import ID3 -from mutagen.id3._util import ID3NoHeaderError, error as ID3Error -from mutagen._util import resize_bytes, delete_bytes, MutagenError - -__all__ = ["AIFF", "Open", "delete"] - - -class error(MutagenError, RuntimeError): - pass - - -class InvalidChunk(error, IOError): - pass - - -# based on stdlib's aifc -_HUGE_VAL = 1.79769313486231e+308 - - -def is_valid_chunk_id(id): - assert isinstance(id, text_type) - - return ((len(id) <= 4) and (min(id) >= u' ') and - (max(id) <= u'~')) - - -def read_float(data): # 10 bytes - expon, himant, lomant = struct.unpack('>hLL', data) - sign = 1 - if expon < 0: - sign = -1 - expon = expon + 0x8000 - if expon == himant == lomant == 0: - f = 0.0 - elif expon == 0x7FFF: - f = _HUGE_VAL - else: - expon = expon - 16383 - f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) - return sign * f - - -class IFFChunk(object): - """Representation of a single IFF chunk""" - - # Chunk headers are 8 bytes long (4 for ID and 4 for the size) - HEADER_SIZE = 8 - - def __init__(self, fileobj, parent_chunk=None): - self.__fileobj = fileobj - self.parent_chunk = parent_chunk - self.offset = fileobj.tell() - - header = fileobj.read(self.HEADER_SIZE) - if len(header) < self.HEADER_SIZE: - raise InvalidChunk() - - self.id, self.data_size = struct.unpack('>4si', header) - - try: - self.id = self.id.decode('ascii') - except UnicodeDecodeError: - raise InvalidChunk() - - if not is_valid_chunk_id(self.id): - raise InvalidChunk() - - self.size = self.HEADER_SIZE + self.data_size - self.data_offset = fileobj.tell() - - def read(self): - """Read the chunks data""" - - self.__fileobj.seek(self.data_offset) - return self.__fileobj.read(self.data_size) - - def write(self, data): - """Write the chunk data""" - - if len(data) > self.data_size: - raise ValueError - - self.__fileobj.seek(self.data_offset) - self.__fileobj.write(data) - - def delete(self): - """Removes the chunk from the file""" - - delete_bytes(self.__fileobj, self.size, self.offset) - if self.parent_chunk is not None: - self.parent_chunk._update_size( - self.parent_chunk.data_size - self.size) - - def _update_size(self, data_size): - """Update the size of the chunk""" - - self.__fileobj.seek(self.offset + 4) - self.__fileobj.write(pack('>I', data_size)) - if self.parent_chunk is not None: - size_diff = self.data_size - data_size - self.parent_chunk._update_size( - self.parent_chunk.data_size - size_diff) - self.data_size = data_size - self.size = data_size + self.HEADER_SIZE - - def resize(self, new_data_size): - """Resize the file and update the chunk sizes""" - - resize_bytes( - self.__fileobj, self.data_size, new_data_size, self.data_offset) - self._update_size(new_data_size) - - -class IFFFile(object): - """Representation of a IFF file""" - - def __init__(self, fileobj): - self.__fileobj = fileobj - self.__chunks = {} - - # AIFF Files always start with the FORM chunk which contains a 4 byte - # ID before the start of other chunks - fileobj.seek(0) - self.__chunks[u'FORM'] = IFFChunk(fileobj) - - # Skip past the 4 byte FORM id - fileobj.seek(IFFChunk.HEADER_SIZE + 4) - - # Where the next chunk can be located. We need to keep track of this - # since the size indicated in the FORM header may not match up with the - # offset determined from the size of the last chunk in the file - self.__next_offset = fileobj.tell() - - # Load all of the chunks - while True: - try: - chunk = IFFChunk(fileobj, self[u'FORM']) - except InvalidChunk: - break - self.__chunks[chunk.id.strip()] = chunk - - # Calculate the location of the next chunk, - # considering the pad byte - self.__next_offset = chunk.offset + chunk.size - self.__next_offset += self.__next_offset % 2 - fileobj.seek(self.__next_offset) - - def __contains__(self, id_): - """Check if the IFF file contains a specific chunk""" - - assert isinstance(id_, text_type) - - if not is_valid_chunk_id(id_): - raise KeyError("AIFF key must be four ASCII characters.") - - return id_ in self.__chunks - - def __getitem__(self, id_): - """Get a chunk from the IFF file""" - - assert isinstance(id_, text_type) - - if not is_valid_chunk_id(id_): - raise KeyError("AIFF key must be four ASCII characters.") - - try: - return self.__chunks[id_] - except KeyError: - raise KeyError( - "%r has no %r chunk" % (self.__fileobj.name, id_)) - - def __delitem__(self, id_): - """Remove a chunk from the IFF file""" - - assert isinstance(id_, text_type) - - if not is_valid_chunk_id(id_): - raise KeyError("AIFF key must be four ASCII characters.") - - self.__chunks.pop(id_).delete() - - def insert_chunk(self, id_): - """Insert a new chunk at the end of the IFF file""" - - assert isinstance(id_, text_type) - - if not is_valid_chunk_id(id_): - raise KeyError("AIFF key must be four ASCII characters.") - - self.__fileobj.seek(self.__next_offset) - self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0)) - self.__fileobj.seek(self.__next_offset) - chunk = IFFChunk(self.__fileobj, self[u'FORM']) - self[u'FORM']._update_size(self[u'FORM'].data_size + chunk.size) - - self.__chunks[id_] = chunk - self.__next_offset = chunk.offset + chunk.size - - -class AIFFInfo(StreamInfo): - """AIFF audio stream information. - - Information is parsed from the COMM chunk of the AIFF file - - Useful attributes: - - * length -- audio length, in seconds - * bitrate -- audio bitrate, in bits per second - * channels -- The number of audio channels - * sample_rate -- audio sample rate, in Hz - * sample_size -- The audio sample size - """ - - length = 0 - bitrate = 0 - channels = 0 - sample_rate = 0 - - def __init__(self, fileobj): - iff = IFFFile(fileobj) - try: - common_chunk = iff[u'COMM'] - except KeyError as e: - raise error(str(e)) - - data = common_chunk.read() - - info = struct.unpack('>hLh10s', data[:18]) - channels, frame_count, sample_size, sample_rate = info - - self.sample_rate = int(read_float(sample_rate)) - self.sample_size = sample_size - self.channels = channels - self.bitrate = channels * sample_size * self.sample_rate - self.length = frame_count / float(self.sample_rate) - - def pprint(self): - return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % ( - self.channels, self.bitrate, self.sample_rate, self.length) - - -class _IFFID3(ID3): - """A AIFF file with ID3v2 tags""" - - def _pre_load_header(self, fileobj): - try: - fileobj.seek(IFFFile(fileobj)[u'ID3'].data_offset) - except (InvalidChunk, KeyError): - raise ID3NoHeaderError("No ID3 chunk") - - def save(self, filename=None, v2_version=4, v23_sep='/', padding=None): - """Save ID3v2 data to the AIFF file""" - - if filename is None: - filename = self.filename - - # Unlike the parent ID3.save method, we won't save to a blank file - # since we would have to construct a empty AIFF file - with open(filename, 'rb+') as fileobj: - iff_file = IFFFile(fileobj) - - if u'ID3' not in iff_file: - iff_file.insert_chunk(u'ID3') - - chunk = iff_file[u'ID3'] - - try: - data = self._prepare_data( - fileobj, chunk.data_offset, chunk.data_size, v2_version, - v23_sep, padding) - except ID3Error as e: - reraise(error, e, sys.exc_info()[2]) - - new_size = len(data) - new_size += new_size % 2 # pad byte - assert new_size % 2 == 0 - chunk.resize(new_size) - data += (new_size - len(data)) * b'\x00' - assert new_size == len(data) - chunk.write(data) - - def delete(self, filename=None): - """Completely removes the ID3 chunk from the AIFF file""" - - if filename is None: - filename = self.filename - delete(filename) - self.clear() - - -def delete(filename): - """Completely removes the ID3 chunk from the AIFF file""" - - with open(filename, "rb+") as file_: - try: - del IFFFile(file_)[u'ID3'] - except KeyError: - pass - - -class AIFF(FileType): - """An AIFF audio file. - - :ivar info: :class:`AIFFInfo` - :ivar tags: :class:`ID3` - """ - - _mimes = ["audio/aiff", "audio/x-aiff"] - - @staticmethod - def score(filename, fileobj, header): - filename = filename.lower() - - return (header.startswith(b"FORM") * 2 + endswith(filename, b".aif") + - endswith(filename, b".aiff") + endswith(filename, b".aifc")) - - def add_tags(self): - """Add an empty ID3 tag to the file.""" - if self.tags is None: - self.tags = _IFFID3() - else: - raise error("an ID3 tag already exists") - - def load(self, filename, **kwargs): - """Load stream and tag information from a file.""" - self.filename = filename - - try: - self.tags = _IFFID3(filename, **kwargs) - except ID3NoHeaderError: - self.tags = None - except ID3Error as e: - raise error(e) - - with open(filename, "rb") as fileobj: - self.info = AIFFInfo(fileobj) - - -Open = AIFF diff --git a/resources/lib/libraries/mutagen/apev2.py b/resources/lib/libraries/mutagen/apev2.py deleted file mode 100644 index 3b79aba9..00000000 --- a/resources/lib/libraries/mutagen/apev2.py +++ /dev/null @@ -1,710 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""APEv2 reading and writing. - -The APEv2 format is most commonly used with Musepack files, but is -also the format of choice for WavPack and other formats. Some MP3s -also have APEv2 tags, but this can cause problems with many MP3 -decoders and taggers. - -APEv2 tags, like Vorbis comments, are freeform key=value pairs. APEv2 -keys can be any ASCII string with characters from 0x20 to 0x7E, -between 2 and 255 characters long. Keys are case-sensitive, but -readers are recommended to be case insensitive, and it is forbidden to -multiple keys which differ only in case. Keys are usually stored -title-cased (e.g. 'Artist' rather than 'artist'). - -APEv2 values are slightly more structured than Vorbis comments; values -are flagged as one of text, binary, or an external reference (usually -a URI). - -Based off the format specification found at -http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification. -""" - -__all__ = ["APEv2", "APEv2File", "Open", "delete"] - -import sys -import struct -from collections import MutableSequence - -from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string, - xrange) -from mutagen import Metadata, FileType, StreamInfo -from mutagen._util import (DictMixin, cdata, delete_bytes, total_ordering, - MutagenError) - - -def is_valid_apev2_key(key): - if not isinstance(key, text_type): - if PY3: - raise TypeError("APEv2 key must be str") - - try: - key = key.decode('ascii') - except UnicodeDecodeError: - return False - - # PY26 - Change to set literal syntax (since set is faster than list here) - return ((2 <= len(key) <= 255) and (min(key) >= u' ') and - (max(key) <= u'~') and - (key not in [u"OggS", u"TAG", u"ID3", u"MP+"])) - -# There are three different kinds of APE tag values. -# "0: Item contains text information coded in UTF-8 -# 1: Item contains binary information -# 2: Item is a locator of external stored information [e.g. URL] -# 3: reserved" -TEXT, BINARY, EXTERNAL = xrange(3) - -HAS_HEADER = 1 << 31 -HAS_NO_FOOTER = 1 << 30 -IS_HEADER = 1 << 29 - - -class error(IOError, MutagenError): - pass - - -class APENoHeaderError(error, ValueError): - pass - - -class APEUnsupportedVersionError(error, ValueError): - pass - - -class APEBadItemError(error, ValueError): - pass - - -class _APEv2Data(object): - # Store offsets of the important parts of the file. - start = header = data = footer = end = None - # Footer or header; seek here and read 32 to get version/size/items/flags - metadata = None - # Actual tag data - tag = None - - version = None - size = None - items = None - flags = 0 - - # The tag is at the start rather than the end. A tag at both - # the start and end of the file (i.e. the tag is the whole file) - # is not considered to be at the start. - is_at_start = False - - def __init__(self, fileobj): - self.__find_metadata(fileobj) - - if self.header is None: - self.metadata = self.footer - elif self.footer is None: - self.metadata = self.header - else: - self.metadata = max(self.header, self.footer) - - if self.metadata is None: - return - - self.__fill_missing(fileobj) - self.__fix_brokenness(fileobj) - if self.data is not None: - fileobj.seek(self.data) - self.tag = fileobj.read(self.size) - - def __find_metadata(self, fileobj): - # Try to find a header or footer. - - # Check for a simple footer. - try: - fileobj.seek(-32, 2) - except IOError: - fileobj.seek(0, 2) - return - if fileobj.read(8) == b"APETAGEX": - fileobj.seek(-8, 1) - self.footer = self.metadata = fileobj.tell() - return - - # Check for an APEv2 tag followed by an ID3v1 tag at the end. - try: - fileobj.seek(-128, 2) - if fileobj.read(3) == b"TAG": - - fileobj.seek(-35, 1) # "TAG" + header length - if fileobj.read(8) == b"APETAGEX": - fileobj.seek(-8, 1) - self.footer = fileobj.tell() - return - - # ID3v1 tag at the end, maybe preceded by Lyrics3v2. - # (http://www.id3.org/lyrics3200.html) - # (header length - "APETAGEX") - "LYRICS200" - fileobj.seek(15, 1) - if fileobj.read(9) == b'LYRICS200': - fileobj.seek(-15, 1) # "LYRICS200" + size tag - try: - offset = int(fileobj.read(6)) - except ValueError: - raise IOError - - fileobj.seek(-32 - offset - 6, 1) - if fileobj.read(8) == b"APETAGEX": - fileobj.seek(-8, 1) - self.footer = fileobj.tell() - return - - except IOError: - pass - - # Check for a tag at the start. - fileobj.seek(0, 0) - if fileobj.read(8) == b"APETAGEX": - self.is_at_start = True - self.header = 0 - - def __fill_missing(self, fileobj): - fileobj.seek(self.metadata + 8) - self.version = fileobj.read(4) - self.size = cdata.uint_le(fileobj.read(4)) - self.items = cdata.uint_le(fileobj.read(4)) - self.flags = cdata.uint_le(fileobj.read(4)) - - if self.header is not None: - self.data = self.header + 32 - # If we're reading the header, the size is the header - # offset + the size, which includes the footer. - self.end = self.data + self.size - fileobj.seek(self.end - 32, 0) - if fileobj.read(8) == b"APETAGEX": - self.footer = self.end - 32 - elif self.footer is not None: - self.end = self.footer + 32 - self.data = self.end - self.size - if self.flags & HAS_HEADER: - self.header = self.data - 32 - else: - self.header = self.data - else: - raise APENoHeaderError("No APE tag found") - - # exclude the footer from size - if self.footer is not None: - self.size -= 32 - - def __fix_brokenness(self, fileobj): - # Fix broken tags written with PyMusepack. - if self.header is not None: - start = self.header - else: - start = self.data - fileobj.seek(start) - - while start > 0: - # Clean up broken writing from pre-Mutagen PyMusepack. - # It didn't remove the first 24 bytes of header. - try: - fileobj.seek(-24, 1) - except IOError: - break - else: - if fileobj.read(8) == b"APETAGEX": - fileobj.seek(-8, 1) - start = fileobj.tell() - else: - break - self.start = start - - -class _CIDictProxy(DictMixin): - - def __init__(self, *args, **kwargs): - self.__casemap = {} - self.__dict = {} - super(_CIDictProxy, self).__init__(*args, **kwargs) - # Internally all names are stored as lowercase, but the case - # they were set with is remembered and used when saving. This - # is roughly in line with the standard, which says that keys - # are case-sensitive but two keys differing only in case are - # not allowed, and recommends case-insensitive - # implementations. - - def __getitem__(self, key): - return self.__dict[key.lower()] - - def __setitem__(self, key, value): - lower = key.lower() - self.__casemap[lower] = key - self.__dict[lower] = value - - def __delitem__(self, key): - lower = key.lower() - del(self.__casemap[lower]) - del(self.__dict[lower]) - - def keys(self): - return [self.__casemap.get(key, key) for key in self.__dict.keys()] - - -class APEv2(_CIDictProxy, Metadata): - """A file with an APEv2 tag. - - ID3v1 tags are silently ignored and overwritten. - """ - - filename = None - - def pprint(self): - """Return tag key=value pairs in a human-readable format.""" - - items = sorted(self.items()) - return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items) - - def load(self, filename): - """Load tags from a filename.""" - - self.filename = filename - with open(filename, "rb") as fileobj: - data = _APEv2Data(fileobj) - - if data.tag: - self.clear() - self.__parse_tag(data.tag, data.items) - else: - raise APENoHeaderError("No APE tag found") - - def __parse_tag(self, tag, count): - fileobj = cBytesIO(tag) - - for i in xrange(count): - size_data = fileobj.read(4) - # someone writes wrong item counts - if not size_data: - break - size = cdata.uint_le(size_data) - flags = cdata.uint_le(fileobj.read(4)) - - # Bits 1 and 2 bits are flags, 0-3 - # Bit 0 is read/write flag, ignored - kind = (flags & 6) >> 1 - if kind == 3: - raise APEBadItemError("value type must be 0, 1, or 2") - key = value = fileobj.read(1) - while key[-1:] != b'\x00' and value: - value = fileobj.read(1) - key += value - if key[-1:] == b"\x00": - key = key[:-1] - if PY3: - try: - key = key.decode("ascii") - except UnicodeError as err: - reraise(APEBadItemError, err, sys.exc_info()[2]) - value = fileobj.read(size) - - value = _get_value_type(kind)._new(value) - - self[key] = value - - def __getitem__(self, key): - if not is_valid_apev2_key(key): - raise KeyError("%r is not a valid APEv2 key" % key) - if PY2: - key = key.encode('ascii') - - return super(APEv2, self).__getitem__(key) - - def __delitem__(self, key): - if not is_valid_apev2_key(key): - raise KeyError("%r is not a valid APEv2 key" % key) - if PY2: - key = key.encode('ascii') - - super(APEv2, self).__delitem__(key) - - def __setitem__(self, key, value): - """'Magic' value setter. - - This function tries to guess at what kind of value you want to - store. If you pass in a valid UTF-8 or Unicode string, it - treats it as a text value. If you pass in a list, it treats it - as a list of string/Unicode values. If you pass in a string - that is not valid UTF-8, it assumes it is a binary value. - - Python 3: all bytes will be assumed to be a byte value, even - if they are valid utf-8. - - If you need to force a specific type of value (e.g. binary - data that also happens to be valid UTF-8, or an external - reference), use the APEValue factory and set the value to the - result of that:: - - from mutagen.apev2 import APEValue, EXTERNAL - tag['Website'] = APEValue('http://example.org', EXTERNAL) - """ - - if not is_valid_apev2_key(key): - raise KeyError("%r is not a valid APEv2 key" % key) - - if PY2: - key = key.encode('ascii') - - if not isinstance(value, _APEValue): - # let's guess at the content if we're not already a value... - if isinstance(value, text_type): - # unicode? we've got to be text. - value = APEValue(value, TEXT) - elif isinstance(value, list): - items = [] - for v in value: - if not isinstance(v, text_type): - if PY3: - raise TypeError("item in list not str") - v = v.decode("utf-8") - items.append(v) - - # list? text. - value = APEValue(u"\0".join(items), TEXT) - else: - if PY3: - value = APEValue(value, BINARY) - else: - try: - value.decode("utf-8") - except UnicodeError: - # invalid UTF8 text, probably binary - value = APEValue(value, BINARY) - else: - # valid UTF8, probably text - value = APEValue(value, TEXT) - - super(APEv2, self).__setitem__(key, value) - - def save(self, filename=None): - """Save changes to a file. - - If no filename is given, the one most recently loaded is used. - - Tags are always written at the end of the file, and include - a header and a footer. - """ - - filename = filename or self.filename - try: - fileobj = open(filename, "r+b") - except IOError: - fileobj = open(filename, "w+b") - data = _APEv2Data(fileobj) - - if data.is_at_start: - delete_bytes(fileobj, data.end - data.start, data.start) - elif data.start is not None: - fileobj.seek(data.start) - # Delete an ID3v1 tag if present, too. - fileobj.truncate() - fileobj.seek(0, 2) - - tags = [] - for key, value in self.items(): - # Packed format for an item: - # 4B: Value length - # 4B: Value type - # Key name - # 1B: Null - # Key value - value_data = value._write() - if not isinstance(key, bytes): - key = key.encode("utf-8") - tag_data = bytearray() - tag_data += struct.pack("<2I", len(value_data), value.kind << 1) - tag_data += key + b"\0" + value_data - tags.append(bytes(tag_data)) - - # "APE tags items should be sorted ascending by size... This is - # not a MUST, but STRONGLY recommended. Actually the items should - # be sorted by importance/byte, but this is not feasible." - tags.sort(key=len) - num_tags = len(tags) - tags = b"".join(tags) - - header = bytearray(b"APETAGEX") - # version, tag size, item count, flags - header += struct.pack("<4I", 2000, len(tags) + 32, num_tags, - HAS_HEADER | IS_HEADER) - header += b"\0" * 8 - fileobj.write(header) - - fileobj.write(tags) - - footer = bytearray(b"APETAGEX") - footer += struct.pack("<4I", 2000, len(tags) + 32, num_tags, - HAS_HEADER) - footer += b"\0" * 8 - - fileobj.write(footer) - fileobj.close() - - def delete(self, filename=None): - """Remove tags from a file.""" - - filename = filename or self.filename - with open(filename, "r+b") as fileobj: - data = _APEv2Data(fileobj) - if data.start is not None and data.size is not None: - delete_bytes(fileobj, data.end - data.start, data.start) - - self.clear() - - -Open = APEv2 - - -def delete(filename): - """Remove tags from a file.""" - - try: - APEv2(filename).delete() - except APENoHeaderError: - pass - - -def _get_value_type(kind): - """Returns a _APEValue subclass or raises ValueError""" - - if kind == TEXT: - return APETextValue - elif kind == BINARY: - return APEBinaryValue - elif kind == EXTERNAL: - return APEExtValue - raise ValueError("unknown kind %r" % kind) - - -def APEValue(value, kind): - """APEv2 tag value factory. - - Use this if you need to specify the value's type manually. Binary - and text data are automatically detected by APEv2.__setitem__. - """ - - try: - type_ = _get_value_type(kind) - except ValueError: - raise ValueError("kind must be TEXT, BINARY, or EXTERNAL") - else: - return type_(value) - - -class _APEValue(object): - - kind = None - value = None - - def __init__(self, value, kind=None): - # kind kwarg is for backwards compat - if kind is not None and kind != self.kind: - raise ValueError - self.value = self._validate(value) - - @classmethod - def _new(cls, data): - instance = cls.__new__(cls) - instance._parse(data) - return instance - - def _parse(self, data): - """Sets value or raises APEBadItemError""" - - raise NotImplementedError - - def _write(self): - """Returns bytes""" - - raise NotImplementedError - - def _validate(self, value): - """Returns validated value or raises TypeError/ValueErrr""" - - raise NotImplementedError - - def __repr__(self): - return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind) - - -@swap_to_string -@total_ordering -class _APEUtf8Value(_APEValue): - - def _parse(self, data): - try: - self.value = data.decode("utf-8") - except UnicodeDecodeError as e: - reraise(APEBadItemError, e, sys.exc_info()[2]) - - def _validate(self, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") - return value - - def _write(self): - return self.value.encode("utf-8") - - def __len__(self): - return len(self.value) - - def __bytes__(self): - return self._write() - - def __eq__(self, other): - return self.value == other - - def __lt__(self, other): - return self.value < other - - def __str__(self): - return self.value - - -class APETextValue(_APEUtf8Value, MutableSequence): - """An APEv2 text value. - - Text values are Unicode/UTF-8 strings. They can be accessed like - strings (with a null separating the values), or arrays of strings. - """ - - kind = TEXT - - def __iter__(self): - """Iterate over the strings of the value (not the characters)""" - - return iter(self.value.split(u"\0")) - - def __getitem__(self, index): - return self.value.split(u"\0")[index] - - def __len__(self): - return self.value.count(u"\0") + 1 - - def __setitem__(self, index, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") - - values = list(self) - values[index] = value - self.value = u"\0".join(values) - - def insert(self, index, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") - - values = list(self) - values.insert(index, value) - self.value = u"\0".join(values) - - def __delitem__(self, index): - values = list(self) - del values[index] - self.value = u"\0".join(values) - - def pprint(self): - return u" / ".join(self) - - -@swap_to_string -@total_ordering -class APEBinaryValue(_APEValue): - """An APEv2 binary value.""" - - kind = BINARY - - def _parse(self, data): - self.value = data - - def _write(self): - return self.value - - def _validate(self, value): - if not isinstance(value, bytes): - raise TypeError("value not bytes") - return bytes(value) - - def __len__(self): - return len(self.value) - - def __bytes__(self): - return self._write() - - def __eq__(self, other): - return self.value == other - - def __lt__(self, other): - return self.value < other - - def pprint(self): - return u"[%d bytes]" % len(self) - - -class APEExtValue(_APEUtf8Value): - """An APEv2 external value. - - External values are usually URI or IRI strings. - """ - - kind = EXTERNAL - - def pprint(self): - return u"[External] %s" % self.value - - -class APEv2File(FileType): - class _Info(StreamInfo): - length = 0 - bitrate = 0 - - def __init__(self, fileobj): - pass - - @staticmethod - def pprint(): - return u"Unknown format with APEv2 tag." - - def load(self, filename): - self.filename = filename - self.info = self._Info(open(filename, "rb")) - try: - self.tags = APEv2(filename) - except APENoHeaderError: - self.tags = None - - def add_tags(self): - if self.tags is None: - self.tags = APEv2() - else: - raise error("%r already has tags: %r" % (self, self.tags)) - - @staticmethod - def score(filename, fileobj, header): - try: - fileobj.seek(-160, 2) - except IOError: - fileobj.seek(0) - footer = fileobj.read() - return ((b"APETAGEX" in footer) - header.startswith(b"ID3")) diff --git a/resources/lib/libraries/mutagen/asf/__init__.py b/resources/lib/libraries/mutagen/asf/__init__.py deleted file mode 100644 index e667192d..00000000 --- a/resources/lib/libraries/mutagen/asf/__init__.py +++ /dev/null @@ -1,319 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2005-2006 Joe Wreschnig -# Copyright (C) 2006-2007 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write ASF (Window Media Audio) files.""" - -__all__ = ["ASF", "Open"] - -from mutagen import FileType, Metadata, StreamInfo -from mutagen._util import resize_bytes, DictMixin -from mutagen._compat import string_types, long_, PY3, izip - -from ._util import error, ASFError, ASFHeaderError -from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ - ExtendedContentDescriptionObject, HeaderExtensionObject, \ - ContentDescriptionObject -from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \ - ASFDWordAttribute, ASFBoolAttribute, ASFByteArrayAttribute, \ - ASFUnicodeAttribute, ASFBaseAttribute, ASFValue - - -# pyflakes -error, ASFError, ASFHeaderError, ASFValue - - -class ASFInfo(StreamInfo): - """ASF stream information.""" - - length = 0.0 - """Length in seconds (`float`)""" - - sample_rate = 0 - """Sample rate in Hz (`int`)""" - - bitrate = 0 - """Bitrate in bps (`int`)""" - - channels = 0 - """Number of channels (`int`)""" - - codec_type = u"" - """Name of the codec type of the first audio stream or - an empty string if unknown. Example: ``Windows Media Audio 9 Standard`` - (:class:`mutagen.text`) - """ - - codec_name = u"" - """Name and maybe version of the codec used. Example: - ``Windows Media Audio 9.1`` (:class:`mutagen.text`) - """ - - codec_description = u"" - """Further information on the codec used. - Example: ``64 kbps, 48 kHz, stereo 2-pass CBR`` (:class:`mutagen.text`) - """ - - def __init__(self): - self.length = 0.0 - self.sample_rate = 0 - self.bitrate = 0 - self.channels = 0 - self.codec_type = u"" - self.codec_name = u"" - self.codec_description = u"" - - def pprint(self): - """Returns a stream information text summary - - :rtype: text - """ - - s = u"ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % ( - self.codec_type or self.codec_name or u"???", self.bitrate, - self.sample_rate, self.channels, self.length) - return s - - -class ASFTags(list, DictMixin, Metadata): - """Dictionary containing ASF attributes.""" - - def __getitem__(self, key): - """A list of values for the key. - - This is a copy, so comment['title'].append('a title') will not - work. - - """ - - # PY3 only - if isinstance(key, slice): - return list.__getitem__(self, key) - - values = [value for (k, value) in self if k == key] - if not values: - raise KeyError(key) - else: - return values - - def __delitem__(self, key): - """Delete all values associated with the key.""" - - # PY3 only - if isinstance(key, slice): - return list.__delitem__(self, key) - - to_delete = [x for x in self if x[0] == key] - if not to_delete: - raise KeyError(key) - else: - for k in to_delete: - self.remove(k) - - def __contains__(self, key): - """Return true if the key has any values.""" - for k, value in self: - if k == key: - return True - else: - return False - - def __setitem__(self, key, values): - """Set a key's value or values. - - Setting a value overwrites all old ones. The value may be a - list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 - string. - """ - - # PY3 only - if isinstance(key, slice): - return list.__setitem__(self, key, values) - - if not isinstance(values, list): - values = [values] - - to_append = [] - for value in values: - if not isinstance(value, ASFBaseAttribute): - if isinstance(value, string_types): - value = ASFUnicodeAttribute(value) - elif PY3 and isinstance(value, bytes): - value = ASFByteArrayAttribute(value) - elif isinstance(value, bool): - value = ASFBoolAttribute(value) - elif isinstance(value, int): - value = ASFDWordAttribute(value) - elif isinstance(value, long_): - value = ASFQWordAttribute(value) - else: - raise TypeError("Invalid type %r" % type(value)) - to_append.append((key, value)) - - try: - del(self[key]) - except KeyError: - pass - - self.extend(to_append) - - def keys(self): - """Return a sequence of all keys in the comment.""" - - return self and set(next(izip(*self))) - - def as_dict(self): - """Return a copy of the comment data in a real dict.""" - - d = {} - for key, value in self: - d.setdefault(key, []).append(value) - return d - - def pprint(self): - """Returns a string containing all key, value pairs. - - :rtype: text - """ - - return "\n".join("%s=%s" % (k, v) for k, v in self) - - -UNICODE = ASFUnicodeAttribute.TYPE -"""Unicode string type""" - -BYTEARRAY = ASFByteArrayAttribute.TYPE -"""Byte array type""" - -BOOL = ASFBoolAttribute.TYPE -"""Bool type""" - -DWORD = ASFDWordAttribute.TYPE -""""DWord type (uint32)""" - -QWORD = ASFQWordAttribute.TYPE -"""QWord type (uint64)""" - -WORD = ASFWordAttribute.TYPE -"""Word type (uint16)""" - -GUID = ASFGUIDAttribute.TYPE -"""GUID type""" - - -class ASF(FileType): - """An ASF file, probably containing WMA or WMV. - - :param filename: a filename to load - :raises mutagen.asf.error: In case loading fails - """ - - _mimes = ["audio/x-ms-wma", "audio/x-ms-wmv", "video/x-ms-asf", - "audio/x-wma", "video/x-wmv"] - - info = None - """A `ASFInfo` instance""" - - tags = None - """A `ASFTags` instance""" - - def load(self, filename): - self.filename = filename - self.info = ASFInfo() - self.tags = ASFTags() - - with open(filename, "rb") as fileobj: - self._tags = {} - - self._header = HeaderObject.parse_full(self, fileobj) - - for guid in [ContentDescriptionObject.GUID, - ExtendedContentDescriptionObject.GUID, MetadataObject.GUID, - MetadataLibraryObject.GUID]: - self.tags.extend(self._tags.pop(guid, [])) - - assert not self._tags - - def save(self, filename=None, padding=None): - """Save tag changes back to the loaded file. - - :param padding: A callback which returns the amount of padding to use. - See :class:`mutagen.PaddingInfo` - - :raises mutagen.asf.error: In case saving fails - """ - - if filename is not None and filename != self.filename: - raise ValueError("saving to another file not supported atm") - - # Move attributes to the right objects - self.to_content_description = {} - self.to_extended_content_description = {} - self.to_metadata = {} - self.to_metadata_library = [] - for name, value in self.tags: - library_only = (value.data_size() > 0xFFFF or value.TYPE == GUID) - can_cont_desc = value.TYPE == UNICODE - - if library_only or value.language is not None: - self.to_metadata_library.append((name, value)) - elif value.stream is not None: - if name not in self.to_metadata: - self.to_metadata[name] = value - else: - self.to_metadata_library.append((name, value)) - elif name in ContentDescriptionObject.NAMES: - if name not in self.to_content_description and can_cont_desc: - self.to_content_description[name] = value - else: - self.to_metadata_library.append((name, value)) - else: - if name not in self.to_extended_content_description: - self.to_extended_content_description[name] = value - else: - self.to_metadata_library.append((name, value)) - - # Add missing objects - header = self._header - if header.get_child(ContentDescriptionObject.GUID) is None: - header.objects.append(ContentDescriptionObject()) - if header.get_child(ExtendedContentDescriptionObject.GUID) is None: - header.objects.append(ExtendedContentDescriptionObject()) - header_ext = header.get_child(HeaderExtensionObject.GUID) - if header_ext is None: - header_ext = HeaderExtensionObject() - header.objects.append(header_ext) - if header_ext.get_child(MetadataObject.GUID) is None: - header_ext.objects.append(MetadataObject()) - if header_ext.get_child(MetadataLibraryObject.GUID) is None: - header_ext.objects.append(MetadataLibraryObject()) - - # Render to file - with open(self.filename, "rb+") as fileobj: - old_size = header.parse_size(fileobj)[0] - data = header.render_full(self, fileobj, old_size, padding) - size = len(data) - resize_bytes(fileobj, old_size, size, 0) - fileobj.seek(0) - fileobj.write(data) - - def add_tags(self): - raise ASFError - - def delete(self, filename=None): - - if filename is not None and filename != self.filename: - raise ValueError("saving to another file not supported atm") - - self.tags.clear() - self.save(padding=lambda x: 0) - - @staticmethod - def score(filename, fileobj, header): - return header.startswith(HeaderObject.GUID) * 2 - -Open = ASF diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index 277e2c5fe677f3fb5deae56dc081e4c55d092175..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8567 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 GqW=Or25P<l diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc deleted file mode 100644 index aa916eddf8713bbf7a8cf3defef0062f0f3221c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15131 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 diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc deleted file mode 100644 index cb0810d8909b8a57de791aca68842bd2ba3565d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15903 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 NVtv;%exDt){tt%Az9#?x diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc deleted file mode 100644 index 661bff507dd00b7435a4f08bb07b58b7deed1cf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10603 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 J|NUPB{{?fvP5S@< diff --git a/resources/lib/libraries/mutagen/asf/_attrs.py b/resources/lib/libraries/mutagen/asf/_attrs.py deleted file mode 100644 index 4621c9fa..00000000 --- a/resources/lib/libraries/mutagen/asf/_attrs.py +++ /dev/null @@ -1,438 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2005-2006 Joe Wreschnig -# Copyright (C) 2006-2007 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -import sys -import struct - -from mutagen._compat import swap_to_string, text_type, PY2, reraise -from mutagen._util import total_ordering - -from ._util import ASFError - - -class ASFBaseAttribute(object): - """Generic attribute.""" - - TYPE = None - - _TYPES = {} - - value = None - """The Python value of this attribute (type depends on the class)""" - - language = None - """Language""" - - stream = None - """Stream""" - - def __init__(self, value=None, data=None, language=None, - stream=None, **kwargs): - self.language = language - self.stream = stream - if data: - self.value = self.parse(data, **kwargs) - else: - if value is None: - # we used to support not passing any args and instead assign - # them later, keep that working.. - self.value = None - else: - self.value = self._validate(value) - - @classmethod - def _register(cls, other): - cls._TYPES[other.TYPE] = other - return other - - @classmethod - def _get_type(cls, type_): - """Raises KeyError""" - - return cls._TYPES[type_] - - def _validate(self, value): - """Raises TypeError or ValueError in case the user supplied value - isn't valid. - """ - - return value - - def data_size(self): - raise NotImplementedError - - def __repr__(self): - name = "%s(%r" % (type(self).__name__, self.value) - if self.language: - name += ", language=%d" % self.language - if self.stream: - name += ", stream=%d" % self.stream - name += ")" - return name - - def render(self, name): - name = name.encode("utf-16-le") + b"\x00\x00" - data = self._render() - return (struct.pack("<H", len(name)) + name + - struct.pack("<HH", self.TYPE, len(data)) + data) - - def render_m(self, name): - name = name.encode("utf-16-le") + b"\x00\x00" - if self.TYPE == 2: - data = self._render(dword=False) - else: - data = self._render() - return (struct.pack("<HHHHI", 0, self.stream or 0, len(name), - self.TYPE, len(data)) + name + data) - - def render_ml(self, name): - name = name.encode("utf-16-le") + b"\x00\x00" - if self.TYPE == 2: - data = self._render(dword=False) - else: - data = self._render() - - return (struct.pack("<HHHHI", self.language or 0, self.stream or 0, - len(name), self.TYPE, len(data)) + name + data) - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFUnicodeAttribute(ASFBaseAttribute): - """Unicode string attribute. - - :: - - ASFUnicodeAttribute(u'some text') - """ - - TYPE = 0x0000 - - def parse(self, data): - try: - return data.decode("utf-16-le").strip("\x00") - except UnicodeDecodeError as e: - reraise(ASFError, e, sys.exc_info()[2]) - - def _validate(self, value): - if not isinstance(value, text_type): - if PY2: - return value.decode("utf-8") - else: - raise TypeError("%r not str" % value) - return value - - def _render(self): - return self.value.encode("utf-16-le") + b"\x00\x00" - - def data_size(self): - return len(self._render()) - - def __bytes__(self): - return self.value.encode("utf-16-le") - - def __str__(self): - return self.value - - def __eq__(self, other): - return text_type(self) == other - - def __lt__(self, other): - return text_type(self) < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFByteArrayAttribute(ASFBaseAttribute): - """Byte array attribute. - - :: - - ASFByteArrayAttribute(b'1234') - """ - TYPE = 0x0001 - - def parse(self, data): - assert isinstance(data, bytes) - return data - - def _render(self): - assert isinstance(self.value, bytes) - return self.value - - def _validate(self, value): - if not isinstance(value, bytes): - raise TypeError("must be bytes/str: %r" % value) - return value - - def data_size(self): - return len(self.value) - - def __bytes__(self): - return self.value - - def __str__(self): - return "[binary data (%d bytes)]" % len(self.value) - - def __eq__(self, other): - return self.value == other - - def __lt__(self, other): - return self.value < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFBoolAttribute(ASFBaseAttribute): - """Bool attribute. - - :: - - ASFBoolAttribute(True) - """ - - TYPE = 0x0002 - - def parse(self, data, dword=True): - if dword: - return struct.unpack("<I", data)[0] == 1 - else: - return struct.unpack("<H", data)[0] == 1 - - def _render(self, dword=True): - if dword: - return struct.pack("<I", bool(self.value)) - else: - return struct.pack("<H", bool(self.value)) - - def _validate(self, value): - return bool(value) - - def data_size(self): - return 4 - - def __bool__(self): - return bool(self.value) - - def __bytes__(self): - return text_type(self.value).encode('utf-8') - - def __str__(self): - return text_type(self.value) - - def __eq__(self, other): - return bool(self.value) == other - - def __lt__(self, other): - return bool(self.value) < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFDWordAttribute(ASFBaseAttribute): - """DWORD attribute. - - :: - - ASFDWordAttribute(42) - """ - - TYPE = 0x0003 - - def parse(self, data): - return struct.unpack("<L", data)[0] - - def _render(self): - return struct.pack("<L", self.value) - - def _validate(self, value): - value = int(value) - if not 0 <= value <= 2 ** 32 - 1: - raise ValueError("Out of range") - return value - - def data_size(self): - return 4 - - def __int__(self): - return self.value - - def __bytes__(self): - return text_type(self.value).encode('utf-8') - - def __str__(self): - return text_type(self.value) - - def __eq__(self, other): - return int(self.value) == other - - def __lt__(self, other): - return int(self.value) < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFQWordAttribute(ASFBaseAttribute): - """QWORD attribute. - - :: - - ASFQWordAttribute(42) - """ - - TYPE = 0x0004 - - def parse(self, data): - return struct.unpack("<Q", data)[0] - - def _render(self): - return struct.pack("<Q", self.value) - - def _validate(self, value): - value = int(value) - if not 0 <= value <= 2 ** 64 - 1: - raise ValueError("Out of range") - return value - - def data_size(self): - return 8 - - def __int__(self): - return self.value - - def __bytes__(self): - return text_type(self.value).encode('utf-8') - - def __str__(self): - return text_type(self.value) - - def __eq__(self, other): - return int(self.value) == other - - def __lt__(self, other): - return int(self.value) < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFWordAttribute(ASFBaseAttribute): - """WORD attribute. - - :: - - ASFWordAttribute(42) - """ - - TYPE = 0x0005 - - def parse(self, data): - return struct.unpack("<H", data)[0] - - def _render(self): - return struct.pack("<H", self.value) - - def _validate(self, value): - value = int(value) - if not 0 <= value <= 2 ** 16 - 1: - raise ValueError("Out of range") - return value - - def data_size(self): - return 2 - - def __int__(self): - return self.value - - def __bytes__(self): - return text_type(self.value).encode('utf-8') - - def __str__(self): - return text_type(self.value) - - def __eq__(self, other): - return int(self.value) == other - - def __lt__(self, other): - return int(self.value) < other - - __hash__ = ASFBaseAttribute.__hash__ - - -@ASFBaseAttribute._register -@swap_to_string -@total_ordering -class ASFGUIDAttribute(ASFBaseAttribute): - """GUID attribute.""" - - TYPE = 0x0006 - - def parse(self, data): - assert isinstance(data, bytes) - return data - - def _render(self): - assert isinstance(self.value, bytes) - return self.value - - def _validate(self, value): - if not isinstance(value, bytes): - raise TypeError("must be bytes/str: %r" % value) - return value - - def data_size(self): - return len(self.value) - - def __bytes__(self): - return self.value - - def __str__(self): - return repr(self.value) - - def __eq__(self, other): - return self.value == other - - def __lt__(self, other): - return self.value < other - - __hash__ = ASFBaseAttribute.__hash__ - - -def ASFValue(value, kind, **kwargs): - """Create a tag value of a specific kind. - - :: - - ASFValue(u"My Value", UNICODE) - - :rtype: ASFBaseAttribute - :raises TypeError: in case a wrong type was passed - :raises ValueError: in case the value can't be be represented as ASFValue. - """ - - try: - attr_type = ASFBaseAttribute._get_type(kind) - except KeyError: - raise ValueError("Unknown value type %r" % kind) - else: - return attr_type(value=value, **kwargs) diff --git a/resources/lib/libraries/mutagen/asf/_objects.py b/resources/lib/libraries/mutagen/asf/_objects.py deleted file mode 100644 index ed942679..00000000 --- a/resources/lib/libraries/mutagen/asf/_objects.py +++ /dev/null @@ -1,437 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2005-2006 Joe Wreschnig -# Copyright (C) 2006-2007 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -import struct - -from mutagen._util import cdata, get_size -from mutagen._compat import text_type, xrange, izip -from mutagen._tags import PaddingInfo - -from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError -from ._attrs import ASFBaseAttribute, ASFUnicodeAttribute - - -class BaseObject(object): - """Base ASF object.""" - - GUID = None - _TYPES = {} - - def __init__(self): - self.objects = [] - self.data = b"" - - def parse(self, asf, data): - self.data = data - - def render(self, asf): - data = self.GUID + struct.pack("<Q", len(self.data) + 24) + self.data - return data - - def get_child(self, guid): - for obj in self.objects: - if obj.GUID == guid: - return obj - return None - - @classmethod - def _register(cls, other): - cls._TYPES[other.GUID] = other - return other - - @classmethod - def _get_object(cls, guid): - if guid in cls._TYPES: - return cls._TYPES[guid]() - else: - return UnknownObject(guid) - - def __repr__(self): - return "<%s GUID=%s objects=%r>" % ( - type(self).__name__, bytes2guid(self.GUID), self.objects) - - def pprint(self): - l = [] - l.append("%s(%s)" % (type(self).__name__, bytes2guid(self.GUID))) - for o in self.objects: - for e in o.pprint().splitlines(): - l.append(" " + e) - return "\n".join(l) - - -class UnknownObject(BaseObject): - """Unknown ASF object.""" - - def __init__(self, guid): - super(UnknownObject, self).__init__() - assert isinstance(guid, bytes) - self.GUID = guid - - -@BaseObject._register -class HeaderObject(BaseObject): - """ASF header.""" - - GUID = guid2bytes("75B22630-668E-11CF-A6D9-00AA0062CE6C") - - @classmethod - def parse_full(cls, asf, fileobj): - """Raises ASFHeaderError""" - - header = cls() - - size, num_objects = cls.parse_size(fileobj) - for i in xrange(num_objects): - guid, size = struct.unpack("<16sQ", fileobj.read(24)) - obj = BaseObject._get_object(guid) - data = fileobj.read(size - 24) - obj.parse(asf, data) - header.objects.append(obj) - - return header - - @classmethod - def parse_size(cls, fileobj): - """Returns (size, num_objects) - - Raises ASFHeaderError - """ - - header = fileobj.read(30) - if len(header) != 30 or header[:16] != HeaderObject.GUID: - raise ASFHeaderError("Not an ASF file.") - - return struct.unpack("<QL", header[16:28]) - - def render_full(self, asf, fileobj, available, padding_func): - # Render everything except padding - num_objects = 0 - data = bytearray() - for obj in self.objects: - if obj.GUID == PaddingObject.GUID: - continue - data += obj.render(asf) - num_objects += 1 - - # calculate how much space we need at least - padding_obj = PaddingObject() - header_size = len(HeaderObject.GUID) + 14 - padding_overhead = len(padding_obj.render(asf)) - needed_size = len(data) + header_size + padding_overhead - - # ask the user for padding adjustments - file_size = get_size(fileobj) - content_size = file_size - available - assert content_size >= 0 - info = PaddingInfo(available - needed_size, content_size) - - # add padding - padding = info._get_padding(padding_func) - padding_obj.parse(asf, b"\x00" * padding) - data += padding_obj.render(asf) - num_objects += 1 - - data = (HeaderObject.GUID + - struct.pack("<QL", len(data) + 30, num_objects) + - b"\x01\x02" + data) - - return data - - def parse(self, asf, data): - raise NotImplementedError - - def render(self, asf): - raise NotImplementedError - - -@BaseObject._register -class ContentDescriptionObject(BaseObject): - """Content description.""" - - GUID = guid2bytes("75B22633-668E-11CF-A6D9-00AA0062CE6C") - - NAMES = [ - u"Title", - u"Author", - u"Copyright", - u"Description", - u"Rating", - ] - - def parse(self, asf, data): - super(ContentDescriptionObject, self).parse(asf, data) - lengths = struct.unpack("<HHHHH", data[:10]) - texts = [] - pos = 10 - for length in lengths: - end = pos + length - if length > 0: - texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00")) - else: - texts.append(None) - pos = end - - for key, value in izip(self.NAMES, texts): - if value is not None: - value = ASFUnicodeAttribute(value=value) - asf._tags.setdefault(self.GUID, []).append((key, value)) - - def render(self, asf): - def render_text(name): - value = asf.to_content_description.get(name) - if value is not None: - return text_type(value).encode("utf-16-le") + b"\x00\x00" - else: - return b"" - - texts = [render_text(x) for x in self.NAMES] - data = struct.pack("<HHHHH", *map(len, texts)) + b"".join(texts) - return self.GUID + struct.pack("<Q", 24 + len(data)) + data - - -@BaseObject._register -class ExtendedContentDescriptionObject(BaseObject): - """Extended content description.""" - - GUID = guid2bytes("D2D0A440-E307-11D2-97F0-00A0C95EA850") - - def parse(self, asf, data): - super(ExtendedContentDescriptionObject, self).parse(asf, data) - num_attributes, = struct.unpack("<H", data[0:2]) - pos = 2 - for i in xrange(num_attributes): - name_length, = struct.unpack("<H", data[pos:pos + 2]) - pos += 2 - name = data[pos:pos + name_length] - name = name.decode("utf-16-le").strip("\x00") - pos += name_length - value_type, value_length = struct.unpack("<HH", data[pos:pos + 4]) - pos += 4 - value = data[pos:pos + value_length] - pos += value_length - attr = ASFBaseAttribute._get_type(value_type)(data=value) - asf._tags.setdefault(self.GUID, []).append((name, attr)) - - def render(self, asf): - attrs = asf.to_extended_content_description.items() - data = b"".join(attr.render(name) for (name, attr) in attrs) - data = struct.pack("<QH", 26 + len(data), len(attrs)) + data - return self.GUID + data - - -@BaseObject._register -class FilePropertiesObject(BaseObject): - """File properties.""" - - GUID = guid2bytes("8CABDCA1-A947-11CF-8EE4-00C00C205365") - - def parse(self, asf, data): - super(FilePropertiesObject, self).parse(asf, data) - length, _, preroll = struct.unpack("<QQQ", data[40:64]) - # there are files where preroll is larger than length, limit to >= 0 - asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0) - - -@BaseObject._register -class StreamPropertiesObject(BaseObject): - """Stream properties.""" - - GUID = guid2bytes("B7DC0791-A9B7-11CF-8EE6-00C00C205365") - - def parse(self, asf, data): - super(StreamPropertiesObject, self).parse(asf, data) - channels, sample_rate, bitrate = struct.unpack("<HII", data[56:66]) - asf.info.channels = channels - asf.info.sample_rate = sample_rate - asf.info.bitrate = bitrate * 8 - - -@BaseObject._register -class CodecListObject(BaseObject): - """Codec List""" - - GUID = guid2bytes("86D15240-311D-11D0-A3A4-00A0C90348F6") - - def _parse_entry(self, data, offset): - """can raise cdata.error""" - - type_, offset = cdata.uint16_le_from(data, offset) - - units, offset = cdata.uint16_le_from(data, offset) - # utf-16 code units, not characters.. - next_offset = offset + units * 2 - try: - name = data[offset:next_offset].decode("utf-16-le").strip("\x00") - except UnicodeDecodeError: - name = u"" - offset = next_offset - - units, offset = cdata.uint16_le_from(data, offset) - next_offset = offset + units * 2 - try: - desc = data[offset:next_offset].decode("utf-16-le").strip("\x00") - except UnicodeDecodeError: - desc = u"" - offset = next_offset - - bytes_, offset = cdata.uint16_le_from(data, offset) - next_offset = offset + bytes_ - codec = u"" - if bytes_ == 2: - codec_id = cdata.uint16_le_from(data, offset)[0] - if codec_id in CODECS: - codec = CODECS[codec_id] - offset = next_offset - - return offset, type_, name, desc, codec - - def parse(self, asf, data): - super(CodecListObject, self).parse(asf, data) - - offset = 16 - count, offset = cdata.uint32_le_from(data, offset) - for i in xrange(count): - try: - offset, type_, name, desc, codec = \ - self._parse_entry(data, offset) - except cdata.error: - raise ASFError("invalid codec entry") - - # go with the first audio entry - if type_ == 2: - name = name.strip() - desc = desc.strip() - asf.info.codec_type = codec - asf.info.codec_name = name - asf.info.codec_description = desc - return - - -@BaseObject._register -class PaddingObject(BaseObject): - """Padding object""" - - GUID = guid2bytes("1806D474-CADF-4509-A4BA-9AABCB96AAE8") - - -@BaseObject._register -class StreamBitratePropertiesObject(BaseObject): - """Stream bitrate properties""" - - GUID = guid2bytes("7BF875CE-468D-11D1-8D82-006097C9A2B2") - - -@BaseObject._register -class ContentEncryptionObject(BaseObject): - """Content encryption""" - - GUID = guid2bytes("2211B3FB-BD23-11D2-B4B7-00A0C955FC6E") - - -@BaseObject._register -class ExtendedContentEncryptionObject(BaseObject): - """Extended content encryption""" - - GUID = guid2bytes("298AE614-2622-4C17-B935-DAE07EE9289C") - - -@BaseObject._register -class HeaderExtensionObject(BaseObject): - """Header extension.""" - - GUID = guid2bytes("5FBF03B5-A92E-11CF-8EE3-00C00C205365") - - def parse(self, asf, data): - super(HeaderExtensionObject, self).parse(asf, data) - datasize, = struct.unpack("<I", data[18:22]) - datapos = 0 - while datapos < datasize: - guid, size = struct.unpack( - "<16sQ", data[22 + datapos:22 + datapos + 24]) - obj = BaseObject._get_object(guid) - obj.parse(asf, data[22 + datapos + 24:22 + datapos + size]) - self.objects.append(obj) - datapos += size - - def render(self, asf): - data = bytearray() - for obj in self.objects: - # some files have the padding in the extension header, but we - # want to add it at the end of the top level header. Just - # skip padding at this level. - if obj.GUID == PaddingObject.GUID: - continue - data += obj.render(asf) - return (self.GUID + struct.pack("<Q", 24 + 16 + 6 + len(data)) + - b"\x11\xD2\xD3\xAB\xBA\xA9\xcf\x11" + - b"\x8E\xE6\x00\xC0\x0C\x20\x53\x65" + - b"\x06\x00" + struct.pack("<I", len(data)) + data) - - -@BaseObject._register -class MetadataObject(BaseObject): - """Metadata description.""" - - GUID = guid2bytes("C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA") - - def parse(self, asf, data): - super(MetadataObject, self).parse(asf, data) - num_attributes, = struct.unpack("<H", data[0:2]) - pos = 2 - for i in xrange(num_attributes): - (reserved, stream, name_length, value_type, - value_length) = struct.unpack("<HHHHI", data[pos:pos + 12]) - pos += 12 - name = data[pos:pos + name_length] - name = name.decode("utf-16-le").strip("\x00") - pos += name_length - value = data[pos:pos + value_length] - pos += value_length - args = {'data': value, 'stream': stream} - if value_type == 2: - args['dword'] = False - attr = ASFBaseAttribute._get_type(value_type)(**args) - asf._tags.setdefault(self.GUID, []).append((name, attr)) - - def render(self, asf): - attrs = asf.to_metadata.items() - data = b"".join([attr.render_m(name) for (name, attr) in attrs]) - return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) + - data) - - -@BaseObject._register -class MetadataLibraryObject(BaseObject): - """Metadata library description.""" - - GUID = guid2bytes("44231C94-9498-49D1-A141-1D134E457054") - - def parse(self, asf, data): - super(MetadataLibraryObject, self).parse(asf, data) - num_attributes, = struct.unpack("<H", data[0:2]) - pos = 2 - for i in xrange(num_attributes): - (language, stream, name_length, value_type, - value_length) = struct.unpack("<HHHHI", data[pos:pos + 12]) - pos += 12 - name = data[pos:pos + name_length] - name = name.decode("utf-16-le").strip("\x00") - pos += name_length - value = data[pos:pos + value_length] - pos += value_length - args = {'data': value, 'language': language, 'stream': stream} - if value_type == 2: - args['dword'] = False - attr = ASFBaseAttribute._get_type(value_type)(**args) - asf._tags.setdefault(self.GUID, []).append((name, attr)) - - def render(self, asf): - attrs = asf.to_metadata_library - data = b"".join([attr.render_ml(name) for (name, attr) in attrs]) - return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) + - data) diff --git a/resources/lib/libraries/mutagen/asf/_util.py b/resources/lib/libraries/mutagen/asf/_util.py deleted file mode 100644 index 42154bff..00000000 --- a/resources/lib/libraries/mutagen/asf/_util.py +++ /dev/null @@ -1,315 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2005-2006 Joe Wreschnig -# Copyright (C) 2006-2007 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -import struct - -from mutagen._util import MutagenError - - -class error(IOError, MutagenError): - """Error raised by :mod:`mutagen.asf`""" - - -class ASFError(error): - pass - - -class ASFHeaderError(error): - pass - - -def guid2bytes(s): - """Converts a GUID to the serialized bytes representation""" - - assert isinstance(s, str) - assert len(s) == 36 - - p = struct.pack - return b"".join([ - p("<IHH", int(s[:8], 16), int(s[9:13], 16), int(s[14:18], 16)), - p(">H", int(s[19:23], 16)), - p(">Q", int(s[24:], 16))[2:], - ]) - - -def bytes2guid(s): - """Converts a serialized GUID to a text GUID""" - - assert isinstance(s, bytes) - - u = struct.unpack - v = [] - v.extend(u("<IHH", s[:8])) - v.extend(u(">HQ", s[8:10] + b"\x00\x00" + s[10:])) - return "%08X-%04X-%04X-%04X-%012X" % tuple(v) - - -# Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4} -CODECS = { - 0x0000: u"Unknown Wave Format", - 0x0001: u"Microsoft PCM Format", - 0x0002: u"Microsoft ADPCM Format", - 0x0003: u"IEEE Float", - 0x0004: u"Compaq Computer VSELP", - 0x0005: u"IBM CVSD", - 0x0006: u"Microsoft CCITT A-Law", - 0x0007: u"Microsoft CCITT u-Law", - 0x0008: u"Microsoft DTS", - 0x0009: u"Microsoft DRM", - 0x000A: u"Windows Media Audio 9 Voice", - 0x000B: u"Windows Media Audio 10 Voice", - 0x000C: u"OGG Vorbis", - 0x000D: u"FLAC", - 0x000E: u"MOT AMR", - 0x000F: u"Nice Systems IMBE", - 0x0010: u"OKI ADPCM", - 0x0011: u"Intel IMA ADPCM", - 0x0012: u"Videologic MediaSpace ADPCM", - 0x0013: u"Sierra Semiconductor ADPCM", - 0x0014: u"Antex Electronics G.723 ADPCM", - 0x0015: u"DSP Solutions DIGISTD", - 0x0016: u"DSP Solutions DIGIFIX", - 0x0017: u"Dialogic OKI ADPCM", - 0x0018: u"MediaVision ADPCM", - 0x0019: u"Hewlett-Packard CU codec", - 0x001A: u"Hewlett-Packard Dynamic Voice", - 0x0020: u"Yamaha ADPCM", - 0x0021: u"Speech Compression SONARC", - 0x0022: u"DSP Group True Speech", - 0x0023: u"Echo Speech EchoSC1", - 0x0024: u"Ahead Inc. Audiofile AF36", - 0x0025: u"Audio Processing Technology APTX", - 0x0026: u"Ahead Inc. AudioFile AF10", - 0x0027: u"Aculab Prosody 1612", - 0x0028: u"Merging Technologies S.A. LRC", - 0x0030: u"Dolby Labs AC2", - 0x0031: u"Microsoft GSM 6.10", - 0x0032: u"Microsoft MSNAudio", - 0x0033: u"Antex Electronics ADPCME", - 0x0034: u"Control Resources VQLPC", - 0x0035: u"DSP Solutions Digireal", - 0x0036: u"DSP Solutions DigiADPCM", - 0x0037: u"Control Resources CR10", - 0x0038: u"Natural MicroSystems VBXADPCM", - 0x0039: u"Crystal Semiconductor IMA ADPCM", - 0x003A: u"Echo Speech EchoSC3", - 0x003B: u"Rockwell ADPCM", - 0x003C: u"Rockwell DigiTalk", - 0x003D: u"Xebec Multimedia Solutions", - 0x0040: u"Antex Electronics G.721 ADPCM", - 0x0041: u"Antex Electronics G.728 CELP", - 0x0042: u"Intel G.723", - 0x0043: u"Intel G.723.1", - 0x0044: u"Intel G.729 Audio", - 0x0045: u"Sharp G.726 Audio", - 0x0050: u"Microsoft MPEG-1", - 0x0052: u"InSoft RT24", - 0x0053: u"InSoft PAC", - 0x0055: u"MP3 - MPEG Layer III", - 0x0059: u"Lucent G.723", - 0x0060: u"Cirrus Logic", - 0x0061: u"ESS Technology ESPCM", - 0x0062: u"Voxware File-Mode", - 0x0063: u"Canopus Atrac", - 0x0064: u"APICOM G.726 ADPCM", - 0x0065: u"APICOM G.722 ADPCM", - 0x0066: u"Microsoft DSAT", - 0x0067: u"Microsoft DSAT Display", - 0x0069: u"Voxware Byte Aligned", - 0x0070: u"Voxware AC8", - 0x0071: u"Voxware AC10", - 0x0072: u"Voxware AC16", - 0x0073: u"Voxware AC20", - 0x0074: u"Voxware RT24 MetaVoice", - 0x0075: u"Voxware RT29 MetaSound", - 0x0076: u"Voxware RT29HW", - 0x0077: u"Voxware VR12", - 0x0078: u"Voxware VR18", - 0x0079: u"Voxware TQ40", - 0x007A: u"Voxware SC3", - 0x007B: u"Voxware SC3", - 0x0080: u"Softsound", - 0x0081: u"Voxware TQ60", - 0x0082: u"Microsoft MSRT24", - 0x0083: u"AT&T Labs G.729A", - 0x0084: u"Motion Pixels MVI MV12", - 0x0085: u"DataFusion Systems G.726", - 0x0086: u"DataFusion Systems GSM610", - 0x0088: u"Iterated Systems ISIAudio", - 0x0089: u"Onlive", - 0x008A: u"Multitude FT SX20", - 0x008B: u"Infocom ITS ACM G.721", - 0x008C: u"Convedia G.729", - 0x008D: u"Congruency Audio", - 0x0091: u"Siemens Business Communications SBC24", - 0x0092: u"Sonic Foundry Dolby AC3 SPDIF", - 0x0093: u"MediaSonic G.723", - 0x0094: u"Aculab Prosody 8KBPS", - 0x0097: u"ZyXEL ADPCM", - 0x0098: u"Philips LPCBB", - 0x0099: u"Studer Professional Audio AG Packed", - 0x00A0: u"Malden Electronics PHONYTALK", - 0x00A1: u"Racal Recorder GSM", - 0x00A2: u"Racal Recorder G720.a", - 0x00A3: u"Racal Recorder G723.1", - 0x00A4: u"Racal Recorder Tetra ACELP", - 0x00B0: u"NEC AAC", - 0x00FF: u"CoreAAC Audio", - 0x0100: u"Rhetorex ADPCM", - 0x0101: u"BeCubed Software IRAT", - 0x0111: u"Vivo G.723", - 0x0112: u"Vivo Siren", - 0x0120: u"Philips CELP", - 0x0121: u"Philips Grundig", - 0x0123: u"Digital G.723", - 0x0125: u"Sanyo ADPCM", - 0x0130: u"Sipro Lab Telecom ACELP.net", - 0x0131: u"Sipro Lab Telecom ACELP.4800", - 0x0132: u"Sipro Lab Telecom ACELP.8V3", - 0x0133: u"Sipro Lab Telecom ACELP.G.729", - 0x0134: u"Sipro Lab Telecom ACELP.G.729A", - 0x0135: u"Sipro Lab Telecom ACELP.KELVIN", - 0x0136: u"VoiceAge AMR", - 0x0140: u"Dictaphone G.726 ADPCM", - 0x0141: u"Dictaphone CELP68", - 0x0142: u"Dictaphone CELP54", - 0x0150: u"Qualcomm PUREVOICE", - 0x0151: u"Qualcomm HALFRATE", - 0x0155: u"Ring Zero Systems TUBGSM", - 0x0160: u"Windows Media Audio Standard", - 0x0161: u"Windows Media Audio 9 Standard", - 0x0162: u"Windows Media Audio 9 Professional", - 0x0163: u"Windows Media Audio 9 Lossless", - 0x0164: u"Windows Media Audio Pro over SPDIF", - 0x0170: u"Unisys NAP ADPCM", - 0x0171: u"Unisys NAP ULAW", - 0x0172: u"Unisys NAP ALAW", - 0x0173: u"Unisys NAP 16K", - 0x0174: u"Sycom ACM SYC008", - 0x0175: u"Sycom ACM SYC701 G725", - 0x0176: u"Sycom ACM SYC701 CELP54", - 0x0177: u"Sycom ACM SYC701 CELP68", - 0x0178: u"Knowledge Adventure ADPCM", - 0x0180: u"Fraunhofer IIS MPEG-2 AAC", - 0x0190: u"Digital Theater Systems DTS", - 0x0200: u"Creative Labs ADPCM", - 0x0202: u"Creative Labs FastSpeech8", - 0x0203: u"Creative Labs FastSpeech10", - 0x0210: u"UHER informatic GmbH ADPCM", - 0x0215: u"Ulead DV Audio", - 0x0216: u"Ulead DV Audio", - 0x0220: u"Quarterdeck", - 0x0230: u"I-link Worldwide ILINK VC", - 0x0240: u"Aureal Semiconductor RAW SPORT", - 0x0249: u"Generic Passthru", - 0x0250: u"Interactive Products HSX", - 0x0251: u"Interactive Products RPELP", - 0x0260: u"Consistent Software CS2", - 0x0270: u"Sony SCX", - 0x0271: u"Sony SCY", - 0x0272: u"Sony ATRAC3", - 0x0273: u"Sony SPC", - 0x0280: u"Telum Audio", - 0x0281: u"Telum IA Audio", - 0x0285: u"Norcom Voice Systems ADPCM", - 0x0300: u"Fujitsu TOWNS SND", - 0x0350: u"Micronas SC4 Speech", - 0x0351: u"Micronas CELP833", - 0x0400: u"Brooktree BTV Digital", - 0x0401: u"Intel Music Coder", - 0x0402: u"Intel Audio", - 0x0450: u"QDesign Music", - 0x0500: u"On2 AVC0 Audio", - 0x0501: u"On2 AVC1 Audio", - 0x0680: u"AT&T Labs VME VMPCM", - 0x0681: u"AT&T Labs TPC", - 0x08AE: u"ClearJump Lightwave Lossless", - 0x1000: u"Olivetti GSM", - 0x1001: u"Olivetti ADPCM", - 0x1002: u"Olivetti CELP", - 0x1003: u"Olivetti SBC", - 0x1004: u"Olivetti OPR", - 0x1100: u"Lernout & Hauspie", - 0x1101: u"Lernout & Hauspie CELP", - 0x1102: u"Lernout & Hauspie SBC8", - 0x1103: u"Lernout & Hauspie SBC12", - 0x1104: u"Lernout & Hauspie SBC16", - 0x1400: u"Norris Communication", - 0x1401: u"ISIAudio", - 0x1500: u"AT&T Labs Soundspace Music Compression", - 0x1600: u"Microsoft MPEG ADTS AAC", - 0x1601: u"Microsoft MPEG RAW AAC", - 0x1608: u"Nokia MPEG ADTS AAC", - 0x1609: u"Nokia MPEG RAW AAC", - 0x181C: u"VoxWare MetaVoice RT24", - 0x1971: u"Sonic Foundry Lossless", - 0x1979: u"Innings Telecom ADPCM", - 0x1FC4: u"NTCSoft ALF2CD ACM", - 0x2000: u"Dolby AC3", - 0x2001: u"DTS", - 0x4143: u"Divio AAC", - 0x4201: u"Nokia Adaptive Multi-Rate", - 0x4243: u"Divio G.726", - 0x4261: u"ITU-T H.261", - 0x4263: u"ITU-T H.263", - 0x4264: u"ITU-T H.264", - 0x674F: u"Ogg Vorbis Mode 1", - 0x6750: u"Ogg Vorbis Mode 2", - 0x6751: u"Ogg Vorbis Mode 3", - 0x676F: u"Ogg Vorbis Mode 1+", - 0x6770: u"Ogg Vorbis Mode 2+", - 0x6771: u"Ogg Vorbis Mode 3+", - 0x7000: u"3COM NBX Audio", - 0x706D: u"FAAD AAC Audio", - 0x77A1: u"True Audio Lossless Audio", - 0x7A21: u"GSM-AMR CBR 3GPP Audio", - 0x7A22: u"GSM-AMR VBR 3GPP Audio", - 0xA100: u"Comverse Infosys G723.1", - 0xA101: u"Comverse Infosys AVQSBC", - 0xA102: u"Comverse Infosys SBC", - 0xA103: u"Symbol Technologies G729a", - 0xA104: u"VoiceAge AMR WB", - 0xA105: u"Ingenient Technologies G.726", - 0xA106: u"ISO/MPEG-4 Advanced Audio Coding (AAC)", - 0xA107: u"Encore Software Ltd's G.726", - 0xA108: u"ZOLL Medical Corporation ASAO", - 0xA109: u"Speex Voice", - 0xA10A: u"Vianix MASC Speech Compression", - 0xA10B: u"Windows Media 9 Spectrum Analyzer Output", - 0xA10C: u"Media Foundation Spectrum Analyzer Output", - 0xA10D: u"GSM 6.10 (Full-Rate) Speech", - 0xA10E: u"GSM 6.20 (Half-Rate) Speech", - 0xA10F: u"GSM 6.60 (Enchanced Full-Rate) Speech", - 0xA110: u"GSM 6.90 (Adaptive Multi-Rate) Speech", - 0xA111: u"GSM Adaptive Multi-Rate WideBand Speech", - 0xA112: u"Polycom G.722", - 0xA113: u"Polycom G.728", - 0xA114: u"Polycom G.729a", - 0xA115: u"Polycom Siren", - 0xA116: u"Global IP Sound ILBC", - 0xA117: u"Radio Time Time Shifted Radio", - 0xA118: u"Nice Systems ACA", - 0xA119: u"Nice Systems ADPCM", - 0xA11A: u"Vocord Group ITU-T G.721", - 0xA11B: u"Vocord Group ITU-T G.726", - 0xA11C: u"Vocord Group ITU-T G.722.1", - 0xA11D: u"Vocord Group ITU-T G.728", - 0xA11E: u"Vocord Group ITU-T G.729", - 0xA11F: u"Vocord Group ITU-T G.729a", - 0xA120: u"Vocord Group ITU-T G.723.1", - 0xA121: u"Vocord Group LBC", - 0xA122: u"Nice G.728", - 0xA123: u"France Telecom G.729 ACM Audio", - 0xA124: u"CODIAN Audio", - 0xCC12: u"Intel YUV12 Codec", - 0xCFCC: u"Digital Processing Systems Perception Motion JPEG", - 0xD261: u"DEC H.261", - 0xD263: u"DEC H.263", - 0xFFFE: u"Extensible Wave Format", - 0xFFFF: u"Unregistered", -} diff --git a/resources/lib/libraries/mutagen/easyid3.py b/resources/lib/libraries/mutagen/easyid3.py deleted file mode 100644 index f8dd2de0..00000000 --- a/resources/lib/libraries/mutagen/easyid3.py +++ /dev/null @@ -1,534 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""Easier access to ID3 tags. - -EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear -more like Vorbis or APEv2 tags. -""" - -import mutagen.id3 - -from ._compat import iteritems, text_type, PY2 -from mutagen import Metadata -from mutagen._util import DictMixin, dict_match -from mutagen.id3 import ID3, error, delete, ID3FileType - - -__all__ = ['EasyID3', 'Open', 'delete'] - - -class EasyID3KeyError(KeyError, ValueError, error): - """Raised when trying to get/set an invalid key. - - Subclasses both KeyError and ValueError for API compatibility, - catching KeyError is preferred. - """ - - -class EasyID3(DictMixin, Metadata): - """A file with an ID3 tag. - - Like Vorbis comments, EasyID3 keys are case-insensitive ASCII - strings. Only a subset of ID3 frames are supported by default. Use - EasyID3.RegisterKey and its wrappers to support more. - - You can also set the GetFallback, SetFallback, and DeleteFallback - to generic key getter/setter/deleter functions, which are called - if no specific handler is registered for a key. Additionally, - ListFallback can be used to supply an arbitrary list of extra - keys. These can be set on EasyID3 or on individual instances after - creation. - - To use an EasyID3 class with mutagen.mp3.MP3:: - - from mutagen.mp3 import EasyMP3 as MP3 - MP3(filename) - - Because many of the attributes are constructed on the fly, things - like the following will not work:: - - ezid3["performer"].append("Joe") - - Instead, you must do:: - - values = ezid3["performer"] - values.append("Joe") - ezid3["performer"] = values - - """ - - Set = {} - Get = {} - Delete = {} - List = {} - - # For compatibility. - valid_keys = Get - - GetFallback = None - SetFallback = None - DeleteFallback = None - ListFallback = None - - @classmethod - def RegisterKey(cls, key, - getter=None, setter=None, deleter=None, lister=None): - """Register a new key mapping. - - A key mapping is four functions, a getter, setter, deleter, - and lister. The key may be either a string or a glob pattern. - - The getter, deleted, and lister receive an ID3 instance and - the requested key name. The setter also receives the desired - value, which will be a list of strings. - - The getter, setter, and deleter are used to implement __getitem__, - __setitem__, and __delitem__. - - The lister is used to implement keys(). It should return a - list of keys that are actually in the ID3 instance, provided - by its associated getter. - """ - key = key.lower() - if getter is not None: - cls.Get[key] = getter - if setter is not None: - cls.Set[key] = setter - if deleter is not None: - cls.Delete[key] = deleter - if lister is not None: - cls.List[key] = lister - - @classmethod - def RegisterTextKey(cls, key, frameid): - """Register a text key. - - If the key you need to register is a simple one-to-one mapping - of ID3 frame name to EasyID3 key, then you can use this - function:: - - EasyID3.RegisterTextKey("title", "TIT2") - """ - def getter(id3, key): - return list(id3[frameid]) - - def setter(id3, key, value): - try: - frame = id3[frameid] - except KeyError: - id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value)) - else: - frame.encoding = 3 - frame.text = value - - def deleter(id3, key): - del(id3[frameid]) - - cls.RegisterKey(key, getter, setter, deleter) - - @classmethod - def RegisterTXXXKey(cls, key, desc): - """Register a user-defined text frame key. - - Some ID3 tags are stored in TXXX frames, which allow a - freeform 'description' which acts as a subkey, - e.g. TXXX:BARCODE.:: - - EasyID3.RegisterTXXXKey('barcode', 'BARCODE'). - """ - frameid = "TXXX:" + desc - - def getter(id3, key): - return list(id3[frameid]) - - def setter(id3, key, value): - try: - frame = id3[frameid] - except KeyError: - enc = 0 - # Store 8859-1 if we can, per MusicBrainz spec. - for v in value: - if v and max(v) > u'\x7f': - enc = 3 - break - - id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) - else: - frame.text = value - - def deleter(id3, key): - del(id3[frameid]) - - cls.RegisterKey(key, getter, setter, deleter) - - def __init__(self, filename=None): - self.__id3 = ID3() - if filename is not None: - self.load(filename) - - load = property(lambda s: s.__id3.load, - lambda s, v: setattr(s.__id3, 'load', v)) - - def save(self, *args, **kwargs): - # ignore v2_version until we support 2.3 here - kwargs.pop("v2_version", None) - self.__id3.save(*args, **kwargs) - - delete = property(lambda s: s.__id3.delete, - lambda s, v: setattr(s.__id3, 'delete', v)) - - filename = property(lambda s: s.__id3.filename, - lambda s, fn: setattr(s.__id3, 'filename', fn)) - - size = property(lambda s: s.__id3.size, - lambda s, fn: setattr(s.__id3, 'size', s)) - - def __getitem__(self, key): - key = key.lower() - func = dict_match(self.Get, key, self.GetFallback) - if func is not None: - return func(self.__id3, key) - else: - raise EasyID3KeyError("%r is not a valid key" % key) - - def __setitem__(self, key, value): - key = key.lower() - if PY2: - if isinstance(value, basestring): - value = [value] - else: - if isinstance(value, text_type): - value = [value] - func = dict_match(self.Set, key, self.SetFallback) - if func is not None: - return func(self.__id3, key, value) - else: - raise EasyID3KeyError("%r is not a valid key" % key) - - def __delitem__(self, key): - key = key.lower() - func = dict_match(self.Delete, key, self.DeleteFallback) - if func is not None: - return func(self.__id3, key) - else: - raise EasyID3KeyError("%r is not a valid key" % key) - - def keys(self): - keys = [] - for key in self.Get.keys(): - if key in self.List: - keys.extend(self.List[key](self.__id3, key)) - elif key in self: - keys.append(key) - if self.ListFallback is not None: - keys.extend(self.ListFallback(self.__id3, "")) - return keys - - def pprint(self): - """Print tag key=value pairs.""" - strings = [] - for key in sorted(self.keys()): - values = self[key] - for value in values: - strings.append("%s=%s" % (key, value)) - return "\n".join(strings) - - -Open = EasyID3 - - -def genre_get(id3, key): - return id3["TCON"].genres - - -def genre_set(id3, key, value): - try: - frame = id3["TCON"] - except KeyError: - id3.add(mutagen.id3.TCON(encoding=3, text=value)) - else: - frame.encoding = 3 - frame.genres = value - - -def genre_delete(id3, key): - del(id3["TCON"]) - - -def date_get(id3, key): - return [stamp.text for stamp in id3["TDRC"].text] - - -def date_set(id3, key, value): - id3.add(mutagen.id3.TDRC(encoding=3, text=value)) - - -def date_delete(id3, key): - del(id3["TDRC"]) - - -def original_date_get(id3, key): - return [stamp.text for stamp in id3["TDOR"].text] - - -def original_date_set(id3, key, value): - id3.add(mutagen.id3.TDOR(encoding=3, text=value)) - - -def original_date_delete(id3, key): - del(id3["TDOR"]) - - -def performer_get(id3, key): - people = [] - wanted_role = key.split(":", 1)[1] - try: - mcl = id3["TMCL"] - except KeyError: - raise KeyError(key) - for role, person in mcl.people: - if role == wanted_role: - people.append(person) - if people: - return people - else: - raise KeyError(key) - - -def performer_set(id3, key, value): - wanted_role = key.split(":", 1)[1] - try: - mcl = id3["TMCL"] - except KeyError: - mcl = mutagen.id3.TMCL(encoding=3, people=[]) - id3.add(mcl) - mcl.encoding = 3 - people = [p for p in mcl.people if p[0] != wanted_role] - for v in value: - people.append((wanted_role, v)) - mcl.people = people - - -def performer_delete(id3, key): - wanted_role = key.split(":", 1)[1] - try: - mcl = id3["TMCL"] - except KeyError: - raise KeyError(key) - people = [p for p in mcl.people if p[0] != wanted_role] - if people == mcl.people: - raise KeyError(key) - elif people: - mcl.people = people - else: - del(id3["TMCL"]) - - -def performer_list(id3, key): - try: - mcl = id3["TMCL"] - except KeyError: - return [] - else: - return list(set("performer:" + p[0] for p in mcl.people)) - - -def musicbrainz_trackid_get(id3, key): - return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')] - - -def musicbrainz_trackid_set(id3, key, value): - if len(value) != 1: - raise ValueError("only one track ID may be set per song") - value = value[0].encode('ascii') - try: - frame = id3["UFID:http://musicbrainz.org"] - except KeyError: - frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value) - id3.add(frame) - else: - frame.data = value - - -def musicbrainz_trackid_delete(id3, key): - del(id3["UFID:http://musicbrainz.org"]) - - -def website_get(id3, key): - urls = [frame.url for frame in id3.getall("WOAR")] - if urls: - return urls - else: - raise EasyID3KeyError(key) - - -def website_set(id3, key, value): - id3.delall("WOAR") - for v in value: - id3.add(mutagen.id3.WOAR(url=v)) - - -def website_delete(id3, key): - id3.delall("WOAR") - - -def gain_get(id3, key): - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - raise EasyID3KeyError(key) - else: - return [u"%+f dB" % frame.gain] - - -def gain_set(id3, key, value): - if len(value) != 1: - raise ValueError( - "there must be exactly one gain value, not %r.", value) - gain = float(value[0].split()[0]) - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) - id3.add(frame) - frame.gain = gain - - -def gain_delete(id3, key): - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - pass - else: - if frame.peak: - frame.gain = 0.0 - else: - del(id3["RVA2:" + key[11:-5]]) - - -def peak_get(id3, key): - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - raise EasyID3KeyError(key) - else: - return [u"%f" % frame.peak] - - -def peak_set(id3, key, value): - if len(value) != 1: - raise ValueError( - "there must be exactly one peak value, not %r.", value) - peak = float(value[0]) - if peak >= 2 or peak < 0: - raise ValueError("peak must be => 0 and < 2.") - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) - id3.add(frame) - frame.peak = peak - - -def peak_delete(id3, key): - try: - frame = id3["RVA2:" + key[11:-5]] - except KeyError: - pass - else: - if frame.gain: - frame.peak = 0.0 - else: - del(id3["RVA2:" + key[11:-5]]) - - -def peakgain_list(id3, key): - keys = [] - for frame in id3.getall("RVA2"): - keys.append("replaygain_%s_gain" % frame.desc) - keys.append("replaygain_%s_peak" % frame.desc) - return keys - -for frameid, key in iteritems({ - "TALB": "album", - "TBPM": "bpm", - "TCMP": "compilation", # iTunes extension - "TCOM": "composer", - "TCOP": "copyright", - "TENC": "encodedby", - "TEXT": "lyricist", - "TLEN": "length", - "TMED": "media", - "TMOO": "mood", - "TIT2": "title", - "TIT3": "version", - "TPE1": "artist", - "TPE2": "performer", - "TPE3": "conductor", - "TPE4": "arranger", - "TPOS": "discnumber", - "TPUB": "organization", - "TRCK": "tracknumber", - "TOLY": "author", - "TSO2": "albumartistsort", # iTunes extension - "TSOA": "albumsort", - "TSOC": "composersort", # iTunes extension - "TSOP": "artistsort", - "TSOT": "titlesort", - "TSRC": "isrc", - "TSST": "discsubtitle", - "TLAN": "language", -}): - EasyID3.RegisterTextKey(key, frameid) - -EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete) -EasyID3.RegisterKey("date", date_get, date_set, date_delete) -EasyID3.RegisterKey("originaldate", original_date_get, original_date_set, - original_date_delete) -EasyID3.RegisterKey( - "performer:*", performer_get, performer_set, performer_delete, - performer_list) -EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get, - musicbrainz_trackid_set, musicbrainz_trackid_delete) -EasyID3.RegisterKey("website", website_get, website_set, website_delete) -EasyID3.RegisterKey( - "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list) -EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete) - -# At various times, information for this came from -# http://musicbrainz.org/docs/specs/metadata_tags.html -# http://bugs.musicbrainz.org/ticket/1383 -# http://musicbrainz.org/doc/MusicBrainzTag -for desc, key in iteritems({ - u"MusicBrainz Artist Id": "musicbrainz_artistid", - u"MusicBrainz Album Id": "musicbrainz_albumid", - u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid", - u"MusicBrainz TRM Id": "musicbrainz_trmid", - u"MusicIP PUID": "musicip_puid", - u"MusicMagic Fingerprint": "musicip_fingerprint", - u"MusicBrainz Album Status": "musicbrainz_albumstatus", - u"MusicBrainz Album Type": "musicbrainz_albumtype", - u"MusicBrainz Album Release Country": "releasecountry", - u"MusicBrainz Disc Id": "musicbrainz_discid", - u"ASIN": "asin", - u"ALBUMARTISTSORT": "albumartistsort", - u"BARCODE": "barcode", - u"CATALOGNUMBER": "catalognumber", - u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid", - u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid", - u"MusicBrainz Work Id": "musicbrainz_workid", - u"Acoustid Fingerprint": "acoustid_fingerprint", - u"Acoustid Id": "acoustid_id", -}): - EasyID3.RegisterTXXXKey(key, desc) - - -class EasyID3FileType(ID3FileType): - """Like ID3FileType, but uses EasyID3 for tags.""" - ID3 = EasyID3 diff --git a/resources/lib/libraries/mutagen/easymp4.py b/resources/lib/libraries/mutagen/easymp4.py deleted file mode 100644 index b965f37d..00000000 --- a/resources/lib/libraries/mutagen/easymp4.py +++ /dev/null @@ -1,285 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2009 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -from mutagen import Metadata -from mutagen._util import DictMixin, dict_match -from mutagen.mp4 import MP4, MP4Tags, error, delete -from ._compat import PY2, text_type, PY3 - - -__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] - - -class EasyMP4KeyError(error, KeyError, ValueError): - pass - - -class EasyMP4Tags(DictMixin, Metadata): - """A file with MPEG-4 iTunes metadata. - - Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII - strings, and values are a list of Unicode strings (and these lists - are always of length 0 or 1). - - If you need access to the full MP4 metadata feature set, you should use - MP4, not EasyMP4. - """ - - Set = {} - Get = {} - Delete = {} - List = {} - - def __init__(self, *args, **kwargs): - self.__mp4 = MP4Tags(*args, **kwargs) - self.load = self.__mp4.load - self.save = self.__mp4.save - self.delete = self.__mp4.delete - self._padding = self.__mp4._padding - - filename = property(lambda s: s.__mp4.filename, - lambda s, fn: setattr(s.__mp4, 'filename', fn)) - - @classmethod - def RegisterKey(cls, key, - getter=None, setter=None, deleter=None, lister=None): - """Register a new key mapping. - - A key mapping is four functions, a getter, setter, deleter, - and lister. The key may be either a string or a glob pattern. - - The getter, deleted, and lister receive an MP4Tags instance - and the requested key name. The setter also receives the - desired value, which will be a list of strings. - - The getter, setter, and deleter are used to implement __getitem__, - __setitem__, and __delitem__. - - The lister is used to implement keys(). It should return a - list of keys that are actually in the MP4 instance, provided - by its associated getter. - """ - key = key.lower() - if getter is not None: - cls.Get[key] = getter - if setter is not None: - cls.Set[key] = setter - if deleter is not None: - cls.Delete[key] = deleter - if lister is not None: - cls.List[key] = lister - - @classmethod - def RegisterTextKey(cls, key, atomid): - """Register a text key. - - If the key you need to register is a simple one-to-one mapping - of MP4 atom name to EasyMP4Tags key, then you can use this - function:: - - EasyMP4Tags.RegisterTextKey("artist", "\xa9ART") - """ - def getter(tags, key): - return tags[atomid] - - def setter(tags, key, value): - tags[atomid] = value - - def deleter(tags, key): - del(tags[atomid]) - - cls.RegisterKey(key, getter, setter, deleter) - - @classmethod - def RegisterIntKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1): - """Register a scalar integer key. - """ - - def getter(tags, key): - return list(map(text_type, tags[atomid])) - - def setter(tags, key, value): - clamp = lambda x: int(min(max(min_value, x), max_value)) - tags[atomid] = [clamp(v) for v in map(int, value)] - - def deleter(tags, key): - del(tags[atomid]) - - cls.RegisterKey(key, getter, setter, deleter) - - @classmethod - def RegisterIntPairKey(cls, key, atomid, min_value=0, - max_value=(2 ** 16) - 1): - def getter(tags, key): - ret = [] - for (track, total) in tags[atomid]: - if total: - ret.append(u"%d/%d" % (track, total)) - else: - ret.append(text_type(track)) - return ret - - def setter(tags, key, value): - clamp = lambda x: int(min(max(min_value, x), max_value)) - data = [] - for v in value: - try: - tracks, total = v.split("/") - tracks = clamp(int(tracks)) - total = clamp(int(total)) - except (ValueError, TypeError): - tracks = clamp(int(v)) - total = min_value - data.append((tracks, total)) - tags[atomid] = data - - def deleter(tags, key): - del(tags[atomid]) - - cls.RegisterKey(key, getter, setter, deleter) - - @classmethod - def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"): - """Register a text key. - - If the key you need to register is a simple one-to-one mapping - of MP4 freeform atom (----) and name to EasyMP4Tags key, then - you can use this function:: - - EasyMP4Tags.RegisterFreeformKey( - "musicbrainz_artistid", "MusicBrainz Artist Id") - """ - atomid = "----:" + mean + ":" + name - - def getter(tags, key): - return [s.decode("utf-8", "replace") for s in tags[atomid]] - - def setter(tags, key, value): - encoded = [] - for v in value: - if not isinstance(v, text_type): - if PY3: - raise TypeError("%r not str" % v) - v = v.decode("utf-8") - encoded.append(v.encode("utf-8")) - tags[atomid] = encoded - - def deleter(tags, key): - del(tags[atomid]) - - cls.RegisterKey(key, getter, setter, deleter) - - def __getitem__(self, key): - key = key.lower() - func = dict_match(self.Get, key) - if func is not None: - return func(self.__mp4, key) - else: - raise EasyMP4KeyError("%r is not a valid key" % key) - - def __setitem__(self, key, value): - key = key.lower() - - if PY2: - if isinstance(value, basestring): - value = [value] - else: - if isinstance(value, text_type): - value = [value] - - func = dict_match(self.Set, key) - if func is not None: - return func(self.__mp4, key, value) - else: - raise EasyMP4KeyError("%r is not a valid key" % key) - - def __delitem__(self, key): - key = key.lower() - func = dict_match(self.Delete, key) - if func is not None: - return func(self.__mp4, key) - else: - raise EasyMP4KeyError("%r is not a valid key" % key) - - def keys(self): - keys = [] - for key in self.Get.keys(): - if key in self.List: - keys.extend(self.List[key](self.__mp4, key)) - elif key in self: - keys.append(key) - return keys - - def pprint(self): - """Print tag key=value pairs.""" - strings = [] - for key in sorted(self.keys()): - values = self[key] - for value in values: - strings.append("%s=%s" % (key, value)) - return "\n".join(strings) - -for atomid, key in { - '\xa9nam': 'title', - '\xa9alb': 'album', - '\xa9ART': 'artist', - 'aART': 'albumartist', - '\xa9day': 'date', - '\xa9cmt': 'comment', - 'desc': 'description', - '\xa9grp': 'grouping', - '\xa9gen': 'genre', - 'cprt': 'copyright', - 'soal': 'albumsort', - 'soaa': 'albumartistsort', - 'soar': 'artistsort', - 'sonm': 'titlesort', - 'soco': 'composersort', -}.items(): - EasyMP4Tags.RegisterTextKey(key, atomid) - -for name, key in { - 'MusicBrainz Artist Id': 'musicbrainz_artistid', - 'MusicBrainz Track Id': 'musicbrainz_trackid', - 'MusicBrainz Album Id': 'musicbrainz_albumid', - 'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid', - 'MusicIP PUID': 'musicip_puid', - 'MusicBrainz Album Status': 'musicbrainz_albumstatus', - 'MusicBrainz Album Type': 'musicbrainz_albumtype', - 'MusicBrainz Release Country': 'releasecountry', -}.items(): - EasyMP4Tags.RegisterFreeformKey(key, name) - -for name, key in { - "tmpo": "bpm", -}.items(): - EasyMP4Tags.RegisterIntKey(key, name) - -for name, key in { - "trkn": "tracknumber", - "disk": "discnumber", -}.items(): - EasyMP4Tags.RegisterIntPairKey(key, name) - - -class EasyMP4(MP4): - """Like :class:`MP4 <mutagen.mp4.MP4>`, - but uses :class:`EasyMP4Tags` for tags. - - :ivar info: :class:`MP4Info <mutagen.mp4.MP4Info>` - :ivar tags: :class:`EasyMP4Tags` - """ - - MP4Tags = EasyMP4Tags - - Get = EasyMP4Tags.Get - Set = EasyMP4Tags.Set - Delete = EasyMP4Tags.Delete - List = EasyMP4Tags.List - RegisterTextKey = EasyMP4Tags.RegisterTextKey - RegisterKey = EasyMP4Tags.RegisterKey diff --git a/resources/lib/libraries/mutagen/flac.py b/resources/lib/libraries/mutagen/flac.py deleted file mode 100644 index e6cd1cf7..00000000 --- a/resources/lib/libraries/mutagen/flac.py +++ /dev/null @@ -1,876 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""Read and write FLAC Vorbis comments and stream information. - -Read more about FLAC at http://flac.sourceforge.net. - -FLAC supports arbitrary metadata blocks. The two most interesting ones -are the FLAC stream information block, and the Vorbis comment block; -these are also the only ones Mutagen can currently read. - -This module does not handle Ogg FLAC files. - -Based off documentation available at -http://flac.sourceforge.net/format.html -""" - -__all__ = ["FLAC", "Open", "delete"] - -import struct -from ._vorbis import VCommentDict -import mutagen - -from ._compat import cBytesIO, endswith, chr_, xrange -from mutagen._util import resize_bytes, MutagenError, get_size -from mutagen._tags import PaddingInfo -from mutagen.id3 import BitPaddedInt -from functools import reduce - - -class error(IOError, MutagenError): - pass - - -class FLACNoHeaderError(error): - pass - - -class FLACVorbisError(ValueError, error): - pass - - -def to_int_be(data): - """Convert an arbitrarily-long string to a long using big-endian - byte order.""" - return reduce(lambda a, b: (a << 8) + b, bytearray(data), 0) - - -class StrictFileObject(object): - """Wraps a file-like object and raises an exception if the requested - amount of data to read isn't returned.""" - - def __init__(self, fileobj): - self._fileobj = fileobj - for m in ["close", "tell", "seek", "write", "name"]: - if hasattr(fileobj, m): - setattr(self, m, getattr(fileobj, m)) - - def read(self, size=-1): - data = self._fileobj.read(size) - if size >= 0 and len(data) != size: - raise error("file said %d bytes, read %d bytes" % ( - size, len(data))) - return data - - def tryread(self, *args): - return self._fileobj.read(*args) - - -class MetadataBlock(object): - """A generic block of FLAC metadata. - - This class is extended by specific used as an ancestor for more specific - blocks, and also as a container for data blobs of unknown blocks. - - Attributes: - - * data -- raw binary data for this block - """ - - _distrust_size = False - """For block types setting this, we don't trust the size field and - use the size of the content instead.""" - - _invalid_overflow_size = -1 - """In case the real size was bigger than what is representable by the - 24 bit size field, we save the wrong specified size here. This can - only be set if _distrust_size is True""" - - _MAX_SIZE = 2 ** 24 - 1 - - def __init__(self, data): - """Parse the given data string or file-like as a metadata block. - The metadata header should not be included.""" - if data is not None: - if not isinstance(data, StrictFileObject): - if isinstance(data, bytes): - data = cBytesIO(data) - elif not hasattr(data, 'read'): - raise TypeError( - "StreamInfo requires string data or a file-like") - data = StrictFileObject(data) - self.load(data) - - def load(self, data): - self.data = data.read() - - def write(self): - return self.data - - @classmethod - def _writeblock(cls, block, is_last=False): - """Returns the block content + header. - - Raises error. - """ - - data = bytearray() - code = (block.code | 128) if is_last else block.code - datum = block.write() - size = len(datum) - if size > cls._MAX_SIZE: - if block._distrust_size and block._invalid_overflow_size != -1: - # The original size of this block was (1) wrong and (2) - # the real size doesn't allow us to save the file - # according to the spec (too big for 24 bit uint). Instead - # simply write back the original wrong size.. at least - # we don't make the file more "broken" as it is. - size = block._invalid_overflow_size - else: - raise error("block is too long to write") - assert not size > cls._MAX_SIZE - length = struct.pack(">I", size)[-3:] - data.append(code) - data += length - data += datum - return data - - @classmethod - def _writeblocks(cls, blocks, available, cont_size, padding_func): - """Render metadata block as a byte string.""" - - # write everything except padding - data = bytearray() - for block in blocks: - if isinstance(block, Padding): - continue - data += cls._writeblock(block) - blockssize = len(data) - - # take the padding overhead into account. we always add one - # to make things simple. - padding_block = Padding() - blockssize += len(cls._writeblock(padding_block)) - - # finally add a padding block - info = PaddingInfo(available - blockssize, cont_size) - padding_block.length = min(info._get_padding(padding_func), - cls._MAX_SIZE) - data += cls._writeblock(padding_block, is_last=True) - - return data - - -class StreamInfo(MetadataBlock, mutagen.StreamInfo): - """FLAC stream information. - - This contains information about the audio data in the FLAC file. - Unlike most stream information objects in Mutagen, changes to this - one will rewritten to the file when it is saved. Unless you are - actually changing the audio stream itself, don't change any - attributes of this block. - - Attributes: - - * min_blocksize -- minimum audio block size - * max_blocksize -- maximum audio block size - * sample_rate -- audio sample rate in Hz - * channels -- audio channels (1 for mono, 2 for stereo) - * bits_per_sample -- bits per sample - * total_samples -- total samples in file - * length -- audio length in seconds - """ - - code = 0 - - def __eq__(self, other): - try: - return (self.min_blocksize == other.min_blocksize and - self.max_blocksize == other.max_blocksize and - self.sample_rate == other.sample_rate and - self.channels == other.channels and - self.bits_per_sample == other.bits_per_sample and - self.total_samples == other.total_samples) - except: - return False - - __hash__ = MetadataBlock.__hash__ - - def load(self, data): - self.min_blocksize = int(to_int_be(data.read(2))) - self.max_blocksize = int(to_int_be(data.read(2))) - self.min_framesize = int(to_int_be(data.read(3))) - self.max_framesize = int(to_int_be(data.read(3))) - # first 16 bits of sample rate - sample_first = to_int_be(data.read(2)) - # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample - sample_channels_bps = to_int_be(data.read(1)) - # last 4 of bits/sample, 36 of total samples - bps_total = to_int_be(data.read(5)) - - sample_tail = sample_channels_bps >> 4 - self.sample_rate = int((sample_first << 4) + sample_tail) - if not self.sample_rate: - raise error("A sample rate value of 0 is invalid") - self.channels = int(((sample_channels_bps >> 1) & 7) + 1) - bps_tail = bps_total >> 36 - bps_head = (sample_channels_bps & 1) << 4 - self.bits_per_sample = int(bps_head + bps_tail + 1) - self.total_samples = bps_total & 0xFFFFFFFFF - self.length = self.total_samples / float(self.sample_rate) - - self.md5_signature = to_int_be(data.read(16)) - - def write(self): - f = cBytesIO() - f.write(struct.pack(">I", self.min_blocksize)[-2:]) - f.write(struct.pack(">I", self.max_blocksize)[-2:]) - f.write(struct.pack(">I", self.min_framesize)[-3:]) - f.write(struct.pack(">I", self.max_framesize)[-3:]) - - # first 16 bits of sample rate - f.write(struct.pack(">I", self.sample_rate >> 4)[-2:]) - # 4 bits sample, 3 channel, 1 bps - byte = (self.sample_rate & 0xF) << 4 - byte += ((self.channels - 1) & 7) << 1 - byte += ((self.bits_per_sample - 1) >> 4) & 1 - f.write(chr_(byte)) - # 4 bits of bps, 4 of sample count - byte = ((self.bits_per_sample - 1) & 0xF) << 4 - byte += (self.total_samples >> 32) & 0xF - f.write(chr_(byte)) - # last 32 of sample count - f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF)) - # MD5 signature - sig = self.md5_signature - f.write(struct.pack( - ">4I", (sig >> 96) & 0xFFFFFFFF, (sig >> 64) & 0xFFFFFFFF, - (sig >> 32) & 0xFFFFFFFF, sig & 0xFFFFFFFF)) - return f.getvalue() - - def pprint(self): - return u"FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate) - - -class SeekPoint(tuple): - """A single seek point in a FLAC file. - - Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL, - and byte_offset and num_samples undefined. Seek points must be - sorted in ascending order by first_sample number. Seek points must - be unique by first_sample number, except for placeholder - points. Placeholder points must occur last in the table and there - may be any number of them. - - Attributes: - - * first_sample -- sample number of first sample in the target frame - * byte_offset -- offset from first frame to target frame - * num_samples -- number of samples in target frame - """ - - def __new__(cls, first_sample, byte_offset, num_samples): - return super(cls, SeekPoint).__new__( - cls, (first_sample, byte_offset, num_samples)) - - first_sample = property(lambda self: self[0]) - byte_offset = property(lambda self: self[1]) - num_samples = property(lambda self: self[2]) - - -class SeekTable(MetadataBlock): - """Read and write FLAC seek tables. - - Attributes: - - * seekpoints -- list of SeekPoint objects - """ - - __SEEKPOINT_FORMAT = '>QQH' - __SEEKPOINT_SIZE = struct.calcsize(__SEEKPOINT_FORMAT) - - code = 3 - - def __init__(self, data): - self.seekpoints = [] - super(SeekTable, self).__init__(data) - - def __eq__(self, other): - try: - return (self.seekpoints == other.seekpoints) - except (AttributeError, TypeError): - return False - - __hash__ = MetadataBlock.__hash__ - - def load(self, data): - self.seekpoints = [] - sp = data.tryread(self.__SEEKPOINT_SIZE) - while len(sp) == self.__SEEKPOINT_SIZE: - self.seekpoints.append(SeekPoint( - *struct.unpack(self.__SEEKPOINT_FORMAT, sp))) - sp = data.tryread(self.__SEEKPOINT_SIZE) - - def write(self): - f = cBytesIO() - for seekpoint in self.seekpoints: - packed = struct.pack( - self.__SEEKPOINT_FORMAT, - seekpoint.first_sample, seekpoint.byte_offset, - seekpoint.num_samples) - f.write(packed) - return f.getvalue() - - def __repr__(self): - return "<%s seekpoints=%r>" % (type(self).__name__, self.seekpoints) - - -class VCFLACDict(VCommentDict): - """Read and write FLAC Vorbis comments. - - FLACs don't use the framing bit at the end of the comment block. - So this extends VCommentDict to not use the framing bit. - """ - - code = 4 - _distrust_size = True - - def load(self, data, errors='replace', framing=False): - super(VCFLACDict, self).load(data, errors=errors, framing=framing) - - def write(self, framing=False): - return super(VCFLACDict, self).write(framing=framing) - - -class CueSheetTrackIndex(tuple): - """Index for a track in a cuesheet. - - For CD-DA, an index_number of 0 corresponds to the track - pre-gap. The first index in a track must have a number of 0 or 1, - and subsequently, index_numbers must increase by 1. Index_numbers - must be unique within a track. And index_offset must be evenly - divisible by 588 samples. - - Attributes: - - * index_number -- index point number - * index_offset -- offset in samples from track start - """ - - def __new__(cls, index_number, index_offset): - return super(cls, CueSheetTrackIndex).__new__( - cls, (index_number, index_offset)) - - index_number = property(lambda self: self[0]) - index_offset = property(lambda self: self[1]) - - -class CueSheetTrack(object): - """A track in a cuesheet. - - For CD-DA, track_numbers must be 1-99, or 170 for the - lead-out. Track_numbers must be unique within a cue sheet. There - must be atleast one index in every track except the lead-out track - which must have none. - - Attributes: - - * track_number -- track number - * start_offset -- track offset in samples from start of FLAC stream - * isrc -- ISRC code - * type -- 0 for audio, 1 for digital data - * pre_emphasis -- true if the track is recorded with pre-emphasis - * indexes -- list of CueSheetTrackIndex objects - """ - - def __init__(self, track_number, start_offset, isrc='', type_=0, - pre_emphasis=False): - self.track_number = track_number - self.start_offset = start_offset - self.isrc = isrc - self.type = type_ - self.pre_emphasis = pre_emphasis - self.indexes = [] - - def __eq__(self, other): - try: - return (self.track_number == other.track_number and - self.start_offset == other.start_offset and - self.isrc == other.isrc and - self.type == other.type and - self.pre_emphasis == other.pre_emphasis and - self.indexes == other.indexes) - except (AttributeError, TypeError): - return False - - __hash__ = object.__hash__ - - def __repr__(self): - return (("<%s number=%r, offset=%d, isrc=%r, type=%r, " - "pre_emphasis=%r, indexes=%r)>") % - (type(self).__name__, self.track_number, self.start_offset, - self.isrc, self.type, self.pre_emphasis, self.indexes)) - - -class CueSheet(MetadataBlock): - """Read and write FLAC embedded cue sheets. - - Number of tracks should be from 1 to 100. There should always be - exactly one lead-out track and that track must be the last track - in the cue sheet. - - Attributes: - - * media_catalog_number -- media catalog number in ASCII - * lead_in_samples -- number of lead-in samples - * compact_disc -- true if the cuesheet corresponds to a compact disc - * tracks -- list of CueSheetTrack objects - * lead_out -- lead-out as CueSheetTrack or None if lead-out was not found - """ - - __CUESHEET_FORMAT = '>128sQB258xB' - __CUESHEET_SIZE = struct.calcsize(__CUESHEET_FORMAT) - __CUESHEET_TRACK_FORMAT = '>QB12sB13xB' - __CUESHEET_TRACK_SIZE = struct.calcsize(__CUESHEET_TRACK_FORMAT) - __CUESHEET_TRACKINDEX_FORMAT = '>QB3x' - __CUESHEET_TRACKINDEX_SIZE = struct.calcsize(__CUESHEET_TRACKINDEX_FORMAT) - - code = 5 - - media_catalog_number = b'' - lead_in_samples = 88200 - compact_disc = True - - def __init__(self, data): - self.tracks = [] - super(CueSheet, self).__init__(data) - - def __eq__(self, other): - try: - return (self.media_catalog_number == other.media_catalog_number and - self.lead_in_samples == other.lead_in_samples and - self.compact_disc == other.compact_disc and - self.tracks == other.tracks) - except (AttributeError, TypeError): - return False - - __hash__ = MetadataBlock.__hash__ - - def load(self, data): - header = data.read(self.__CUESHEET_SIZE) - media_catalog_number, lead_in_samples, flags, num_tracks = \ - struct.unpack(self.__CUESHEET_FORMAT, header) - self.media_catalog_number = media_catalog_number.rstrip(b'\0') - self.lead_in_samples = lead_in_samples - self.compact_disc = bool(flags & 0x80) - self.tracks = [] - for i in xrange(num_tracks): - track = data.read(self.__CUESHEET_TRACK_SIZE) - start_offset, track_number, isrc_padded, flags, num_indexes = \ - struct.unpack(self.__CUESHEET_TRACK_FORMAT, track) - isrc = isrc_padded.rstrip(b'\0') - type_ = (flags & 0x80) >> 7 - pre_emphasis = bool(flags & 0x40) - val = CueSheetTrack( - track_number, start_offset, isrc, type_, pre_emphasis) - for j in xrange(num_indexes): - index = data.read(self.__CUESHEET_TRACKINDEX_SIZE) - index_offset, index_number = struct.unpack( - self.__CUESHEET_TRACKINDEX_FORMAT, index) - val.indexes.append( - CueSheetTrackIndex(index_number, index_offset)) - self.tracks.append(val) - - def write(self): - f = cBytesIO() - flags = 0 - if self.compact_disc: - flags |= 0x80 - packed = struct.pack( - self.__CUESHEET_FORMAT, self.media_catalog_number, - self.lead_in_samples, flags, len(self.tracks)) - f.write(packed) - for track in self.tracks: - track_flags = 0 - track_flags |= (track.type & 1) << 7 - if track.pre_emphasis: - track_flags |= 0x40 - track_packed = struct.pack( - self.__CUESHEET_TRACK_FORMAT, track.start_offset, - track.track_number, track.isrc, track_flags, - len(track.indexes)) - f.write(track_packed) - for index in track.indexes: - index_packed = struct.pack( - self.__CUESHEET_TRACKINDEX_FORMAT, - index.index_offset, index.index_number) - f.write(index_packed) - return f.getvalue() - - def __repr__(self): - return (("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, " - "tracks=%r>") % - (type(self).__name__, self.media_catalog_number, - self.lead_in_samples, self.compact_disc, self.tracks)) - - -class Picture(MetadataBlock): - """Read and write FLAC embed pictures. - - Attributes: - - * type -- picture type (same as types for ID3 APIC frames) - * mime -- MIME type of the picture - * desc -- picture's description - * width -- width in pixels - * height -- height in pixels - * depth -- color depth in bits-per-pixel - * colors -- number of colors for indexed palettes (like GIF), - 0 for non-indexed - * data -- picture data - - To create a picture from file (in order to add to a FLAC file), - instantiate this object without passing anything to the constructor and - then set the properties manually:: - - p = Picture() - - with open("Folder.jpg", "rb") as f: - pic.data = f.read() - - pic.type = id3.PictureType.COVER_FRONT - pic.mime = u"image/jpeg" - pic.width = 500 - pic.height = 500 - pic.depth = 16 # color depth - """ - - code = 6 - _distrust_size = True - - def __init__(self, data=None): - self.type = 0 - self.mime = u'' - self.desc = u'' - self.width = 0 - self.height = 0 - self.depth = 0 - self.colors = 0 - self.data = b'' - super(Picture, self).__init__(data) - - def __eq__(self, other): - try: - return (self.type == other.type and - self.mime == other.mime and - self.desc == other.desc and - self.width == other.width and - self.height == other.height and - self.depth == other.depth and - self.colors == other.colors and - self.data == other.data) - except (AttributeError, TypeError): - return False - - __hash__ = MetadataBlock.__hash__ - - def load(self, data): - self.type, length = struct.unpack('>2I', data.read(8)) - self.mime = data.read(length).decode('UTF-8', 'replace') - length, = struct.unpack('>I', data.read(4)) - self.desc = data.read(length).decode('UTF-8', 'replace') - (self.width, self.height, self.depth, - self.colors, length) = struct.unpack('>5I', data.read(20)) - self.data = data.read(length) - - def write(self): - f = cBytesIO() - mime = self.mime.encode('UTF-8') - f.write(struct.pack('>2I', self.type, len(mime))) - f.write(mime) - desc = self.desc.encode('UTF-8') - f.write(struct.pack('>I', len(desc))) - f.write(desc) - f.write(struct.pack('>5I', self.width, self.height, self.depth, - self.colors, len(self.data))) - f.write(self.data) - return f.getvalue() - - def __repr__(self): - return "<%s '%s' (%d bytes)>" % (type(self).__name__, self.mime, - len(self.data)) - - -class Padding(MetadataBlock): - """Empty padding space for metadata blocks. - - To avoid rewriting the entire FLAC file when editing comments, - metadata is often padded. Padding should occur at the end, and no - more than one padding block should be in any FLAC file. - """ - - code = 1 - - def __init__(self, data=b""): - super(Padding, self).__init__(data) - - def load(self, data): - self.length = len(data.read()) - - def write(self): - try: - return b"\x00" * self.length - # On some 64 bit platforms this won't generate a MemoryError - # or OverflowError since you might have enough RAM, but it - # still generates a ValueError. On other 64 bit platforms, - # this will still succeed for extremely large values. - # Those should never happen in the real world, and if they - # do, writeblocks will catch it. - except (OverflowError, ValueError, MemoryError): - raise error("cannot write %d bytes" % self.length) - - def __eq__(self, other): - return isinstance(other, Padding) and self.length == other.length - - __hash__ = MetadataBlock.__hash__ - - def __repr__(self): - return "<%s (%d bytes)>" % (type(self).__name__, self.length) - - -class FLAC(mutagen.FileType): - """A FLAC audio file. - - Attributes: - - * cuesheet -- CueSheet object, if any - * seektable -- SeekTable object, if any - * pictures -- list of embedded pictures - """ - - _mimes = ["audio/x-flac", "application/x-flac"] - - info = None - """A `StreamInfo`""" - - tags = None - """A `VCommentDict`""" - - METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict, - CueSheet, Picture] - """Known metadata block types, indexed by ID.""" - - @staticmethod - def score(filename, fileobj, header_data): - return (header_data.startswith(b"fLaC") + - endswith(filename.lower(), ".flac") * 3) - - def __read_metadata_block(self, fileobj): - byte = ord(fileobj.read(1)) - size = to_int_be(fileobj.read(3)) - code = byte & 0x7F - last_block = bool(byte & 0x80) - - try: - block_type = self.METADATA_BLOCKS[code] or MetadataBlock - except IndexError: - block_type = MetadataBlock - - if block_type._distrust_size: - # Some jackass is writing broken Metadata block length - # for Vorbis comment blocks, and the FLAC reference - # implementaton can parse them (mostly by accident), - # so we have to too. Instead of parsing the size - # given, parse an actual Vorbis comment, leaving - # fileobj in the right position. - # http://code.google.com/p/mutagen/issues/detail?id=52 - # ..same for the Picture block: - # http://code.google.com/p/mutagen/issues/detail?id=106 - start = fileobj.tell() - block = block_type(fileobj) - real_size = fileobj.tell() - start - if real_size > MetadataBlock._MAX_SIZE: - block._invalid_overflow_size = size - else: - data = fileobj.read(size) - block = block_type(data) - block.code = code - - if block.code == VCFLACDict.code: - if self.tags is None: - self.tags = block - else: - raise FLACVorbisError("> 1 Vorbis comment block found") - elif block.code == CueSheet.code: - if self.cuesheet is None: - self.cuesheet = block - else: - raise error("> 1 CueSheet block found") - elif block.code == SeekTable.code: - if self.seektable is None: - self.seektable = block - else: - raise error("> 1 SeekTable block found") - self.metadata_blocks.append(block) - return not last_block - - def add_tags(self): - """Add a Vorbis comment block to the file.""" - if self.tags is None: - self.tags = VCFLACDict() - self.metadata_blocks.append(self.tags) - else: - raise FLACVorbisError("a Vorbis comment already exists") - - add_vorbiscomment = add_tags - - def delete(self, filename=None): - """Remove Vorbis comments from a file. - - If no filename is given, the one most recently loaded is used. - """ - if filename is None: - filename = self.filename - - if self.tags is not None: - self.metadata_blocks.remove(self.tags) - self.save(padding=lambda x: 0) - self.metadata_blocks.append(self.tags) - self.tags.clear() - - vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") - - def load(self, filename): - """Load file information from a filename.""" - - self.metadata_blocks = [] - self.tags = None - self.cuesheet = None - self.seektable = None - self.filename = filename - fileobj = StrictFileObject(open(filename, "rb")) - try: - self.__check_header(fileobj) - while self.__read_metadata_block(fileobj): - pass - finally: - fileobj.close() - - try: - self.metadata_blocks[0].length - except (AttributeError, IndexError): - raise FLACNoHeaderError("Stream info block not found") - - @property - def info(self): - return self.metadata_blocks[0] - - def add_picture(self, picture): - """Add a new picture to the file.""" - self.metadata_blocks.append(picture) - - def clear_pictures(self): - """Delete all pictures from the file.""" - - blocks = [b for b in self.metadata_blocks if b.code != Picture.code] - self.metadata_blocks = blocks - - @property - def pictures(self): - """List of embedded pictures""" - - return [b for b in self.metadata_blocks if b.code == Picture.code] - - def save(self, filename=None, deleteid3=False, padding=None): - """Save metadata blocks to a file. - - If no filename is given, the one most recently loaded is used. - """ - - if filename is None: - filename = self.filename - - with open(filename, 'rb+') as f: - header = self.__check_header(f) - audio_offset = self.__find_audio_offset(f) - # "fLaC" and maybe ID3 - available = audio_offset - header - - # Delete ID3v2 - if deleteid3 and header > 4: - available += header - 4 - header = 4 - - content_size = get_size(f) - audio_offset - assert content_size >= 0 - data = MetadataBlock._writeblocks( - self.metadata_blocks, available, content_size, padding) - data_size = len(data) - - resize_bytes(f, available, data_size, header) - f.seek(header - 4) - f.write(b"fLaC") - f.write(data) - - # Delete ID3v1 - if deleteid3: - try: - f.seek(-128, 2) - except IOError: - pass - else: - if f.read(3) == b"TAG": - f.seek(-128, 2) - f.truncate() - - def __find_audio_offset(self, fileobj): - byte = 0x00 - while not (byte & 0x80): - byte = ord(fileobj.read(1)) - size = to_int_be(fileobj.read(3)) - try: - block_type = self.METADATA_BLOCKS[byte & 0x7F] - except IndexError: - block_type = None - - if block_type and block_type._distrust_size: - # See comments in read_metadata_block; the size can't - # be trusted for Vorbis comment blocks and Picture block - block_type(fileobj) - else: - fileobj.read(size) - return fileobj.tell() - - def __check_header(self, fileobj): - """Returns the offset of the flac block start - (skipping id3 tags if found). The passed fileobj will be advanced to - that offset as well. - """ - - size = 4 - header = fileobj.read(4) - if header != b"fLaC": - size = None - if header[:3] == b"ID3": - size = 14 + BitPaddedInt(fileobj.read(6)[2:]) - fileobj.seek(size - 4) - if fileobj.read(4) != b"fLaC": - size = None - if size is None: - raise FLACNoHeaderError( - "%r is not a valid FLAC file" % fileobj.name) - return size - - -Open = FLAC - - -def delete(filename): - """Remove tags from a file.""" - FLAC(filename).delete() diff --git a/resources/lib/libraries/mutagen/id3/__init__.py b/resources/lib/libraries/mutagen/id3/__init__.py deleted file mode 100644 index 9aef865b..00000000 --- a/resources/lib/libraries/mutagen/id3/__init__.py +++ /dev/null @@ -1,1093 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Michael Urman -# 2006 Lukas Lalinsky -# 2013 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""ID3v2 reading and writing. - -This is based off of the following references: - -* http://id3.org/id3v2.4.0-structure -* http://id3.org/id3v2.4.0-frames -* http://id3.org/id3v2.3.0 -* http://id3.org/id3v2-00 -* http://id3.org/ID3v1 - -Its largest deviation from the above (versions 2.3 and 2.2) is that it -will not interpret the / characters as a separator, and will almost -always accept null separators to generate multi-valued text frames. - -Because ID3 frame structure differs between frame types, each frame is -implemented as a different class (e.g. TIT2 as mutagen.id3.TIT2). Each -frame's documentation contains a list of its attributes. - -Since this file's documentation is a little unwieldy, you are probably -interested in the :class:`ID3` class to start with. -""" - -__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] - -import struct -import errno - -from struct import unpack, pack, error as StructError - -import mutagen -from mutagen._util import insert_bytes, delete_bytes, DictProxy, enum -from mutagen._tags import PaddingInfo -from .._compat import chr_, PY3 - -from ._util import * -from ._frames import * -from ._specs import * - - -@enum -class ID3v1SaveOptions(object): - - REMOVE = 0 - """ID3v1 tags will be removed""" - - UPDATE = 1 - """ID3v1 tags will be updated but not added""" - - CREATE = 2 - """ID3v1 tags will be created and/or updated""" - - -def _fullread(fileobj, size): - """Read a certain number of bytes from the source file. - - Raises ValueError on invalid size input or EOFError/IOError. - """ - - if size < 0: - raise ValueError('Requested bytes (%s) less than zero' % size) - data = fileobj.read(size) - if len(data) != size: - raise EOFError("Not enough data to read") - return data - - -class ID3Header(object): - - _V24 = (2, 4, 0) - _V23 = (2, 3, 0) - _V22 = (2, 2, 0) - _V11 = (1, 1) - - f_unsynch = property(lambda s: bool(s._flags & 0x80)) - f_extended = property(lambda s: bool(s._flags & 0x40)) - f_experimental = property(lambda s: bool(s._flags & 0x20)) - f_footer = property(lambda s: bool(s._flags & 0x10)) - - def __init__(self, fileobj=None): - """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error""" - - if fileobj is None: - # for testing - self._flags = 0 - return - - fn = getattr(fileobj, "name", "<unknown>") - try: - data = _fullread(fileobj, 10) - except EOFError: - raise ID3NoHeaderError("%s: too small" % fn) - - id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data) - self._flags = flags - self.size = BitPaddedInt(size) + 10 - self.version = (2, vmaj, vrev) - - if id3 != b'ID3': - raise ID3NoHeaderError("%r doesn't start with an ID3 tag" % fn) - - if vmaj not in [2, 3, 4]: - raise ID3UnsupportedVersionError("%r ID3v2.%d not supported" - % (fn, vmaj)) - - if not BitPaddedInt.has_valid_padding(size): - raise error("Header size not synchsafe") - - if (self.version >= self._V24) and (flags & 0x0f): - raise error( - "%r has invalid flags %#02x" % (fn, flags)) - elif (self._V23 <= self.version < self._V24) and (flags & 0x1f): - raise error( - "%r has invalid flags %#02x" % (fn, flags)) - - if self.f_extended: - try: - extsize_data = _fullread(fileobj, 4) - except EOFError: - raise error("%s: too small" % fn) - - if PY3: - frame_id = extsize_data.decode("ascii", "replace") - else: - frame_id = extsize_data - - if frame_id in Frames: - # Some tagger sets the extended header flag but - # doesn't write an extended header; in this case, the - # ID3 data follows immediately. Since no extended - # header is going to be long enough to actually match - # a frame, and if it's *not* a frame we're going to be - # completely lost anyway, this seems to be the most - # correct check. - # http://code.google.com/p/quodlibet/issues/detail?id=126 - self._flags ^= 0x40 - extsize = 0 - fileobj.seek(-4, 1) - elif self.version >= self._V24: - # "Where the 'Extended header size' is the size of the whole - # extended header, stored as a 32 bit synchsafe integer." - extsize = BitPaddedInt(extsize_data) - 4 - if not BitPaddedInt.has_valid_padding(extsize_data): - raise error( - "Extended header size not synchsafe") - else: - # "Where the 'Extended header size', currently 6 or 10 bytes, - # excludes itself." - extsize = unpack('>L', extsize_data)[0] - - try: - self._extdata = _fullread(fileobj, extsize) - except EOFError: - raise error("%s: too small" % fn) - - -class ID3(DictProxy, mutagen.Metadata): - """A file with an ID3v2 tag. - - Attributes: - - * version -- ID3 tag version as a tuple - * unknown_frames -- raw frame data of any unknown frames found - * size -- the total size of the ID3 tag, including the header - """ - - __module__ = "mutagen.id3" - - PEDANTIC = True - """Deprecated. Doesn't have any effect""" - - filename = None - - def __init__(self, *args, **kwargs): - self.unknown_frames = [] - self.__unknown_version = None - self._header = None - self._version = (2, 4, 0) - super(ID3, self).__init__(*args, **kwargs) - - @property - def version(self): - """ID3 tag version as a tuple (of the loaded file)""" - - if self._header is not None: - return self._header.version - return self._version - - @version.setter - def version(self, value): - self._version = value - - @property - def f_unsynch(self): - if self._header is not None: - return self._header.f_unsynch - return False - - @property - def f_extended(self): - if self._header is not None: - return self._header.f_extended - return False - - @property - def size(self): - if self._header is not None: - return self._header.size - return 0 - - def _pre_load_header(self, fileobj): - # XXX: for aiff to adjust the offset.. - pass - - def load(self, filename, known_frames=None, translate=True, v2_version=4): - """Load tags from a filename. - - Keyword arguments: - - * filename -- filename to load tag data from - * known_frames -- dict mapping frame IDs to Frame objects - * translate -- Update all tags to ID3v2.3/4 internally. If you - intend to save, this must be true or you have to - call update_to_v23() / update_to_v24() manually. - * v2_version -- if update_to_v23 or update_to_v24 get called (3 or 4) - - Example of loading a custom frame:: - - my_frames = dict(mutagen.id3.Frames) - class XMYF(Frame): ... - my_frames["XMYF"] = XMYF - mutagen.id3.ID3(filename, known_frames=my_frames) - """ - - if v2_version not in (3, 4): - raise ValueError("Only 3 and 4 possible for v2_version") - - self.filename = filename - self.unknown_frames = [] - self.__known_frames = known_frames - self._header = None - self._padding = 0 # for testing - - with open(filename, 'rb') as fileobj: - self._pre_load_header(fileobj) - - try: - self._header = ID3Header(fileobj) - except (ID3NoHeaderError, ID3UnsupportedVersionError): - frames, offset = _find_id3v1(fileobj) - if frames is None: - raise - - self.version = ID3Header._V11 - for v in frames.values(): - self.add(v) - else: - frames = self.__known_frames - if frames is None: - if self.version >= ID3Header._V23: - frames = Frames - elif self.version >= ID3Header._V22: - frames = Frames_2_2 - - try: - data = _fullread(fileobj, self.size - 10) - except (ValueError, EOFError, IOError) as e: - raise error(e) - - for frame in self.__read_frames(data, frames=frames): - if isinstance(frame, Frame): - self.add(frame) - else: - self.unknown_frames.append(frame) - self.__unknown_version = self.version[:2] - - if translate: - if v2_version == 3: - self.update_to_v23() - else: - self.update_to_v24() - - def getall(self, key): - """Return all frames with a given name (the list may be empty). - - This is best explained by examples:: - - id3.getall('TIT2') == [id3['TIT2']] - id3.getall('TTTT') == [] - id3.getall('TXXX') == [TXXX(desc='woo', text='bar'), - TXXX(desc='baz', text='quuuux'), ...] - - Since this is based on the frame's HashKey, which is - colon-separated, you can use it to do things like - ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``. - """ - if key in self: - return [self[key]] - else: - key = key + ":" - return [v for s, v in self.items() if s.startswith(key)] - - def delall(self, key): - """Delete all tags of a given kind; see getall.""" - if key in self: - del(self[key]) - else: - key = key + ":" - for k in list(self.keys()): - if k.startswith(key): - del(self[k]) - - def setall(self, key, values): - """Delete frames of the given type and add frames in 'values'.""" - self.delall(key) - for tag in values: - self[tag.HashKey] = tag - - def pprint(self): - """Return tags in a human-readable format. - - "Human-readable" is used loosely here. The format is intended - to mirror that used for Vorbis or APEv2 output, e.g. - - ``TIT2=My Title`` - - However, ID3 frames can have multiple keys: - - ``POPM=user@example.org=3 128/255`` - """ - frames = sorted(Frame.pprint(s) for s in self.values()) - return "\n".join(frames) - - def loaded_frame(self, tag): - """Deprecated; use the add method.""" - # turn 2.2 into 2.3/2.4 tags - if len(type(tag).__name__) == 3: - tag = type(tag).__base__(tag) - self[tag.HashKey] = tag - - # add = loaded_frame (and vice versa) break applications that - # expect to be able to override loaded_frame (e.g. Quod Libet), - # as does making loaded_frame call add. - def add(self, frame): - """Add a frame to the tag.""" - return self.loaded_frame(frame) - - def __read_frames(self, data, frames): - assert self.version >= ID3Header._V22 - - if self.version < ID3Header._V24 and self.f_unsynch: - try: - data = unsynch.decode(data) - except ValueError: - pass - - if self.version >= ID3Header._V23: - if self.version < ID3Header._V24: - bpi = int - else: - bpi = _determine_bpi(data, frames) - - while data: - header = data[:10] - try: - name, size, flags = unpack('>4sLH', header) - except struct.error: - return # not enough header - if name.strip(b'\x00') == b'': - return - - size = bpi(size) - framedata = data[10:10 + size] - data = data[10 + size:] - self._padding = len(data) - if size == 0: - continue # drop empty frames - - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue - - try: - # someone writes 2.3 frames with 2.2 names - if name[-1] == "\x00": - tag = Frames_2_2[name[:-1]] - name = tag.__base__.__name__ - - tag = frames[name] - except KeyError: - if is_valid_frame_id(name): - yield header + framedata - else: - try: - yield tag._fromData(self._header, flags, framedata) - except NotImplementedError: - yield header + framedata - except ID3JunkFrameError: - pass - elif self.version >= ID3Header._V22: - while data: - header = data[0:6] - try: - name, size = unpack('>3s3s', header) - except struct.error: - return # not enough header - size, = struct.unpack('>L', b'\x00' + size) - if name.strip(b'\x00') == b'': - return - - framedata = data[6:6 + size] - data = data[6 + size:] - self._padding = len(data) - if size == 0: - continue # drop empty frames - - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue - - try: - tag = frames[name] - except KeyError: - if is_valid_frame_id(name): - yield header + framedata - else: - try: - yield tag._fromData(self._header, 0, framedata) - except (ID3EncryptionUnsupportedError, - NotImplementedError): - yield header + framedata - except ID3JunkFrameError: - pass - - def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, - pad_func): - if v2_version == 3: - version = ID3Header._V23 - elif v2_version == 4: - version = ID3Header._V24 - else: - raise ValueError("Only 3 or 4 allowed for v2_version") - - # Sort frames by 'importance' - order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] - order = dict((b, a) for a, b in enumerate(order)) - last = len(order) - frames = sorted(self.items(), - key=lambda a: (order.get(a[0][:4], last), a[0])) - - framedata = [self.__save_frame(frame, version=version, v23_sep=v23_sep) - for (key, frame) in frames] - - # only write unknown frames if they were loaded from the version - # we are saving with or upgraded to it - if self.__unknown_version == version[:2]: - framedata.extend(data for data in self.unknown_frames - if len(data) > 10) - - needed = sum(map(len, framedata)) + 10 - - fileobj.seek(0, 2) - trailing_size = fileobj.tell() - start - - info = PaddingInfo(available - needed, trailing_size) - new_padding = info._get_padding(pad_func) - if new_padding < 0: - raise error("invalid padding") - new_size = needed + new_padding - - new_framesize = BitPaddedInt.to_str(new_size - 10, width=4) - header = pack('>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize) - - data = bytearray(header) - for frame in framedata: - data += frame - assert new_size >= len(data) - data += (new_size - len(data)) * b'\x00' - assert new_size == len(data) - - return data - - def save(self, filename=None, v1=1, v2_version=4, v23_sep='/', - padding=None): - """Save changes to a file. - - Args: - filename: - Filename to save the tag to. If no filename is given, - the one most recently loaded is used. - v1 (ID3v1SaveOptions): - if 0, ID3v1 tags will be removed. - if 1, ID3v1 tags will be updated but not added. - if 2, ID3v1 tags will be created and/or updated - v2 (int): - version of ID3v2 tags (3 or 4). - v23_sep (str): - the separator used to join multiple text values - if v2_version == 3. Defaults to '/' but if it's None - will be the ID3v2v2.4 null separator. - padding (function): - A function taking a PaddingInfo which should - return the amount of padding to use. If None (default) - will default to something reasonable. - - By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3 - tags, you must call method update_to_v23 before saving the file. - - The lack of a way to update only an ID3v1 tag is intentional. - - Can raise id3.error. - """ - - if filename is None: - filename = self.filename - - try: - f = open(filename, 'rb+') - except IOError as err: - from errno import ENOENT - if err.errno != ENOENT: - raise - f = open(filename, 'ab') # create, then reopen - f = open(filename, 'rb+') - - try: - try: - header = ID3Header(f) - except ID3NoHeaderError: - old_size = 0 - else: - old_size = header.size - - data = self._prepare_data( - f, 0, old_size, v2_version, v23_sep, padding) - new_size = len(data) - - if (old_size < new_size): - insert_bytes(f, new_size - old_size, old_size) - elif (old_size > new_size): - delete_bytes(f, old_size - new_size, new_size) - f.seek(0) - f.write(data) - - self.__save_v1(f, v1) - - finally: - f.close() - - def __save_v1(self, f, v1): - tag, offset = _find_id3v1(f) - has_v1 = tag is not None - - f.seek(offset, 2) - if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \ - v1 == ID3v1SaveOptions.CREATE: - f.write(MakeID3v1(self)) - else: - f.truncate() - - def delete(self, filename=None, delete_v1=True, delete_v2=True): - """Remove tags from a file. - - If no filename is given, the one most recently loaded is used. - - Keyword arguments: - - * delete_v1 -- delete any ID3v1 tag - * delete_v2 -- delete any ID3v2 tag - """ - if filename is None: - filename = self.filename - delete(filename, delete_v1, delete_v2) - self.clear() - - def __save_frame(self, frame, name=None, version=ID3Header._V24, - v23_sep=None): - flags = 0 - if isinstance(frame, TextFrame): - if len(str(frame)) == 0: - return b'' - - if version == ID3Header._V23: - framev23 = frame._get_v23_frame(sep=v23_sep) - framedata = framev23._writeData() - else: - framedata = frame._writeData() - - usize = len(framedata) - if usize > 2048: - # Disabled as this causes iTunes and other programs - # to fail to find these frames, which usually includes - # e.g. APIC. - # framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib') - # flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN - pass - - if version == ID3Header._V24: - bits = 7 - elif version == ID3Header._V23: - bits = 8 - else: - raise ValueError - - datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) - - if name is not None: - assert isinstance(name, bytes) - frame_name = name - else: - frame_name = type(frame).__name__ - if PY3: - frame_name = frame_name.encode("ascii") - - header = pack('>4s4sH', frame_name, datasize, flags) - return header + framedata - - def __update_common(self): - """Updates done by both v23 and v24 update""" - - if "TCON" in self: - # Get rid of "(xx)Foobr" format. - self["TCON"].genres = self["TCON"].genres - - # ID3v2.2 LNK frames are just way too different to upgrade. - for frame in self.getall("LINK"): - if len(frame.frameid) != 4: - del self[frame.HashKey] - - mimes = {"PNG": "image/png", "JPG": "image/jpeg"} - for pic in self.getall("APIC"): - if pic.mime in mimes: - newpic = APIC( - encoding=pic.encoding, mime=mimes[pic.mime], - type=pic.type, desc=pic.desc, data=pic.data) - self.add(newpic) - - def update_to_v24(self): - """Convert older tags into an ID3v2.4 tag. - - This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to - TDRC). If you intend to save tags, you must call this function - at some point; it is called by default when loading the tag. - """ - - self.__update_common() - - if self.__unknown_version == (2, 3): - # convert unknown 2.3 frames (flags/size) to 2.4 - converted = [] - for frame in self.unknown_frames: - try: - name, size, flags = unpack('>4sLH', frame[:10]) - except struct.error: - continue - - try: - frame = BinaryFrame._fromData( - self._header, flags, frame[10:]) - except (error, NotImplementedError): - continue - - converted.append(self.__save_frame(frame, name=name)) - self.unknown_frames[:] = converted - self.__unknown_version = (2, 4) - - # TDAT, TYER, and TIME have been turned into TDRC. - try: - date = text_type(self.get("TYER", "")) - if date.strip(u"\x00"): - self.pop("TYER") - dat = text_type(self.get("TDAT", "")) - if dat.strip("\x00"): - self.pop("TDAT") - date = "%s-%s-%s" % (date, dat[2:], dat[:2]) - time = text_type(self.get("TIME", "")) - if time.strip("\x00"): - self.pop("TIME") - date += "T%s:%s:00" % (time[:2], time[2:]) - if "TDRC" not in self: - self.add(TDRC(encoding=0, text=date)) - except UnicodeDecodeError: - # Old ID3 tags have *lots* of Unicode problems, so if TYER - # is bad, just chuck the frames. - pass - - # TORY can be the first part of a TDOR. - if "TORY" in self: - f = self.pop("TORY") - if "TDOR" not in self: - try: - self.add(TDOR(encoding=0, text=str(f))) - except UnicodeDecodeError: - pass - - # IPLS is now TIPL. - if "IPLS" in self: - f = self.pop("IPLS") - if "TIPL" not in self: - self.add(TIPL(encoding=f.encoding, people=f.people)) - - # These can't be trivially translated to any ID3v2.4 tags, or - # should have been removed already. - for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]: - if key in self: - del(self[key]) - - def update_to_v23(self): - """Convert older (and newer) tags into an ID3v2.3 tag. - - This updates incompatible ID3v2 frames to ID3v2.3 ones. If you - intend to save tags as ID3v2.3, you must call this function - at some point. - - If you want to to go off spec and include some v2.4 frames - in v2.3, remove them before calling this and add them back afterwards. - """ - - self.__update_common() - - # we could downgrade unknown v2.4 frames here, but given that - # the main reason to save v2.3 is compatibility and this - # might increase the chance of some parser breaking.. better not - - # TMCL, TIPL -> TIPL - if "TIPL" in self or "TMCL" in self: - people = [] - if "TIPL" in self: - f = self.pop("TIPL") - people.extend(f.people) - if "TMCL" in self: - f = self.pop("TMCL") - people.extend(f.people) - if "IPLS" not in self: - self.add(IPLS(encoding=f.encoding, people=people)) - - # TDOR -> TORY - if "TDOR" in self: - f = self.pop("TDOR") - if f.text: - d = f.text[0] - if d.year and "TORY" not in self: - self.add(TORY(encoding=f.encoding, text="%04d" % d.year)) - - # TDRC -> TYER, TDAT, TIME - if "TDRC" in self: - f = self.pop("TDRC") - if f.text: - d = f.text[0] - if d.year and "TYER" not in self: - self.add(TYER(encoding=f.encoding, text="%04d" % d.year)) - if d.month and d.day and "TDAT" not in self: - self.add(TDAT(encoding=f.encoding, - text="%02d%02d" % (d.day, d.month))) - if d.hour and d.minute and "TIME" not in self: - self.add(TIME(encoding=f.encoding, - text="%02d%02d" % (d.hour, d.minute))) - - # New frames added in v2.4 - v24_frames = [ - 'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR', - 'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO', - 'TSOA', 'TSOP', 'TSOT', 'TSST', - ] - - for key in v24_frames: - if key in self: - del(self[key]) - - -def delete(filename, delete_v1=True, delete_v2=True): - """Remove tags from a file. - - Keyword arguments: - - * delete_v1 -- delete any ID3v1 tag - * delete_v2 -- delete any ID3v2 tag - """ - - with open(filename, 'rb+') as f: - - if delete_v1: - tag, offset = _find_id3v1(f) - if tag is not None: - f.seek(offset, 2) - f.truncate() - - # technically an insize=0 tag is invalid, but we delete it anyway - # (primarily because we used to write it) - if delete_v2: - f.seek(0, 0) - idata = f.read(10) - try: - id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) - except struct.error: - id3, insize = b'', -1 - insize = BitPaddedInt(insize) - if id3 == b'ID3' and insize >= 0: - delete_bytes(f, insize + 10, 0) - - -# support open(filename) as interface -Open = ID3 - - -def _determine_bpi(data, frames, EMPTY=b"\x00" * 10): - """Takes id3v2.4 frame data and determines if ints or bitpaddedints - should be used for parsing. Needed because iTunes used to write - normal ints for frame sizes. - """ - - # count number of tags found as BitPaddedInt and how far past - o = 0 - asbpi = 0 - while o < len(data) - 10: - part = data[o:o + 10] - if part == EMPTY: - bpioff = -((len(data) - o) % 10) - break - name, size, flags = unpack('>4sLH', part) - size = BitPaddedInt(size) - o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue - if name in frames: - asbpi += 1 - else: - bpioff = o - len(data) - - # count number of tags found as int and how far past - o = 0 - asint = 0 - while o < len(data) - 10: - part = data[o:o + 10] - if part == EMPTY: - intoff = -((len(data) - o) % 10) - break - name, size, flags = unpack('>4sLH', part) - o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue - if name in frames: - asint += 1 - else: - intoff = o - len(data) - - # if more tags as int, or equal and bpi is past and int is not - if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)): - return int - return BitPaddedInt - - -def _find_id3v1(fileobj): - """Returns a tuple of (id3tag, offset_to_end) or (None, 0) - - offset mainly because we used to write too short tags in some cases and - we need the offset to delete them. - """ - - # id3v1 is always at the end (after apev2) - - extra_read = b"APETAGEX".index(b"TAG") - - try: - fileobj.seek(-128 - extra_read, 2) - except IOError as e: - if e.errno == errno.EINVAL: - # If the file is too small, might be ok since we wrote too small - # tags at some point. let's see how the parsing goes.. - fileobj.seek(0, 0) - else: - raise - - data = fileobj.read(128 + extra_read) - try: - idx = data.index(b"TAG") - except ValueError: - return (None, 0) - else: - # FIXME: make use of the apev2 parser here - # if TAG is part of APETAGEX assume this is an APEv2 tag - try: - ape_idx = data.index(b"APETAGEX") - except ValueError: - pass - else: - if idx == ape_idx + extra_read: - return (None, 0) - - tag = ParseID3v1(data[idx:]) - if tag is None: - return (None, 0) - - offset = idx - len(data) - return (tag, offset) - - -# ID3v1.1 support. -def ParseID3v1(data): - """Parse an ID3v1 tag, returning a list of ID3v2.4 frames. - - Returns a {frame_name: frame} dict or None. - """ - - try: - data = data[data.index(b"TAG"):] - except ValueError: - return None - if 128 < len(data) or len(data) < 124: - return None - - # Issue #69 - Previous versions of Mutagen, when encountering - # out-of-spec TDRC and TYER frames of less than four characters, - # wrote only the characters available - e.g. "1" or "" - into the - # year field. To parse those, reduce the size of the year field. - # Amazingly, "0s" works as a struct format string. - unpack_fmt = "3s30s30s30s%ds29sBB" % (len(data) - 124) - - try: - tag, title, artist, album, year, comment, track, genre = unpack( - unpack_fmt, data) - except StructError: - return None - - if tag != b"TAG": - return None - - def fix(data): - return data.split(b"\x00")[0].strip().decode('latin1') - - title, artist, album, year, comment = map( - fix, [title, artist, album, year, comment]) - - frames = {} - if title: - frames["TIT2"] = TIT2(encoding=0, text=title) - if artist: - frames["TPE1"] = TPE1(encoding=0, text=[artist]) - if album: - frames["TALB"] = TALB(encoding=0, text=album) - if year: - frames["TDRC"] = TDRC(encoding=0, text=year) - if comment: - frames["COMM"] = COMM( - encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) - # Don't read a track number if it looks like the comment was - # padded with spaces instead of nulls (thanks, WinAmp). - if track and ((track != 32) or (data[-3] == b'\x00'[0])): - frames["TRCK"] = TRCK(encoding=0, text=str(track)) - if genre != 255: - frames["TCON"] = TCON(encoding=0, text=str(genre)) - return frames - - -def MakeID3v1(id3): - """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames.""" - - v1 = {} - - for v2id, name in {"TIT2": "title", "TPE1": "artist", - "TALB": "album"}.items(): - if v2id in id3: - text = id3[v2id].text[0].encode('latin1', 'replace')[:30] - else: - text = b"" - v1[name] = text + (b"\x00" * (30 - len(text))) - - if "COMM" in id3: - cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] - else: - cmnt = b"" - v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt))) - - if "TRCK" in id3: - try: - v1["track"] = chr_(+id3["TRCK"]) - except ValueError: - v1["track"] = b"\x00" - else: - v1["track"] = b"\x00" - - if "TCON" in id3: - try: - genre = id3["TCON"].genres[0] - except IndexError: - pass - else: - if genre in TCON.GENRES: - v1["genre"] = chr_(TCON.GENRES.index(genre)) - if "genre" not in v1: - v1["genre"] = b"\xff" - - if "TDRC" in id3: - year = text_type(id3["TDRC"]).encode('ascii') - elif "TYER" in id3: - year = text_type(id3["TYER"]).encode('ascii') - else: - year = b"" - v1["year"] = (year + b"\x00\x00\x00\x00")[:4] - - return ( - b"TAG" + - v1["title"] + - v1["artist"] + - v1["album"] + - v1["year"] + - v1["comment"] + - v1["track"] + - v1["genre"] - ) - - -class ID3FileType(mutagen.FileType): - """An unknown type of file with ID3 tags.""" - - ID3 = ID3 - - class _Info(mutagen.StreamInfo): - length = 0 - - def __init__(self, fileobj, offset): - pass - - @staticmethod - def pprint(): - return "Unknown format with ID3 tag" - - @staticmethod - def score(filename, fileobj, header_data): - return header_data.startswith(b"ID3") - - def add_tags(self, ID3=None): - """Add an empty ID3 tag to the file. - - A custom tag reader may be used in instead of the default - mutagen.id3.ID3 object, e.g. an EasyID3 reader. - """ - if ID3 is None: - ID3 = self.ID3 - if self.tags is None: - self.ID3 = ID3 - self.tags = ID3() - else: - raise error("an ID3 tag already exists") - - def load(self, filename, ID3=None, **kwargs): - """Load stream and tag information from a file. - - A custom tag reader may be used in instead of the default - mutagen.id3.ID3 object, e.g. an EasyID3 reader. - """ - - if ID3 is None: - ID3 = self.ID3 - else: - # If this was initialized with EasyID3, remember that for - # when tags are auto-instantiated in add_tags. - self.ID3 = ID3 - self.filename = filename - try: - self.tags = ID3(filename, **kwargs) - except ID3NoHeaderError: - self.tags = None - - if self.tags is not None: - try: - offset = self.tags.size - except AttributeError: - offset = None - else: - offset = None - - with open(filename, "rb") as fileobj: - self.info = self._Info(fileobj, offset) diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index f423db1af7e6371db8a6eea5d505a35cb1aee338..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27785 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 diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc deleted file mode 100644 index 0331df593173cd69e7761b5b023fe931ec86c0b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64560 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 diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc deleted file mode 100644 index 9af16aa1ca17196b9bf38ccd837732df328d8b9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22816 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 bC^ysnQE2@B+4Mj<mL5&Nls=g5PpAJcimw4r diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc deleted file mode 100644 index c2c75dadb9a532645a03141da6396906431288b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4933 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 diff --git a/resources/lib/libraries/mutagen/id3/_frames.py b/resources/lib/libraries/mutagen/id3/_frames.py deleted file mode 100644 index c185cef3..00000000 --- a/resources/lib/libraries/mutagen/id3/_frames.py +++ /dev/null @@ -1,1925 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Michael Urman -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -import zlib -from struct import unpack - -from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch -from ._specs import ( - BinaryDataSpec, StringSpec, Latin1TextSpec, EncodedTextSpec, ByteSpec, - EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, IntegerSpec, - VolumeAdjustmentsSpec, VolumePeakSpec, VolumeAdjustmentSpec, - ChannelSpec, MultiSpec, SynchronizedTextSpec, KeyEventSpec, TimeStampSpec, - EncodedNumericPartTextSpec, EncodedNumericTextSpec, SpecError) -from .._compat import text_type, string_types, swap_to_string, iteritems, izip - - -def is_valid_frame_id(frame_id): - return frame_id.isalnum() and frame_id.isupper() - - -def _bytes2key(b): - assert isinstance(b, bytes) - - return b.decode("latin1") - - -class Frame(object): - """Fundamental unit of ID3 data. - - ID3 tags are split into frames. Each frame has a potentially - different structure, and so this base class is not very featureful. - """ - - FLAG23_ALTERTAG = 0x8000 - FLAG23_ALTERFILE = 0x4000 - FLAG23_READONLY = 0x2000 - FLAG23_COMPRESS = 0x0080 - FLAG23_ENCRYPT = 0x0040 - FLAG23_GROUP = 0x0020 - - FLAG24_ALTERTAG = 0x4000 - FLAG24_ALTERFILE = 0x2000 - FLAG24_READONLY = 0x1000 - FLAG24_GROUPID = 0x0040 - FLAG24_COMPRESS = 0x0008 - FLAG24_ENCRYPT = 0x0004 - FLAG24_UNSYNCH = 0x0002 - FLAG24_DATALEN = 0x0001 - - _framespec = [] - - def __init__(self, *args, **kwargs): - if len(args) == 1 and len(kwargs) == 0 and \ - isinstance(args[0], type(self)): - other = args[0] - # ask the sub class to fill in our data - other._to_other(self) - else: - for checker, val in izip(self._framespec, args): - setattr(self, checker.name, checker.validate(self, val)) - for checker in self._framespec[len(args):]: - try: - validated = checker.validate( - self, kwargs.get(checker.name, None)) - except ValueError as e: - raise ValueError("%s: %s" % (checker.name, e)) - setattr(self, checker.name, validated) - - def _to_other(self, other): - # this impl covers subclasses with the same framespec - if other._framespec is not self._framespec: - raise ValueError - - for checker in other._framespec: - setattr(other, checker.name, getattr(self, checker.name)) - - def _get_v23_frame(self, **kwargs): - """Returns a frame copy which is suitable for writing into a v2.3 tag. - - kwargs get passed to the specs. - """ - - new_kwargs = {} - for checker in self._framespec: - name = checker.name - value = getattr(self, name) - new_kwargs[name] = checker._validate23(self, value, **kwargs) - return type(self)(**new_kwargs) - - @property - def HashKey(self): - """An internal key used to ensure frame uniqueness in a tag""" - - return self.FrameID - - @property - def FrameID(self): - """ID3v2 three or four character frame ID""" - - return type(self).__name__ - - def __repr__(self): - """Python representation of a frame. - - The string returned is a valid Python expression to construct - a copy of this frame. - """ - kw = [] - for attr in self._framespec: - # so repr works during __init__ - if hasattr(self, attr.name): - kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) - return '%s(%s)' % (type(self).__name__, ', '.join(kw)) - - def _readData(self, data): - """Raises ID3JunkFrameError; Returns leftover data""" - - for reader in self._framespec: - if len(data): - try: - value, data = reader.read(self, data) - except SpecError as e: - raise ID3JunkFrameError(e) - else: - raise ID3JunkFrameError("no data left") - setattr(self, reader.name, value) - - return data - - def _writeData(self): - data = [] - for writer in self._framespec: - data.append(writer.write(self, getattr(self, writer.name))) - return b''.join(data) - - def pprint(self): - """Return a human-readable representation of the frame.""" - return "%s=%s" % (type(self).__name__, self._pprint()) - - def _pprint(self): - return "[unrepresentable data]" - - @classmethod - def _fromData(cls, id3, tflags, data): - """Construct this ID3 frame from raw string data. - - Raises: - - ID3JunkFrameError in case parsing failed - NotImplementedError in case parsing isn't implemented - ID3EncryptionUnsupportedError in case the frame is encrypted. - """ - - if id3.version >= id3._V24: - if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN): - # The data length int is syncsafe in 2.4 (but not 2.3). - # However, we don't actually need the data length int, - # except to work around a QL 0.12 bug, and in that case - # all we need are the raw bytes. - datalen_bytes = data[:4] - data = data[4:] - if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch: - try: - data = unsynch.decode(data) - except ValueError: - # Some things write synch-unsafe data with either the frame - # or global unsynch flag set. Try to load them as is. - # https://bitbucket.org/lazka/mutagen/issue/210 - # https://bitbucket.org/lazka/mutagen/issue/223 - pass - if tflags & Frame.FLAG24_ENCRYPT: - raise ID3EncryptionUnsupportedError - if tflags & Frame.FLAG24_COMPRESS: - try: - data = zlib.decompress(data) - except zlib.error as err: - # the initial mutagen that went out with QL 0.12 did not - # write the 4 bytes of uncompressed size. Compensate. - data = datalen_bytes + data - try: - data = zlib.decompress(data) - except zlib.error as err: - raise ID3JunkFrameError( - 'zlib: %s: %r' % (err, data)) - - elif id3.version >= id3._V23: - if tflags & Frame.FLAG23_COMPRESS: - usize, = unpack('>L', data[:4]) - data = data[4:] - if tflags & Frame.FLAG23_ENCRYPT: - raise ID3EncryptionUnsupportedError - if tflags & Frame.FLAG23_COMPRESS: - try: - data = zlib.decompress(data) - except zlib.error as err: - raise ID3JunkFrameError('zlib: %s: %r' % (err, data)) - - frame = cls() - frame._readData(data) - return frame - - def __hash__(self): - raise TypeError("Frame objects are unhashable") - - -class FrameOpt(Frame): - """A frame with optional parts. - - Some ID3 frames have optional data; this class extends Frame to - provide support for those parts. - """ - - _optionalspec = [] - - def __init__(self, *args, **kwargs): - super(FrameOpt, self).__init__(*args, **kwargs) - for spec in self._optionalspec: - if spec.name in kwargs: - validated = spec.validate(self, kwargs[spec.name]) - setattr(self, spec.name, validated) - else: - break - - def _to_other(self, other): - super(FrameOpt, self)._to_other(other) - - # this impl covers subclasses with the same optionalspec - if other._optionalspec is not self._optionalspec: - raise ValueError - - for checker in other._optionalspec: - if hasattr(self, checker.name): - setattr(other, checker.name, getattr(self, checker.name)) - - def _readData(self, data): - """Raises ID3JunkFrameError; Returns leftover data""" - - for reader in self._framespec: - if len(data): - try: - value, data = reader.read(self, data) - except SpecError as e: - raise ID3JunkFrameError(e) - else: - raise ID3JunkFrameError("no data left") - setattr(self, reader.name, value) - - if data: - for reader in self._optionalspec: - if len(data): - try: - value, data = reader.read(self, data) - except SpecError as e: - raise ID3JunkFrameError(e) - else: - break - setattr(self, reader.name, value) - - return data - - def _writeData(self): - data = [] - for writer in self._framespec: - data.append(writer.write(self, getattr(self, writer.name))) - for writer in self._optionalspec: - try: - data.append(writer.write(self, getattr(self, writer.name))) - except AttributeError: - break - return b''.join(data) - - def __repr__(self): - kw = [] - for attr in self._framespec: - kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) - for attr in self._optionalspec: - if hasattr(self, attr.name): - kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) - return '%s(%s)' % (type(self).__name__, ', '.join(kw)) - - -@swap_to_string -class TextFrame(Frame): - """Text strings. - - Text frames support casts to unicode or str objects, as well as - list-like indexing, extend, and append. - - Iterating over a TextFrame iterates over its strings, not its - characters. - - Text frames have a 'text' attribute which is the list of strings, - and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for - UTF-16BE, and 3 for UTF-8. If you don't want to worry about - encodings, just set it to 3. - """ - - _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), - ] - - def __bytes__(self): - return text_type(self).encode('utf-8') - - def __str__(self): - return u'\u0000'.join(self.text) - - def __eq__(self, other): - if isinstance(other, bytes): - return bytes(self) == other - elif isinstance(other, text_type): - return text_type(self) == other - return self.text == other - - __hash__ = Frame.__hash__ - - def __getitem__(self, item): - return self.text[item] - - def __iter__(self): - return iter(self.text) - - def append(self, value): - """Append a string.""" - - return self.text.append(value) - - def extend(self, value): - """Extend the list by appending all strings from the given list.""" - - return self.text.extend(value) - - def _pprint(self): - return " / ".join(self.text) - - -class NumericTextFrame(TextFrame): - """Numerical text strings. - - The numeric value of these frames can be gotten with unary plus, e.g.:: - - frame = TLEN('12345') - length = +frame - """ - - _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000'), - ] - - def __pos__(self): - """Return the numerical value of the string.""" - return int(self.text[0]) - - -class NumericPartTextFrame(TextFrame): - """Multivalue numerical text strings. - - These strings indicate 'part (e.g. track) X of Y', and unary plus - returns the first value:: - - frame = TRCK('4/15') - track = +frame # track == 4 - """ - - _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000'), - ] - - def __pos__(self): - return int(self.text[0].split("/")[0]) - - -@swap_to_string -class TimeStampTextFrame(TextFrame): - """A list of time stamps. - - The 'text' attribute in this frame is a list of ID3TimeStamp - objects, not a list of strings. - """ - - _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', TimeStampSpec('stamp'), sep=u','), - ] - - def __bytes__(self): - return text_type(self).encode('utf-8') - - def __str__(self): - return u','.join([stamp.text for stamp in self.text]) - - def _pprint(self): - return u" / ".join([stamp.text for stamp in self.text]) - - -@swap_to_string -class UrlFrame(Frame): - """A frame containing a URL string. - - The ID3 specification is silent about IRIs and normalized URL - forms. Mutagen assumes all URLs in files are encoded as Latin 1, - but string conversion of this frame returns a UTF-8 representation - for compatibility with other string conversions. - - The only sane way to handle URLs in MP3s is to restrict them to - ASCII. - """ - - _framespec = [Latin1TextSpec('url')] - - def __bytes__(self): - return self.url.encode('utf-8') - - def __str__(self): - return self.url - - def __eq__(self, other): - return self.url == other - - __hash__ = Frame.__hash__ - - def _pprint(self): - return self.url - - -class UrlFrameU(UrlFrame): - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.url) - - -class TALB(TextFrame): - "Album" - - -class TBPM(NumericTextFrame): - "Beats per minute" - - -class TCOM(TextFrame): - "Composer" - - -class TCON(TextFrame): - """Content type (Genre) - - ID3 has several ways genres can be represented; for convenience, - use the 'genres' property rather than the 'text' attribute. - """ - - from mutagen._constants import GENRES - GENRES = GENRES - - def __get_genres(self): - genres = [] - import re - genre_re = re.compile(r"((?:\((?P<id>[0-9]+|RX|CR)\))*)(?P<str>.+)?") - for value in self.text: - # 255 possible entries in id3v1 - if value.isdigit() and int(value) < 256: - try: - genres.append(self.GENRES[int(value)]) - except IndexError: - genres.append(u"Unknown") - elif value == "CR": - genres.append(u"Cover") - elif value == "RX": - genres.append(u"Remix") - elif value: - newgenres = [] - genreid, dummy, genrename = genre_re.match(value).groups() - - if genreid: - for gid in genreid[1:-1].split(")("): - if gid.isdigit() and int(gid) < len(self.GENRES): - gid = text_type(self.GENRES[int(gid)]) - newgenres.append(gid) - elif gid == "CR": - newgenres.append(u"Cover") - elif gid == "RX": - newgenres.append(u"Remix") - else: - newgenres.append(u"Unknown") - - if genrename: - # "Unescaping" the first parenthesis - if genrename.startswith("(("): - genrename = genrename[1:] - if genrename not in newgenres: - newgenres.append(genrename) - - genres.extend(newgenres) - - return genres - - def __set_genres(self, genres): - if isinstance(genres, string_types): - genres = [genres] - self.text = [self.__decode(g) for g in genres] - - def __decode(self, value): - if isinstance(value, bytes): - enc = EncodedTextSpec._encodings[self.encoding][0] - return value.decode(enc) - else: - return value - - genres = property(__get_genres, __set_genres, None, - "A list of genres parsed from the raw text data.") - - def _pprint(self): - return " / ".join(self.genres) - - -class TCOP(TextFrame): - "Copyright (c)" - - -class TCMP(NumericTextFrame): - "iTunes Compilation Flag" - - -class TDAT(TextFrame): - "Date of recording (DDMM)" - - -class TDEN(TimeStampTextFrame): - "Encoding Time" - - -class TDES(TextFrame): - "iTunes Podcast Description" - - -class TDOR(TimeStampTextFrame): - "Original Release Time" - - -class TDLY(NumericTextFrame): - "Audio Delay (ms)" - - -class TDRC(TimeStampTextFrame): - "Recording Time" - - -class TDRL(TimeStampTextFrame): - "Release Time" - - -class TDTG(TimeStampTextFrame): - "Tagging Time" - - -class TENC(TextFrame): - "Encoder" - - -class TEXT(TextFrame): - "Lyricist" - - -class TFLT(TextFrame): - "File type" - - -class TGID(TextFrame): - "iTunes Podcast Identifier" - - -class TIME(TextFrame): - "Time of recording (HHMM)" - - -class TIT1(TextFrame): - "Content group description" - - -class TIT2(TextFrame): - "Title" - - -class TIT3(TextFrame): - "Subtitle/Description refinement" - - -class TKEY(TextFrame): - "Starting Key" - - -class TLAN(TextFrame): - "Audio Languages" - - -class TLEN(NumericTextFrame): - "Audio Length (ms)" - - -class TMED(TextFrame): - "Source Media Type" - - -class TMOO(TextFrame): - "Mood" - - -class TOAL(TextFrame): - "Original Album" - - -class TOFN(TextFrame): - "Original Filename" - - -class TOLY(TextFrame): - "Original Lyricist" - - -class TOPE(TextFrame): - "Original Artist/Performer" - - -class TORY(NumericTextFrame): - "Original Release Year" - - -class TOWN(TextFrame): - "Owner/Licensee" - - -class TPE1(TextFrame): - "Lead Artist/Performer/Soloist/Group" - - -class TPE2(TextFrame): - "Band/Orchestra/Accompaniment" - - -class TPE3(TextFrame): - "Conductor" - - -class TPE4(TextFrame): - "Interpreter/Remixer/Modifier" - - -class TPOS(NumericPartTextFrame): - "Part of set" - - -class TPRO(TextFrame): - "Produced (P)" - - -class TPUB(TextFrame): - "Publisher" - - -class TRCK(NumericPartTextFrame): - "Track Number" - - -class TRDA(TextFrame): - "Recording Dates" - - -class TRSN(TextFrame): - "Internet Radio Station Name" - - -class TRSO(TextFrame): - "Internet Radio Station Owner" - - -class TSIZ(NumericTextFrame): - "Size of audio data (bytes)" - - -class TSO2(TextFrame): - "iTunes Album Artist Sort" - - -class TSOA(TextFrame): - "Album Sort Order key" - - -class TSOC(TextFrame): - "iTunes Composer Sort" - - -class TSOP(TextFrame): - "Perfomer Sort Order key" - - -class TSOT(TextFrame): - "Title Sort Order key" - - -class TSRC(TextFrame): - "International Standard Recording Code (ISRC)" - - -class TSSE(TextFrame): - "Encoder settings" - - -class TSST(TextFrame): - "Set Subtitle" - - -class TYER(NumericTextFrame): - "Year of recording" - - -class TXXX(TextFrame): - """User-defined text data. - - TXXX frames have a 'desc' attribute which is set to any Unicode - value (though the encoding of the text and the description must be - the same). Many taggers use this frame to store freeform keys. - """ - - _framespec = [ - EncodingSpec('encoding'), - EncodedTextSpec('desc'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - def _pprint(self): - return "%s=%s" % (self.desc, " / ".join(self.text)) - - -class WCOM(UrlFrameU): - "Commercial Information" - - -class WCOP(UrlFrame): - "Copyright Information" - - -class WFED(UrlFrame): - "iTunes Podcast Feed" - - -class WOAF(UrlFrame): - "Official File Information" - - -class WOAR(UrlFrameU): - "Official Artist/Performer Information" - - -class WOAS(UrlFrame): - "Official Source Information" - - -class WORS(UrlFrame): - "Official Internet Radio Information" - - -class WPAY(UrlFrame): - "Payment Information" - - -class WPUB(UrlFrame): - "Official Publisher Information" - - -class WXXX(UrlFrame): - """User-defined URL data. - - Like TXXX, this has a freeform description associated with it. - """ - - _framespec = [ - EncodingSpec('encoding'), - EncodedTextSpec('desc'), - Latin1TextSpec('url'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - -class PairedTextFrame(Frame): - """Paired text strings. - - Some ID3 frames pair text strings, to associate names with a more - specific involvement in the song. The 'people' attribute of these - frames contains a list of pairs:: - - [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']] - - Like text frames, these frames also have an encoding attribute. - """ - - _framespec = [ - EncodingSpec('encoding'), - MultiSpec('people', - EncodedTextSpec('involvement'), - EncodedTextSpec('person')) - ] - - def __eq__(self, other): - return self.people == other - - __hash__ = Frame.__hash__ - - -class TIPL(PairedTextFrame): - "Involved People List" - - -class TMCL(PairedTextFrame): - "Musicians Credits List" - - -class IPLS(TIPL): - "Involved People List" - - -class BinaryFrame(Frame): - """Binary data - - The 'data' attribute contains the raw byte string. - """ - - _framespec = [BinaryDataSpec('data')] - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - -class MCDI(BinaryFrame): - "Binary dump of CD's TOC" - - -class ETCO(Frame): - """Event timing codes.""" - - _framespec = [ - ByteSpec("format"), - KeyEventSpec("events"), - ] - - def __eq__(self, other): - return self.events == other - - __hash__ = Frame.__hash__ - - -class MLLT(Frame): - """MPEG location lookup table. - - This frame's attributes may be changed in the future based on - feedback from real-world use. - """ - - _framespec = [ - SizedIntegerSpec('frames', 2), - SizedIntegerSpec('bytes', 3), - SizedIntegerSpec('milliseconds', 3), - ByteSpec('bits_for_bytes'), - ByteSpec('bits_for_milliseconds'), - BinaryDataSpec('data'), - ] - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - -class SYTC(Frame): - """Synchronised tempo codes. - - This frame's attributes may be changed in the future based on - feedback from real-world use. - """ - - _framespec = [ - ByteSpec("format"), - BinaryDataSpec("data"), - ] - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - -@swap_to_string -class USLT(Frame): - """Unsynchronised lyrics/text transcription. - - Lyrics have a three letter ISO language code ('lang'), a - description ('desc'), and a block of plain text ('text'). - """ - - _framespec = [ - EncodingSpec('encoding'), - StringSpec('lang', 3), - EncodedTextSpec('desc'), - EncodedTextSpec('text'), - ] - - @property - def HashKey(self): - return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) - - def __bytes__(self): - return self.text.encode('utf-8') - - def __str__(self): - return self.text - - def __eq__(self, other): - return self.text == other - - __hash__ = Frame.__hash__ - - -@swap_to_string -class SYLT(Frame): - """Synchronised lyrics/text.""" - - _framespec = [ - EncodingSpec('encoding'), - StringSpec('lang', 3), - ByteSpec('format'), - ByteSpec('type'), - EncodedTextSpec('desc'), - SynchronizedTextSpec('text'), - ] - - @property - def HashKey(self): - return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) - - def __eq__(self, other): - return str(self) == other - - __hash__ = Frame.__hash__ - - def __str__(self): - return u"".join(text for (text, time) in self.text) - - def __bytes__(self): - return text_type(self).encode("utf-8") - - -class COMM(TextFrame): - """User comment. - - User comment frames have a descrption, like TXXX, and also a three - letter ISO language code in the 'lang' attribute. - """ - - _framespec = [ - EncodingSpec('encoding'), - StringSpec('lang', 3), - EncodedTextSpec('desc'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), - ] - - @property - def HashKey(self): - return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) - - def _pprint(self): - return "%s=%s=%s" % (self.desc, self.lang, " / ".join(self.text)) - - -class RVA2(Frame): - """Relative volume adjustment (2). - - This frame is used to implemented volume scaling, and in - particular, normalization using ReplayGain. - - Attributes: - - * desc -- description or context of this adjustment - * channel -- audio channel to adjust (master is 1) - * gain -- a + or - dB gain relative to some reference level - * peak -- peak of the audio as a floating point number, [0, 1] - - When storing ReplayGain tags, use descriptions of 'album' and - 'track' on channel 1. - """ - - _framespec = [ - Latin1TextSpec('desc'), - ChannelSpec('channel'), - VolumeAdjustmentSpec('gain'), - VolumePeakSpec('peak'), - ] - - _channels = ["Other", "Master volume", "Front right", "Front left", - "Back right", "Back left", "Front centre", "Back centre", - "Subwoofer"] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - def __eq__(self, other): - try: - return ((str(self) == other) or - (self.desc == other.desc and - self.channel == other.channel and - self.gain == other.gain and - self.peak == other.peak)) - except AttributeError: - return False - - __hash__ = Frame.__hash__ - - def __str__(self): - return "%s: %+0.4f dB/%0.4f" % ( - self._channels[self.channel], self.gain, self.peak) - - -class EQU2(Frame): - """Equalisation (2). - - Attributes: - method -- interpolation method (0 = band, 1 = linear) - desc -- identifying description - adjustments -- list of (frequency, vol_adjustment) pairs - """ - - _framespec = [ - ByteSpec("method"), - Latin1TextSpec("desc"), - VolumeAdjustmentsSpec("adjustments"), - ] - - def __eq__(self, other): - return self.adjustments == other - - __hash__ = Frame.__hash__ - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - -# class RVAD: unsupported -# class EQUA: unsupported - - -class RVRB(Frame): - """Reverb.""" - - _framespec = [ - SizedIntegerSpec('left', 2), - SizedIntegerSpec('right', 2), - ByteSpec('bounce_left'), - ByteSpec('bounce_right'), - ByteSpec('feedback_ltl'), - ByteSpec('feedback_ltr'), - ByteSpec('feedback_rtr'), - ByteSpec('feedback_rtl'), - ByteSpec('premix_ltr'), - ByteSpec('premix_rtl'), - ] - - def __eq__(self, other): - return (self.left, self.right) == other - - __hash__ = Frame.__hash__ - - -class APIC(Frame): - """Attached (or linked) Picture. - - Attributes: - - * encoding -- text encoding for the description - * mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI - * type -- the source of the image (3 is the album front cover) - * desc -- a text description of the image - * data -- raw image data, as a byte string - - Mutagen will automatically compress large images when saving tags. - """ - - _framespec = [ - EncodingSpec('encoding'), - Latin1TextSpec('mime'), - ByteSpec('type'), - EncodedTextSpec('desc'), - BinaryDataSpec('data'), - ] - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - def _validate_from_22(self, other, checker): - if checker.name == "mime": - self.mime = other.mime.decode("ascii", "ignore") - else: - super(APIC, self)._validate_from_22(other, checker) - - def _pprint(self): - return "%s (%s, %d bytes)" % ( - self.desc, self.mime, len(self.data)) - - -class PCNT(Frame): - """Play counter. - - The 'count' attribute contains the (recorded) number of times this - file has been played. - - This frame is basically obsoleted by POPM. - """ - - _framespec = [IntegerSpec('count')] - - def __eq__(self, other): - return self.count == other - - __hash__ = Frame.__hash__ - - def __pos__(self): - return self.count - - def _pprint(self): - return text_type(self.count) - - -class POPM(FrameOpt): - """Popularimeter. - - This frame keys a rating (out of 255) and a play count to an email - address. - - Attributes: - - * email -- email this POPM frame is for - * rating -- rating from 0 to 255 - * count -- number of times the files has been played (optional) - """ - - _framespec = [ - Latin1TextSpec('email'), - ByteSpec('rating'), - ] - - _optionalspec = [IntegerSpec('count')] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.email) - - def __eq__(self, other): - return self.rating == other - - __hash__ = FrameOpt.__hash__ - - def __pos__(self): - return self.rating - - def _pprint(self): - return "%s=%r %r/255" % ( - self.email, getattr(self, 'count', None), self.rating) - - -class GEOB(Frame): - """General Encapsulated Object. - - A blob of binary data, that is not a picture (those go in APIC). - - Attributes: - - * encoding -- encoding of the description - * mime -- MIME type of the data or '-->' if the data is a URI - * filename -- suggested filename if extracted - * desc -- text description of the data - * data -- raw data, as a byte string - """ - - _framespec = [ - EncodingSpec('encoding'), - Latin1TextSpec('mime'), - EncodedTextSpec('filename'), - EncodedTextSpec('desc'), - BinaryDataSpec('data'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.desc) - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - -class RBUF(FrameOpt): - """Recommended buffer size. - - Attributes: - - * size -- recommended buffer size in bytes - * info -- if ID3 tags may be elsewhere in the file (optional) - * offset -- the location of the next ID3 tag, if any - - Mutagen will not find the next tag itself. - """ - - _framespec = [SizedIntegerSpec('size', 3)] - - _optionalspec = [ - ByteSpec('info'), - SizedIntegerSpec('offset', 4), - ] - - def __eq__(self, other): - return self.size == other - - __hash__ = FrameOpt.__hash__ - - def __pos__(self): - return self.size - - -@swap_to_string -class AENC(FrameOpt): - """Audio encryption. - - Attributes: - - * owner -- key identifying this encryption type - * preview_start -- unencrypted data block offset - * preview_length -- number of unencrypted blocks - * data -- data required for decryption (optional) - - Mutagen cannot decrypt files. - """ - - _framespec = [ - Latin1TextSpec('owner'), - SizedIntegerSpec('preview_start', 2), - SizedIntegerSpec('preview_length', 2), - ] - - _optionalspec = [BinaryDataSpec('data')] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.owner) - - def __bytes__(self): - return self.owner.encode('utf-8') - - def __str__(self): - return self.owner - - def __eq__(self, other): - return self.owner == other - - __hash__ = FrameOpt.__hash__ - - -class LINK(FrameOpt): - """Linked information. - - Attributes: - - * frameid -- the ID of the linked frame - * url -- the location of the linked frame - * data -- further ID information for the frame - """ - - _framespec = [ - StringSpec('frameid', 4), - Latin1TextSpec('url'), - ] - - _optionalspec = [BinaryDataSpec('data')] - - @property - def HashKey(self): - try: - return "%s:%s:%s:%s" % ( - self.FrameID, self.frameid, self.url, _bytes2key(self.data)) - except AttributeError: - return "%s:%s:%s" % (self.FrameID, self.frameid, self.url) - - def __eq__(self, other): - try: - return (self.frameid, self.url, self.data) == other - except AttributeError: - return (self.frameid, self.url) == other - - __hash__ = FrameOpt.__hash__ - - -class POSS(Frame): - """Position synchronisation frame - - Attribute: - - * format -- format of the position attribute (frames or milliseconds) - * position -- current position of the file - """ - - _framespec = [ - ByteSpec('format'), - IntegerSpec('position'), - ] - - def __pos__(self): - return self.position - - def __eq__(self, other): - return self.position == other - - __hash__ = Frame.__hash__ - - -class UFID(Frame): - """Unique file identifier. - - Attributes: - - * owner -- format/type of identifier - * data -- identifier - """ - - _framespec = [ - Latin1TextSpec('owner'), - BinaryDataSpec('data'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.owner) - - def __eq__(s, o): - if isinstance(o, UFI): - return s.owner == o.owner and s.data == o.data - else: - return s.data == o - - __hash__ = Frame.__hash__ - - def _pprint(self): - return "%s=%r" % (self.owner, self.data) - - -@swap_to_string -class USER(Frame): - """Terms of use. - - Attributes: - - * encoding -- text encoding - * lang -- ISO three letter language code - * text -- licensing terms for the audio - """ - - _framespec = [ - EncodingSpec('encoding'), - StringSpec('lang', 3), - EncodedTextSpec('text'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.lang) - - def __bytes__(self): - return self.text.encode('utf-8') - - def __str__(self): - return self.text - - def __eq__(self, other): - return self.text == other - - __hash__ = Frame.__hash__ - - def _pprint(self): - return "%r=%s" % (self.lang, self.text) - - -@swap_to_string -class OWNE(Frame): - """Ownership frame.""" - - _framespec = [ - EncodingSpec('encoding'), - Latin1TextSpec('price'), - StringSpec('date', 8), - EncodedTextSpec('seller'), - ] - - def __bytes__(self): - return self.seller.encode('utf-8') - - def __str__(self): - return self.seller - - def __eq__(self, other): - return self.seller == other - - __hash__ = Frame.__hash__ - - -class COMR(FrameOpt): - """Commercial frame.""" - - _framespec = [ - EncodingSpec('encoding'), - Latin1TextSpec('price'), - StringSpec('valid_until', 8), - Latin1TextSpec('contact'), - ByteSpec('format'), - EncodedTextSpec('seller'), - EncodedTextSpec('desc'), - ] - - _optionalspec = [ - Latin1TextSpec('mime'), - BinaryDataSpec('logo'), - ] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, _bytes2key(self._writeData())) - - def __eq__(self, other): - return self._writeData() == other._writeData() - - __hash__ = FrameOpt.__hash__ - - -@swap_to_string -class ENCR(Frame): - """Encryption method registration. - - The standard does not allow multiple ENCR frames with the same owner - or the same method. Mutagen only verifies that the owner is unique. - """ - - _framespec = [ - Latin1TextSpec('owner'), - ByteSpec('method'), - BinaryDataSpec('data'), - ] - - @property - def HashKey(self): - return "%s:%s" % (self.FrameID, self.owner) - - def __bytes__(self): - return self.data - - def __eq__(self, other): - return self.data == other - - __hash__ = Frame.__hash__ - - -@swap_to_string -class GRID(FrameOpt): - """Group identification registration.""" - - _framespec = [ - Latin1TextSpec('owner'), - ByteSpec('group'), - ] - - _optionalspec = [BinaryDataSpec('data')] - - @property - def HashKey(self): - return '%s:%s' % (self.FrameID, self.group) - - def __pos__(self): - return self.group - - def __bytes__(self): - return self.owner.encode('utf-8') - - def __str__(self): - return self.owner - - def __eq__(self, other): - return self.owner == other or self.group == other - - __hash__ = FrameOpt.__hash__ - - -@swap_to_string -class PRIV(Frame): - """Private frame.""" - - _framespec = [ - Latin1TextSpec('owner'), - BinaryDataSpec('data'), - ] - - @property - def HashKey(self): - return '%s:%s:%s' % ( - self.FrameID, self.owner, _bytes2key(self.data)) - - def __bytes__(self): - return self.data - - def __eq__(self, other): - return self.data == other - - def _pprint(self): - return "%s=%r" % (self.owner, self.data) - - __hash__ = Frame.__hash__ - - -@swap_to_string -class SIGN(Frame): - """Signature frame.""" - - _framespec = [ - ByteSpec('group'), - BinaryDataSpec('sig'), - ] - - @property - def HashKey(self): - return '%s:%s:%s' % (self.FrameID, self.group, _bytes2key(self.sig)) - - def __bytes__(self): - return self.sig - - def __eq__(self, other): - return self.sig == other - - __hash__ = Frame.__hash__ - - -class SEEK(Frame): - """Seek frame. - - Mutagen does not find tags at seek offsets. - """ - - _framespec = [IntegerSpec('offset')] - - def __pos__(self): - return self.offset - - def __eq__(self, other): - return self.offset == other - - __hash__ = Frame.__hash__ - - -class ASPI(Frame): - """Audio seek point index. - - Attributes: S, L, N, b, and Fi. For the meaning of these, see - the ID3v2.4 specification. Fi is a list of integers. - """ - _framespec = [ - SizedIntegerSpec("S", 4), - SizedIntegerSpec("L", 4), - SizedIntegerSpec("N", 2), - ByteSpec("b"), - ASPIIndexSpec("Fi"), - ] - - def __eq__(self, other): - return self.Fi == other - - __hash__ = Frame.__hash__ - - -# ID3v2.2 frames -class UFI(UFID): - "Unique File Identifier" - - -class TT1(TIT1): - "Content group description" - - -class TT2(TIT2): - "Title" - - -class TT3(TIT3): - "Subtitle/Description refinement" - - -class TP1(TPE1): - "Lead Artist/Performer/Soloist/Group" - - -class TP2(TPE2): - "Band/Orchestra/Accompaniment" - - -class TP3(TPE3): - "Conductor" - - -class TP4(TPE4): - "Interpreter/Remixer/Modifier" - - -class TCM(TCOM): - "Composer" - - -class TXT(TEXT): - "Lyricist" - - -class TLA(TLAN): - "Audio Language(s)" - - -class TCO(TCON): - "Content Type (Genre)" - - -class TAL(TALB): - "Album" - - -class TPA(TPOS): - "Part of set" - - -class TRK(TRCK): - "Track Number" - - -class TRC(TSRC): - "International Standard Recording Code (ISRC)" - - -class TYE(TYER): - "Year of recording" - - -class TDA(TDAT): - "Date of recording (DDMM)" - - -class TIM(TIME): - "Time of recording (HHMM)" - - -class TRD(TRDA): - "Recording Dates" - - -class TMT(TMED): - "Source Media Type" - - -class TFT(TFLT): - "File Type" - - -class TBP(TBPM): - "Beats per minute" - - -class TCP(TCMP): - "iTunes Compilation Flag" - - -class TCR(TCOP): - "Copyright (C)" - - -class TPB(TPUB): - "Publisher" - - -class TEN(TENC): - "Encoder" - - -class TSS(TSSE): - "Encoder settings" - - -class TOF(TOFN): - "Original Filename" - - -class TLE(TLEN): - "Audio Length (ms)" - - -class TSI(TSIZ): - "Audio Data size (bytes)" - - -class TDY(TDLY): - "Audio Delay (ms)" - - -class TKE(TKEY): - "Starting Key" - - -class TOT(TOAL): - "Original Album" - - -class TOA(TOPE): - "Original Artist/Perfomer" - - -class TOL(TOLY): - "Original Lyricist" - - -class TOR(TORY): - "Original Release Year" - - -class TXX(TXXX): - "User-defined Text" - - -class WAF(WOAF): - "Official File Information" - - -class WAR(WOAR): - "Official Artist/Performer Information" - - -class WAS(WOAS): - "Official Source Information" - - -class WCM(WCOM): - "Commercial Information" - - -class WCP(WCOP): - "Copyright Information" - - -class WPB(WPUB): - "Official Publisher Information" - - -class WXX(WXXX): - "User-defined URL" - - -class IPL(IPLS): - "Involved people list" - - -class MCI(MCDI): - "Binary dump of CD's TOC" - - -class ETC(ETCO): - "Event timing codes" - - -class MLL(MLLT): - "MPEG location lookup table" - - -class STC(SYTC): - "Synced tempo codes" - - -class ULT(USLT): - "Unsychronised lyrics/text transcription" - - -class SLT(SYLT): - "Synchronised lyrics/text" - - -class COM(COMM): - "Comment" - - -# class RVA(RVAD) -# class EQU(EQUA) - - -class REV(RVRB): - "Reverb" - - -class PIC(APIC): - """Attached Picture. - - The 'mime' attribute of an ID3v2.2 attached picture must be either - 'PNG' or 'JPG'. - """ - - _framespec = [ - EncodingSpec('encoding'), - StringSpec('mime', 3), - ByteSpec('type'), - EncodedTextSpec('desc'), - BinaryDataSpec('data') - ] - - def _to_other(self, other): - if not isinstance(other, APIC): - raise TypeError - - other.encoding = self.encoding - other.mime = self.mime - other.type = self.type - other.desc = self.desc - other.data = self.data - - -class GEO(GEOB): - "General Encapsulated Object" - - -class CNT(PCNT): - "Play counter" - - -class POP(POPM): - "Popularimeter" - - -class BUF(RBUF): - "Recommended buffer size" - - -class CRM(Frame): - """Encrypted meta frame""" - _framespec = [Latin1TextSpec('owner'), Latin1TextSpec('desc'), - BinaryDataSpec('data')] - - def __eq__(self, other): - return self.data == other - __hash__ = Frame.__hash__ - - -class CRA(AENC): - "Audio encryption" - - -class LNK(LINK): - """Linked information""" - - _framespec = [ - StringSpec('frameid', 3), - Latin1TextSpec('url') - ] - - _optionalspec = [BinaryDataSpec('data')] - - def _to_other(self, other): - if not isinstance(other, LINK): - raise TypeError - - other.frameid = self.frameid - other.url = self.url - if hasattr(self, "data"): - other.data = self.data - - -Frames = {} -"""All supported ID3v2.3/4 frames, keyed by frame name.""" - - -Frames_2_2 = {} -"""All supported ID3v2.2 frames, keyed by frame name.""" - - -k, v = None, None -for k, v in iteritems(globals()): - if isinstance(v, type) and issubclass(v, Frame): - v.__module__ = "mutagen.id3" - - if len(k) == 3: - Frames_2_2[k] = v - elif len(k) == 4: - Frames[k] = v - -try: - del k - del v -except NameError: - pass diff --git a/resources/lib/libraries/mutagen/id3/_specs.py b/resources/lib/libraries/mutagen/id3/_specs.py deleted file mode 100644 index 4358a65d..00000000 --- a/resources/lib/libraries/mutagen/id3/_specs.py +++ /dev/null @@ -1,635 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Michael Urman -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -import struct -from struct import unpack, pack - -from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \ - xrange -from .._util import total_ordering, decode_terminated, enum, izip -from ._util import BitPaddedInt - - -@enum -class PictureType(object): - """Enumeration of image types defined by the ID3 standard for the APIC - frame, but also reused in WMA/FLAC/VorbisComment. - """ - - OTHER = 0 - """Other""" - - FILE_ICON = 1 - """32x32 pixels 'file icon' (PNG only)""" - - OTHER_FILE_ICON = 2 - """Other file icon""" - - COVER_FRONT = 3 - """Cover (front)""" - - COVER_BACK = 4 - """Cover (back)""" - - LEAFLET_PAGE = 5 - """Leaflet page""" - - MEDIA = 6 - """Media (e.g. label side of CD)""" - - LEAD_ARTIST = 7 - """Lead artist/lead performer/soloist""" - - ARTIST = 8 - """Artist/performer""" - - CONDUCTOR = 9 - """Conductor""" - - BAND = 10 - """Band/Orchestra""" - - COMPOSER = 11 - """Composer""" - - LYRICIST = 12 - """Lyricist/text writer""" - - RECORDING_LOCATION = 13 - """Recording Location""" - - DURING_RECORDING = 14 - """During recording""" - - DURING_PERFORMANCE = 15 - """During performance""" - - SCREEN_CAPTURE = 16 - """Movie/video screen capture""" - - FISH = 17 - """A bright coloured fish""" - - ILLUSTRATION = 18 - """Illustration""" - - BAND_LOGOTYPE = 19 - """Band/artist logotype""" - - PUBLISHER_LOGOTYPE = 20 - """Publisher/Studio logotype""" - - -class SpecError(Exception): - pass - - -class Spec(object): - - def __init__(self, name): - self.name = name - - def __hash__(self): - raise TypeError("Spec objects are unhashable") - - def _validate23(self, frame, value, **kwargs): - """Return a possibly modified value which, if written, - results in valid id3v2.3 data. - """ - - return value - - def read(self, frame, data): - """Returns the (value, left_data) or raises SpecError""" - - raise NotImplementedError - - def write(self, frame, value): - raise NotImplementedError - - def validate(self, frame, value): - """Returns the validated data or raises ValueError/TypeError""" - - raise NotImplementedError - - -class ByteSpec(Spec): - def read(self, frame, data): - return bytearray(data)[0], data[1:] - - def write(self, frame, value): - return chr_(value) - - def validate(self, frame, value): - if value is not None: - chr_(value) - return value - - -class IntegerSpec(Spec): - def read(self, frame, data): - return int(BitPaddedInt(data, bits=8)), b'' - - def write(self, frame, value): - return BitPaddedInt.to_str(value, bits=8, width=-1) - - def validate(self, frame, value): - return value - - -class SizedIntegerSpec(Spec): - def __init__(self, name, size): - self.name, self.__sz = name, size - - def read(self, frame, data): - return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:] - - def write(self, frame, value): - return BitPaddedInt.to_str(value, bits=8, width=self.__sz) - - def validate(self, frame, value): - return value - - -@enum -class Encoding(object): - """Text Encoding""" - - LATIN1 = 0 - """ISO-8859-1""" - - UTF16 = 1 - """UTF-16 with BOM""" - - UTF16BE = 2 - """UTF-16BE without BOM""" - - UTF8 = 3 - """UTF-8""" - - -class EncodingSpec(ByteSpec): - - def read(self, frame, data): - enc, data = super(EncodingSpec, self).read(frame, data) - if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, - Encoding.UTF8): - raise SpecError('Invalid Encoding: %r' % enc) - return enc, data - - def validate(self, frame, value): - if value is None: - return None - if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, - Encoding.UTF8): - raise ValueError('Invalid Encoding: %r' % value) - return value - - def _validate23(self, frame, value, **kwargs): - # only 0, 1 are valid in v2.3, default to utf-16 - if value not in (Encoding.LATIN1, Encoding.UTF16): - value = Encoding.UTF16 - return value - - -class StringSpec(Spec): - """A fixed size ASCII only payload.""" - - def __init__(self, name, length): - super(StringSpec, self).__init__(name) - self.len = length - - def read(s, frame, data): - chunk = data[:s.len] - try: - ascii = chunk.decode("ascii") - except UnicodeDecodeError: - raise SpecError("not ascii") - else: - if PY3: - chunk = ascii - - return chunk, data[s.len:] - - def write(s, frame, value): - if value is None: - return b'\x00' * s.len - else: - if PY3: - value = value.encode("ascii") - return (bytes(value) + b'\x00' * s.len)[:s.len] - - def validate(s, frame, value): - if value is None: - return None - - if PY3: - if not isinstance(value, str): - raise TypeError("%s has to be str" % s.name) - value.encode("ascii") - else: - if not isinstance(value, bytes): - value = value.encode("ascii") - - if len(value) == s.len: - return value - - raise ValueError('Invalid StringSpec[%d] data: %r' % (s.len, value)) - - -class BinaryDataSpec(Spec): - def read(self, frame, data): - return data, b'' - - def write(self, frame, value): - if value is None: - return b"" - if isinstance(value, bytes): - return value - value = text_type(value).encode("ascii") - return value - - def validate(self, frame, value): - if value is None: - return None - - if isinstance(value, bytes): - return value - elif PY3: - raise TypeError("%s has to be bytes" % self.name) - - value = text_type(value).encode("ascii") - return value - - -class EncodedTextSpec(Spec): - - _encodings = { - Encoding.LATIN1: ('latin1', b'\x00'), - Encoding.UTF16: ('utf16', b'\x00\x00'), - Encoding.UTF16BE: ('utf_16_be', b'\x00\x00'), - Encoding.UTF8: ('utf8', b'\x00'), - } - - def read(self, frame, data): - enc, term = self._encodings[frame.encoding] - try: - # allow missing termination - return decode_terminated(data, enc, strict=False) - except ValueError: - # utf-16 termination with missing BOM, or single NULL - if not data[:len(term)].strip(b"\x00"): - return u"", data[len(term):] - - # utf-16 data with single NULL, see issue 169 - try: - return decode_terminated(data + b"\x00", enc) - except ValueError: - raise SpecError("Decoding error") - - def write(self, frame, value): - enc, term = self._encodings[frame.encoding] - return value.encode(enc) + term - - def validate(self, frame, value): - return text_type(value) - - -class MultiSpec(Spec): - def __init__(self, name, *specs, **kw): - super(MultiSpec, self).__init__(name) - self.specs = specs - self.sep = kw.get('sep') - - def read(self, frame, data): - values = [] - while data: - record = [] - for spec in self.specs: - value, data = spec.read(frame, data) - record.append(value) - if len(self.specs) != 1: - values.append(record) - else: - values.append(record[0]) - return values, data - - def write(self, frame, value): - data = [] - if len(self.specs) == 1: - for v in value: - data.append(self.specs[0].write(frame, v)) - else: - for record in value: - for v, s in izip(record, self.specs): - data.append(s.write(frame, v)) - return b''.join(data) - - def validate(self, frame, value): - if value is None: - return [] - if self.sep and isinstance(value, string_types): - value = value.split(self.sep) - if isinstance(value, list): - if len(self.specs) == 1: - return [self.specs[0].validate(frame, v) for v in value] - else: - return [ - [s.validate(frame, v) for (v, s) in izip(val, self.specs)] - for val in value] - raise ValueError('Invalid MultiSpec data: %r' % value) - - def _validate23(self, frame, value, **kwargs): - if len(self.specs) != 1: - return [[s._validate23(frame, v, **kwargs) - for (v, s) in izip(val, self.specs)] - for val in value] - - spec = self.specs[0] - - # Merge single text spec multispecs only. - # (TimeStampSpec beeing the exception, but it's not a valid v2.3 frame) - if not isinstance(spec, EncodedTextSpec) or \ - isinstance(spec, TimeStampSpec): - return value - - value = [spec._validate23(frame, v, **kwargs) for v in value] - if kwargs.get("sep") is not None: - return [spec.validate(frame, kwargs["sep"].join(value))] - return value - - -class EncodedNumericTextSpec(EncodedTextSpec): - pass - - -class EncodedNumericPartTextSpec(EncodedTextSpec): - pass - - -class Latin1TextSpec(EncodedTextSpec): - def read(self, frame, data): - if b'\x00' in data: - data, ret = data.split(b'\x00', 1) - else: - ret = b'' - return data.decode('latin1'), ret - - def write(self, data, value): - return value.encode('latin1') + b'\x00' - - def validate(self, frame, value): - return text_type(value) - - -@swap_to_string -@total_ordering -class ID3TimeStamp(object): - """A time stamp in ID3v2 format. - - This is a restricted form of the ISO 8601 standard; time stamps - take the form of: - YYYY-MM-DD HH:MM:SS - Or some partial form (YYYY-MM-DD HH, YYYY, etc.). - - The 'text' attribute contains the raw text data of the time stamp. - """ - - import re - - def __init__(self, text): - if isinstance(text, ID3TimeStamp): - text = text.text - elif not isinstance(text, text_type): - if PY3: - raise TypeError("not a str") - text = text.decode("utf-8") - - self.text = text - - __formats = ['%04d'] + ['%02d'] * 5 - __seps = ['-', '-', ' ', ':', ':', 'x'] - - def get_text(self): - parts = [self.year, self.month, self.day, - self.hour, self.minute, self.second] - pieces = [] - for i, part in enumerate(parts): - if part is None: - break - pieces.append(self.__formats[i] % part + self.__seps[i]) - return u''.join(pieces)[:-1] - - def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')): - year, month, day, hour, minute, second = \ - splitre.split(text + ':::::')[:6] - for a in 'year month day hour minute second'.split(): - try: - v = int(locals()[a]) - except ValueError: - v = None - setattr(self, a, v) - - text = property(get_text, set_text, doc="ID3v2.4 date and time.") - - def __str__(self): - return self.text - - def __bytes__(self): - return self.text.encode("utf-8") - - def __repr__(self): - return repr(self.text) - - def __eq__(self, other): - return self.text == other.text - - def __lt__(self, other): - return self.text < other.text - - __hash__ = object.__hash__ - - def encode(self, *args): - return self.text.encode(*args) - - -class TimeStampSpec(EncodedTextSpec): - def read(self, frame, data): - value, data = super(TimeStampSpec, self).read(frame, data) - return self.validate(frame, value), data - - def write(self, frame, data): - return super(TimeStampSpec, self).write(frame, - data.text.replace(' ', 'T')) - - def validate(self, frame, value): - try: - return ID3TimeStamp(value) - except TypeError: - raise ValueError("Invalid ID3TimeStamp: %r" % value) - - -class ChannelSpec(ByteSpec): - (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE, - BACKCENTRE, SUBWOOFER) = xrange(9) - - -class VolumeAdjustmentSpec(Spec): - def read(self, frame, data): - value, = unpack('>h', data[0:2]) - return value / 512.0, data[2:] - - def write(self, frame, value): - number = int(round(value * 512)) - # pack only fails in 2.7, do it manually in 2.6 - if not -32768 <= number <= 32767: - raise SpecError("not in range") - return pack('>h', number) - - def validate(self, frame, value): - if value is not None: - try: - self.write(frame, value) - except SpecError: - raise ValueError("out of range") - return value - - -class VolumePeakSpec(Spec): - def read(self, frame, data): - # http://bugs.xmms.org/attachment.cgi?id=113&action=view - peak = 0 - data_array = bytearray(data) - bits = data_array[0] - vol_bytes = min(4, (bits + 7) >> 3) - # not enough frame data - if vol_bytes + 1 > len(data): - raise SpecError("not enough frame data") - shift = ((8 - (bits & 7)) & 7) + (4 - vol_bytes) * 8 - for i in xrange(1, vol_bytes + 1): - peak *= 256 - peak += data_array[i] - peak *= 2 ** shift - return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:] - - def write(self, frame, value): - number = int(round(value * 32768)) - # pack only fails in 2.7, do it manually in 2.6 - if not 0 <= number <= 65535: - raise SpecError("not in range") - # always write as 16 bits for sanity. - return b"\x10" + pack('>H', number) - - def validate(self, frame, value): - if value is not None: - try: - self.write(frame, value) - except SpecError: - raise ValueError("out of range") - return value - - -class SynchronizedTextSpec(EncodedTextSpec): - def read(self, frame, data): - texts = [] - encoding, term = self._encodings[frame.encoding] - while data: - try: - value, data = decode_terminated(data, encoding) - except ValueError: - raise SpecError("decoding error") - - if len(data) < 4: - raise SpecError("not enough data") - time, = struct.unpack(">I", data[:4]) - - texts.append((value, time)) - data = data[4:] - return texts, b"" - - def write(self, frame, value): - data = [] - encoding, term = self._encodings[frame.encoding] - for text, time in value: - text = text.encode(encoding) + term - data.append(text + struct.pack(">I", time)) - return b"".join(data) - - def validate(self, frame, value): - return value - - -class KeyEventSpec(Spec): - def read(self, frame, data): - events = [] - while len(data) >= 5: - events.append(struct.unpack(">bI", data[:5])) - data = data[5:] - return events, data - - def write(self, frame, value): - return b"".join(struct.pack(">bI", *event) for event in value) - - def validate(self, frame, value): - return value - - -class VolumeAdjustmentsSpec(Spec): - # Not to be confused with VolumeAdjustmentSpec. - def read(self, frame, data): - adjustments = {} - while len(data) >= 4: - freq, adj = struct.unpack(">Hh", data[:4]) - data = data[4:] - freq /= 2.0 - adj /= 512.0 - adjustments[freq] = adj - adjustments = sorted(adjustments.items()) - return adjustments, data - - def write(self, frame, value): - value.sort() - return b"".join(struct.pack(">Hh", int(freq * 2), int(adj * 512)) - for (freq, adj) in value) - - def validate(self, frame, value): - return value - - -class ASPIIndexSpec(Spec): - def read(self, frame, data): - if frame.b == 16: - format = "H" - size = 2 - elif frame.b == 8: - format = "B" - size = 1 - else: - raise SpecError("invalid bit count in ASPI (%d)" % frame.b) - - indexes = data[:frame.N * size] - data = data[frame.N * size:] - try: - return list(struct.unpack(">" + format * frame.N, indexes)), data - except struct.error as e: - raise SpecError(e) - - def write(self, frame, values): - if frame.b == 16: - format = "H" - elif frame.b == 8: - format = "B" - else: - raise SpecError("frame.b must be 8 or 16") - try: - return struct.pack(">" + format * frame.N, *values) - except struct.error as e: - raise SpecError(e) - - def validate(self, frame, values): - return values diff --git a/resources/lib/libraries/mutagen/id3/_util.py b/resources/lib/libraries/mutagen/id3/_util.py deleted file mode 100644 index 29f7241d..00000000 --- a/resources/lib/libraries/mutagen/id3/_util.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2005 Michael Urman -# 2013 Christoph Reiter -# 2014 Ben Ockmore -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -from .._compat import long_, integer_types, PY3 -from .._util import MutagenError - - -class error(MutagenError): - pass - - -class ID3NoHeaderError(error, ValueError): - pass - - -class ID3UnsupportedVersionError(error, NotImplementedError): - pass - - -class ID3EncryptionUnsupportedError(error, NotImplementedError): - pass - - -class ID3JunkFrameError(error, ValueError): - pass - - -class unsynch(object): - @staticmethod - def decode(value): - fragments = bytearray(value).split(b'\xff') - if len(fragments) > 1 and not fragments[-1]: - raise ValueError('string ended unsafe') - - for f in fragments[1:]: - if (not f) or (f[0] >= 0xE0): - raise ValueError('invalid sync-safe string') - - if f[0] == 0x00: - del f[0] - - return bytes(bytearray(b'\xff').join(fragments)) - - @staticmethod - def encode(value): - fragments = bytearray(value).split(b'\xff') - for f in fragments[1:]: - if (not f) or (f[0] >= 0xE0) or (f[0] == 0x00): - f.insert(0, 0x00) - return bytes(bytearray(b'\xff').join(fragments)) - - -class _BitPaddedMixin(object): - - def as_str(self, width=4, minwidth=4): - return self.to_str(self, self.bits, self.bigendian, width, minwidth) - - @staticmethod - def to_str(value, bits=7, bigendian=True, width=4, minwidth=4): - mask = (1 << bits) - 1 - - if width != -1: - index = 0 - bytes_ = bytearray(width) - try: - while value: - bytes_[index] = value & mask - value >>= bits - index += 1 - except IndexError: - raise ValueError('Value too wide (>%d bytes)' % width) - else: - # PCNT and POPM use growing integers - # of at least 4 bytes (=minwidth) as counters. - bytes_ = bytearray() - append = bytes_.append - while value: - append(value & mask) - value >>= bits - bytes_ = bytes_.ljust(minwidth, b"\x00") - - if bigendian: - bytes_.reverse() - return bytes(bytes_) - - @staticmethod - def has_valid_padding(value, bits=7): - """Whether the padding bits are all zero""" - - assert bits <= 8 - - mask = (((1 << (8 - bits)) - 1) << bits) - - if isinstance(value, integer_types): - while value: - if value & mask: - return False - value >>= 8 - elif isinstance(value, bytes): - for byte in bytearray(value): - if byte & mask: - return False - else: - raise TypeError - - return True - - -class BitPaddedInt(int, _BitPaddedMixin): - - def __new__(cls, value, bits=7, bigendian=True): - - mask = (1 << (bits)) - 1 - numeric_value = 0 - shift = 0 - - if isinstance(value, integer_types): - while value: - numeric_value += (value & mask) << shift - value >>= 8 - shift += bits - elif isinstance(value, bytes): - if bigendian: - value = reversed(value) - for byte in bytearray(value): - numeric_value += (byte & mask) << shift - shift += bits - else: - raise TypeError - - if isinstance(numeric_value, int): - self = int.__new__(BitPaddedInt, numeric_value) - else: - self = long_.__new__(BitPaddedLong, numeric_value) - - self.bits = bits - self.bigendian = bigendian - return self - -if PY3: - BitPaddedLong = BitPaddedInt -else: - class BitPaddedLong(long_, _BitPaddedMixin): - pass - - -class ID3BadUnsynchData(error, ValueError): - """Deprecated""" - - -class ID3BadCompressedData(error, ValueError): - """Deprecated""" - - -class ID3TagError(error, ValueError): - """Deprecated""" - - -class ID3Warning(error, UserWarning): - """Deprecated""" diff --git a/resources/lib/libraries/mutagen/m4a.py b/resources/lib/libraries/mutagen/m4a.py deleted file mode 100644 index 5730ace3..00000000 --- a/resources/lib/libraries/mutagen/m4a.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -""" -since 1.9: mutagen.m4a is deprecated; use mutagen.mp4 instead. -since 1.31: mutagen.m4a will no longer work; any operation that could fail - will fail now. -""" - -import warnings - -from mutagen import FileType, Metadata, StreamInfo -from ._util import DictProxy, MutagenError - -warnings.warn( - "mutagen.m4a is deprecated; use mutagen.mp4 instead.", - DeprecationWarning) - - -class error(IOError, MutagenError): - pass - - -class M4AMetadataError(error): - pass - - -class M4AStreamInfoError(error): - pass - - -class M4AMetadataValueError(ValueError, M4AMetadataError): - pass - - -__all__ = ['M4A', 'Open', 'delete', 'M4ACover'] - - -class M4ACover(bytes): - - FORMAT_JPEG = 0x0D - FORMAT_PNG = 0x0E - - def __new__(cls, data, imageformat=None): - self = bytes.__new__(cls, data) - if imageformat is None: - imageformat = M4ACover.FORMAT_JPEG - self.imageformat = imageformat - return self - - -class M4ATags(DictProxy, Metadata): - - def load(self, atoms, fileobj): - raise error("deprecated") - - def save(self, filename): - raise error("deprecated") - - def delete(self, filename): - raise error("deprecated") - - def pprint(self): - return u"" - - -class M4AInfo(StreamInfo): - - bitrate = 0 - - def __init__(self, atoms, fileobj): - raise error("deprecated") - - def pprint(self): - return u"" - - -class M4A(FileType): - - _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] - - def load(self, filename): - raise error("deprecated") - - def add_tags(self): - self.tags = M4ATags() - - @staticmethod - def score(filename, fileobj, header): - return 0 - - -Open = M4A - - -def delete(filename): - raise error("deprecated") diff --git a/resources/lib/libraries/mutagen/monkeysaudio.py b/resources/lib/libraries/mutagen/monkeysaudio.py deleted file mode 100644 index 0e29273f..00000000 --- a/resources/lib/libraries/mutagen/monkeysaudio.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Monkey's Audio streams with APEv2 tags. - -Monkey's Audio is a very efficient lossless audio compressor developed -by Matt Ashland. - -For more information, see http://www.monkeysaudio.com/. -""" - -__all__ = ["MonkeysAudio", "Open", "delete"] - -import struct - -from ._compat import endswith -from mutagen import StreamInfo -from mutagen.apev2 import APEv2File, error, delete -from mutagen._util import cdata - - -class MonkeysAudioHeaderError(error): - pass - - -class MonkeysAudioInfo(StreamInfo): - """Monkey's Audio stream information. - - Attributes: - - * channels -- number of audio channels - * length -- file length in seconds, as a float - * sample_rate -- audio sampling rate in Hz - * bits_per_sample -- bits per sample - * version -- Monkey's Audio stream version, as a float (eg: 3.99) - """ - - def __init__(self, fileobj): - header = fileobj.read(76) - if len(header) != 76 or not header.startswith(b"MAC "): - raise MonkeysAudioHeaderError("not a Monkey's Audio file") - self.version = cdata.ushort_le(header[4:6]) - if self.version >= 3980: - (blocks_per_frame, final_frame_blocks, total_frames, - self.bits_per_sample, self.channels, - self.sample_rate) = struct.unpack("<IIIHHI", header[56:76]) - else: - compression_level = cdata.ushort_le(header[6:8]) - self.channels, self.sample_rate = struct.unpack( - "<HI", header[10:16]) - total_frames, final_frame_blocks = struct.unpack( - "<II", header[24:32]) - if self.version >= 3950: - blocks_per_frame = 73728 * 4 - elif self.version >= 3900 or (self.version >= 3800 and - compression_level == 4): - blocks_per_frame = 73728 - else: - blocks_per_frame = 9216 - self.version /= 1000.0 - self.length = 0.0 - if (self.sample_rate != 0) and (total_frames > 0): - total_blocks = ((total_frames - 1) * blocks_per_frame + - final_frame_blocks) - self.length = float(total_blocks) / self.sample_rate - - def pprint(self): - return u"Monkey's Audio %.2f, %.2f seconds, %d Hz" % ( - self.version, self.length, self.sample_rate) - - -class MonkeysAudio(APEv2File): - _Info = MonkeysAudioInfo - _mimes = ["audio/ape", "audio/x-ape"] - - @staticmethod - def score(filename, fileobj, header): - return header.startswith(b"MAC ") + endswith(filename.lower(), ".ape") - - -Open = MonkeysAudio diff --git a/resources/lib/libraries/mutagen/mp3.py b/resources/lib/libraries/mutagen/mp3.py deleted file mode 100644 index afb600cf..00000000 --- a/resources/lib/libraries/mutagen/mp3.py +++ /dev/null @@ -1,362 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""MPEG audio stream information and tags.""" - -import os -import struct - -from ._compat import endswith, xrange -from ._mp3util import XingHeader, XingHeaderError, VBRIHeader, VBRIHeaderError -from mutagen import StreamInfo -from mutagen._util import MutagenError, enum -from mutagen.id3 import ID3FileType, BitPaddedInt, delete - -__all__ = ["MP3", "Open", "delete", "MP3"] - - -class error(RuntimeError, MutagenError): - pass - - -class HeaderNotFoundError(error, IOError): - pass - - -class InvalidMPEGHeader(error, IOError): - pass - - -@enum -class BitrateMode(object): - - UNKNOWN = 0 - """Probably a CBR file, but not sure""" - - CBR = 1 - """Constant Bitrate""" - - VBR = 2 - """Variable Bitrate""" - - ABR = 3 - """Average Bitrate (a variant of VBR)""" - - -def _guess_xing_bitrate_mode(xing): - - if xing.lame_header: - lame = xing.lame_header - if lame.vbr_method in (1, 8): - return BitrateMode.CBR - elif lame.vbr_method in (2, 9): - return BitrateMode.ABR - elif lame.vbr_method in (3, 4, 5, 6): - return BitrateMode.VBR - # everything else undefined, continue guessing - - # info tags get only written by lame for cbr files - if xing.is_info: - return BitrateMode.CBR - - # older lame and non-lame with some variant of vbr - if xing.vbr_scale != -1 or xing.lame_version: - return BitrateMode.VBR - - return BitrateMode.UNKNOWN - - -# Mode values. -STEREO, JOINTSTEREO, DUALCHANNEL, MONO = xrange(4) - - -class MPEGInfo(StreamInfo): - """MPEG audio stream information - - Parse information about an MPEG audio file. This also reads the - Xing VBR header format. - - This code was implemented based on the format documentation at - http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm. - - Useful attributes: - - * length -- audio length, in seconds - * channels -- number of audio channels - * bitrate -- audio bitrate, in bits per second - * sketchy -- if true, the file may not be valid MPEG audio - * encoder_info -- a string containing encoder name and possibly version. - In case a lame tag is present this will start with - ``"LAME "``, if unknown it is empty, otherwise the - text format is undefined. - * bitrate_mode -- a :class:`BitrateMode` - - * track_gain -- replaygain track gain (89db) or None - * track_peak -- replaygain track peak or None - * album_gain -- replaygain album gain (89db) or None - - Useless attributes: - - * version -- MPEG version (1, 2, 2.5) - * layer -- 1, 2, or 3 - * mode -- One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3) - * protected -- whether or not the file is "protected" - * padding -- whether or not audio frames are padded - * sample_rate -- audio sample rate, in Hz - """ - - # Map (version, layer) tuples to bitrates. - __BITRATE = { - (1, 1): [0, 32, 64, 96, 128, 160, 192, 224, - 256, 288, 320, 352, 384, 416, 448], - (1, 2): [0, 32, 48, 56, 64, 80, 96, 112, 128, - 160, 192, 224, 256, 320, 384], - (1, 3): [0, 32, 40, 48, 56, 64, 80, 96, 112, - 128, 160, 192, 224, 256, 320], - (2, 1): [0, 32, 48, 56, 64, 80, 96, 112, 128, - 144, 160, 176, 192, 224, 256], - (2, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, - 80, 96, 112, 128, 144, 160], - } - - __BITRATE[(2, 3)] = __BITRATE[(2, 2)] - for i in xrange(1, 4): - __BITRATE[(2.5, i)] = __BITRATE[(2, i)] - - # Map version to sample rates. - __RATES = { - 1: [44100, 48000, 32000], - 2: [22050, 24000, 16000], - 2.5: [11025, 12000, 8000] - } - - sketchy = False - encoder_info = u"" - bitrate_mode = BitrateMode.UNKNOWN - track_gain = track_peak = album_gain = album_peak = None - - def __init__(self, fileobj, offset=None): - """Parse MPEG stream information from a file-like object. - - If an offset argument is given, it is used to start looking - for stream information and Xing headers; otherwise, ID3v2 tags - will be skipped automatically. A correct offset can make - loading files significantly faster. - """ - - try: - size = os.path.getsize(fileobj.name) - except (IOError, OSError, AttributeError): - fileobj.seek(0, 2) - size = fileobj.tell() - - # If we don't get an offset, try to skip an ID3v2 tag. - if offset is None: - fileobj.seek(0, 0) - idata = fileobj.read(10) - try: - id3, insize = struct.unpack('>3sxxx4s', idata) - except struct.error: - id3, insize = b'', 0 - insize = BitPaddedInt(insize) - if id3 == b'ID3' and insize > 0: - offset = insize + 10 - else: - offset = 0 - - # Try to find two valid headers (meaning, very likely MPEG data) - # at the given offset, 30% through the file, 60% through the file, - # and 90% through the file. - for i in [offset, 0.3 * size, 0.6 * size, 0.9 * size]: - try: - self.__try(fileobj, int(i), size - offset) - except error: - pass - else: - break - # If we can't find any two consecutive frames, try to find just - # one frame back at the original offset given. - else: - self.__try(fileobj, offset, size - offset, False) - self.sketchy = True - - def __try(self, fileobj, offset, real_size, check_second=True): - # This is going to be one really long function; bear with it, - # because there's not really a sane point to cut it up. - fileobj.seek(offset, 0) - - # We "know" we have an MPEG file if we find two frames that look like - # valid MPEG data. If we can't find them in 32k of reads, something - # is horribly wrong (the longest frame can only be about 4k). This - # is assuming the offset didn't lie. - data = fileobj.read(32768) - - frame_1 = data.find(b"\xff") - while 0 <= frame_1 <= (len(data) - 4): - frame_data = struct.unpack(">I", data[frame_1:frame_1 + 4])[0] - if ((frame_data >> 16) & 0xE0) != 0xE0: - frame_1 = data.find(b"\xff", frame_1 + 2) - else: - version = (frame_data >> 19) & 0x3 - layer = (frame_data >> 17) & 0x3 - protection = (frame_data >> 16) & 0x1 - bitrate = (frame_data >> 12) & 0xF - sample_rate = (frame_data >> 10) & 0x3 - padding = (frame_data >> 9) & 0x1 - # private = (frame_data >> 8) & 0x1 - self.mode = (frame_data >> 6) & 0x3 - # mode_extension = (frame_data >> 4) & 0x3 - # copyright = (frame_data >> 3) & 0x1 - # original = (frame_data >> 2) & 0x1 - # emphasis = (frame_data >> 0) & 0x3 - if (version == 1 or layer == 0 or sample_rate == 0x3 or - bitrate == 0 or bitrate == 0xF): - frame_1 = data.find(b"\xff", frame_1 + 2) - else: - break - else: - raise HeaderNotFoundError("can't sync to an MPEG frame") - - self.channels = 1 if self.mode == MONO else 2 - - # There is a serious problem here, which is that many flags - # in an MPEG header are backwards. - self.version = [2.5, None, 2, 1][version] - self.layer = 4 - layer - self.protected = not protection - self.padding = bool(padding) - - self.bitrate = self.__BITRATE[(self.version, self.layer)][bitrate] - self.bitrate *= 1000 - self.sample_rate = self.__RATES[self.version][sample_rate] - - if self.layer == 1: - frame_length = ( - (12 * self.bitrate // self.sample_rate) + padding) * 4 - frame_size = 384 - elif self.version >= 2 and self.layer == 3: - frame_length = (72 * self.bitrate // self.sample_rate) + padding - frame_size = 576 - else: - frame_length = (144 * self.bitrate // self.sample_rate) + padding - frame_size = 1152 - - if check_second: - possible = int(frame_1 + frame_length) - if possible > len(data) + 4: - raise HeaderNotFoundError("can't sync to second MPEG frame") - try: - frame_data = struct.unpack( - ">H", data[possible:possible + 2])[0] - except struct.error: - raise HeaderNotFoundError("can't sync to second MPEG frame") - if (frame_data & 0xFFE0) != 0xFFE0: - raise HeaderNotFoundError("can't sync to second MPEG frame") - - self.length = 8 * real_size / float(self.bitrate) - - # Try to find/parse the Xing header, which trumps the above length - # and bitrate calculation. - - if self.layer != 3: - return - - # Xing - xing_offset = XingHeader.get_offset(self) - fileobj.seek(offset + frame_1 + xing_offset, 0) - try: - xing = XingHeader(fileobj) - except XingHeaderError: - pass - else: - lame = xing.lame_header - self.sketchy = False - self.bitrate_mode = _guess_xing_bitrate_mode(xing) - if xing.frames != -1: - samples = frame_size * xing.frames - if lame is not None: - samples -= lame.encoder_delay_start - samples -= lame.encoder_padding_end - self.length = float(samples) / self.sample_rate - if xing.bytes != -1 and self.length: - self.bitrate = int((xing.bytes * 8) / self.length) - if xing.lame_version: - self.encoder_info = u"LAME %s" % xing.lame_version - if lame is not None: - self.track_gain = lame.track_gain_adjustment - self.track_peak = lame.track_peak - self.album_gain = lame.album_gain_adjustment - return - - # VBRI - vbri_offset = VBRIHeader.get_offset(self) - fileobj.seek(offset + frame_1 + vbri_offset, 0) - try: - vbri = VBRIHeader(fileobj) - except VBRIHeaderError: - pass - else: - self.bitrate_mode = BitrateMode.VBR - self.encoder_info = u"FhG" - self.sketchy = False - self.length = float(frame_size * vbri.frames) / self.sample_rate - if self.length: - self.bitrate = int((vbri.bytes * 8) / self.length) - - def pprint(self): - info = str(self.bitrate_mode).split(".", 1)[-1] - if self.bitrate_mode == BitrateMode.UNKNOWN: - info = u"CBR?" - if self.encoder_info: - info += ", %s" % self.encoder_info - s = u"MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % ( - self.version, self.layer, self.bitrate, info, - self.sample_rate, self.channels, self.length) - if self.sketchy: - s += u" (sketchy)" - return s - - -class MP3(ID3FileType): - """An MPEG audio (usually MPEG-1 Layer 3) file. - - :ivar info: :class:`MPEGInfo` - :ivar tags: :class:`ID3 <mutagen.id3.ID3>` - """ - - _Info = MPEGInfo - - _mimes = ["audio/mpeg", "audio/mpg", "audio/x-mpeg"] - - @property - def mime(self): - l = self.info.layer - return ["audio/mp%d" % l, "audio/x-mp%d" % l] + super(MP3, self).mime - - @staticmethod - def score(filename, fileobj, header_data): - filename = filename.lower() - - return (header_data.startswith(b"ID3") * 2 + - endswith(filename, b".mp3") + - endswith(filename, b".mp2") + endswith(filename, b".mpg") + - endswith(filename, b".mpeg")) - - -Open = MP3 - - -class EasyMP3(MP3): - """Like MP3, but uses EasyID3 for tags. - - :ivar info: :class:`MPEGInfo` - :ivar tags: :class:`EasyID3 <mutagen.easyid3.EasyID3>` - """ - - from mutagen.easyid3 import EasyID3 as ID3 - ID3 = ID3 diff --git a/resources/lib/libraries/mutagen/mp4/__init__.py b/resources/lib/libraries/mutagen/mp4/__init__.py deleted file mode 100644 index bc242ee8..00000000 --- a/resources/lib/libraries/mutagen/mp4/__init__.py +++ /dev/null @@ -1,1010 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write MPEG-4 audio files with iTunes metadata. - -This module will read MPEG-4 audio information and metadata, -as found in Apple's MP4 (aka M4A, M4B, M4P) files. - -There is no official specification for this format. The source code -for TagLib, FAAD, and various MPEG specifications at - -* http://developer.apple.com/documentation/QuickTime/QTFF/ -* http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt -* http://standards.iso.org/ittf/PubliclyAvailableStandards/\ -c041828_ISO_IEC_14496-12_2005(E).zip -* http://wiki.multimedia.cx/index.php?title=Apple_QuickTime - -were all consulted. -""" - -import struct -import sys - -from mutagen import FileType, Metadata, StreamInfo, PaddingInfo -from mutagen._constants import GENRES -from mutagen._util import (cdata, insert_bytes, DictProxy, MutagenError, - hashable, enum, get_size, resize_bytes) -from mutagen._compat import (reraise, PY2, string_types, text_type, chr_, - iteritems, PY3, cBytesIO, izip, xrange) -from ._atom import Atoms, Atom, AtomError -from ._util import parse_full_atom -from ._as_entry import AudioSampleEntry, ASEntryError - - -class error(IOError, MutagenError): - pass - - -class MP4MetadataError(error): - pass - - -class MP4StreamInfoError(error): - pass - - -class MP4MetadataValueError(ValueError, MP4MetadataError): - pass - - -__all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm', 'AtomDataType'] - - -@enum -class AtomDataType(object): - """Enum for `dataformat` attribute of MP4FreeForm. - - .. versionadded:: 1.25 - """ - - IMPLICIT = 0 - """for use with tags for which no type needs to be indicated because - only one type is allowed""" - - UTF8 = 1 - """without any count or null terminator""" - - UTF16 = 2 - """also known as UTF-16BE""" - - SJIS = 3 - """deprecated unless it is needed for special Japanese characters""" - - HTML = 6 - """the HTML file header specifies which HTML version""" - - XML = 7 - """the XML header must identify the DTD or schemas""" - - UUID = 8 - """also known as GUID; stored as 16 bytes in binary (valid as an ID)""" - - ISRC = 9 - """stored as UTF-8 text (valid as an ID)""" - - MI3P = 10 - """stored as UTF-8 text (valid as an ID)""" - - GIF = 12 - """(deprecated) a GIF image""" - - JPEG = 13 - """a JPEG image""" - - PNG = 14 - """PNG image""" - - URL = 15 - """absolute, in UTF-8 characters""" - - DURATION = 16 - """in milliseconds, 32-bit integer""" - - DATETIME = 17 - """in UTC, counting seconds since midnight, January 1, 1904; - 32 or 64-bits""" - - GENRES = 18 - """a list of enumerated values""" - - INTEGER = 21 - """a signed big-endian integer with length one of { 1,2,3,4,8 } bytes""" - - RIAA_PA = 24 - """RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, - 8-bit ingteger""" - - UPC = 25 - """Universal Product Code, in text UTF-8 format (valid as an ID)""" - - BMP = 27 - """Windows bitmap image""" - - -@hashable -class MP4Cover(bytes): - """A cover artwork. - - Attributes: - - * imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG) - """ - - FORMAT_JPEG = AtomDataType.JPEG - FORMAT_PNG = AtomDataType.PNG - - def __new__(cls, data, *args, **kwargs): - return bytes.__new__(cls, data) - - def __init__(self, data, imageformat=FORMAT_JPEG): - self.imageformat = imageformat - - __hash__ = bytes.__hash__ - - def __eq__(self, other): - if not isinstance(other, MP4Cover): - return bytes(self) == other - - return (bytes(self) == bytes(other) and - self.imageformat == other.imageformat) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(%r, %r)" % ( - type(self).__name__, bytes(self), - AtomDataType(self.imageformat)) - - -@hashable -class MP4FreeForm(bytes): - """A freeform value. - - Attributes: - - * dataformat -- format of the data (see AtomDataType) - """ - - FORMAT_DATA = AtomDataType.IMPLICIT # deprecated - FORMAT_TEXT = AtomDataType.UTF8 # deprecated - - def __new__(cls, data, *args, **kwargs): - return bytes.__new__(cls, data) - - def __init__(self, data, dataformat=AtomDataType.UTF8, version=0): - self.dataformat = dataformat - self.version = version - - __hash__ = bytes.__hash__ - - def __eq__(self, other): - if not isinstance(other, MP4FreeForm): - return bytes(self) == other - - return (bytes(self) == bytes(other) and - self.dataformat == other.dataformat and - self.version == other.version) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(%r, %r)" % ( - type(self).__name__, bytes(self), - AtomDataType(self.dataformat)) - - - -def _name2key(name): - if PY2: - return name - return name.decode("latin-1") - - -def _key2name(key): - if PY2: - return key - return key.encode("latin-1") - - -def _find_padding(atom_path): - # Check for padding "free" atom - # XXX: we only use them if they are adjacent to ilst, and only one. - # and there also is a top level free atom which we could use maybe..? - - meta, ilst = atom_path[-2:] - assert meta.name == b"meta" and ilst.name == b"ilst" - index = meta.children.index(ilst) - try: - prev = meta.children[index - 1] - if prev.name == b"free": - return prev - except IndexError: - pass - - try: - next_ = meta.children[index + 1] - if next_.name == b"free": - return next_ - except IndexError: - pass - - -class MP4Tags(DictProxy, Metadata): - r"""Dictionary containing Apple iTunes metadata list key/values. - - Keys are four byte identifiers, except for freeform ('----') - keys. Values are usually unicode strings, but some atoms have a - special structure: - - Text values (multiple values per key are supported): - - * '\\xa9nam' -- track title - * '\\xa9alb' -- album - * '\\xa9ART' -- artist - * 'aART' -- album artist - * '\\xa9wrt' -- composer - * '\\xa9day' -- year - * '\\xa9cmt' -- comment - * 'desc' -- description (usually used in podcasts) - * 'purd' -- purchase date - * '\\xa9grp' -- grouping - * '\\xa9gen' -- genre - * '\\xa9lyr' -- lyrics - * 'purl' -- podcast URL - * 'egid' -- podcast episode GUID - * 'catg' -- podcast category - * 'keyw' -- podcast keywords - * '\\xa9too' -- encoded by - * 'cprt' -- copyright - * 'soal' -- album sort order - * 'soaa' -- album artist sort order - * 'soar' -- artist sort order - * 'sonm' -- title sort order - * 'soco' -- composer sort order - * 'sosn' -- show sort order - * 'tvsh' -- show name - - Boolean values: - - * 'cpil' -- part of a compilation - * 'pgap' -- part of a gapless album - * 'pcst' -- podcast (iTunes reads this only on import) - - Tuples of ints (multiple values per key are supported): - - * 'trkn' -- track number, total tracks - * 'disk' -- disc number, total discs - - Others: - - * 'tmpo' -- tempo/BPM, 16 bit int - * 'covr' -- cover artwork, list of MP4Cover objects (which are - tagged strs) - * 'gnre' -- ID3v1 genre. Not supported, use '\\xa9gen' instead. - - The freeform '----' frames use a key in the format '----:mean:name' - where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique - identifier for this frame. The value is a str, but is probably - text that can be decoded as UTF-8. Multiple values per key are - supported. - - MP4 tag data cannot exist outside of the structure of an MP4 file, - so this class should not be manually instantiated. - - Unknown non-text tags and tags that failed to parse will be written - back as is. - """ - - def __init__(self, *args, **kwargs): - self._failed_atoms = {} - super(MP4Tags, self).__init__(*args, **kwargs) - - def load(self, atoms, fileobj): - try: - path = atoms.path(b"moov", b"udta", b"meta", b"ilst") - except KeyError as key: - raise MP4MetadataError(key) - - free = _find_padding(path) - self._padding = free.datalength if free is not None else 0 - - ilst = path[-1] - for atom in ilst.children: - ok, data = atom.read(fileobj) - if not ok: - raise MP4MetadataError("Not enough data") - - try: - if atom.name in self.__atoms: - info = self.__atoms[atom.name] - info[0](self, atom, data) - else: - # unknown atom, try as text - self.__parse_text(atom, data, implicit=False) - except MP4MetadataError: - # parsing failed, save them so we can write them back - key = _name2key(atom.name) - self._failed_atoms.setdefault(key, []).append(data) - - def __setitem__(self, key, value): - if not isinstance(key, str): - raise TypeError("key has to be str") - super(MP4Tags, self).__setitem__(key, value) - - @classmethod - def _can_load(cls, atoms): - return b"moov.udta.meta.ilst" in atoms - - @staticmethod - def _key_sort(item): - (key, v) = item - # iTunes always writes the tags in order of "relevance", try - # to copy it as closely as possible. - order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb", - "\xa9gen", "gnre", "trkn", "disk", - "\xa9day", "cpil", "pgap", "pcst", "tmpo", - "\xa9too", "----", "covr", "\xa9lyr"] - order = dict(izip(order, xrange(len(order)))) - last = len(order) - # If there's no key-based way to distinguish, order by length. - # If there's still no way, go by string comparison on the - # values, so we at least have something determinstic. - return (order.get(key[:4], last), len(repr(v)), repr(v)) - - def save(self, filename, padding=None): - """Save the metadata to the given filename.""" - - values = [] - items = sorted(self.items(), key=self._key_sort) - for key, value in items: - atom_name = _key2name(key)[:4] - if atom_name in self.__atoms: - render_func = self.__atoms[atom_name][1] - else: - render_func = type(self).__render_text - - try: - values.append(render_func(self, key, value)) - except (TypeError, ValueError) as s: - reraise(MP4MetadataValueError, s, sys.exc_info()[2]) - - for key, failed in iteritems(self._failed_atoms): - # don't write atoms back if we have added a new one with - # the same name, this excludes freeform which can have - # multiple atoms with the same key (most parsers seem to be able - # to handle that) - if key in self: - assert _key2name(key) != b"----" - continue - for data in failed: - values.append(Atom.render(_key2name(key), data)) - - data = Atom.render(b"ilst", b"".join(values)) - - # Find the old atoms. - with open(filename, "rb+") as fileobj: - try: - atoms = Atoms(fileobj) - except AtomError as err: - reraise(error, err, sys.exc_info()[2]) - - self.__save(fileobj, atoms, data, padding) - - def __save(self, fileobj, atoms, data, padding): - try: - path = atoms.path(b"moov", b"udta", b"meta", b"ilst") - except KeyError: - self.__save_new(fileobj, atoms, data, padding) - else: - self.__save_existing(fileobj, atoms, path, data, padding) - - def __pad_ilst(self, data, length=None): - if length is None: - length = ((len(data) + 1023) & ~1023) - len(data) - return Atom.render(b"free", b"\x00" * length) - - def __save_new(self, fileobj, atoms, ilst_data, padding_func): - hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"mdirappl" + b"\x00" * 9) - meta_data = b"\x00\x00\x00\x00" + hdlr + ilst_data - - try: - path = atoms.path(b"moov", b"udta") - except KeyError: - path = atoms.path(b"moov") - - offset = path[-1]._dataoffset - - # ignoring some atom overhead... but we don't have padding left anyway - # and padding_size is guaranteed to be less than zero - content_size = get_size(fileobj) - offset - padding_size = -len(meta_data) - assert padding_size < 0 - info = PaddingInfo(padding_size, content_size) - new_padding = info._get_padding(padding_func) - new_padding = min(0xFFFFFFFF, new_padding) - - free = Atom.render(b"free", b"\x00" * new_padding) - meta = Atom.render(b"meta", meta_data + free) - if path[-1].name != b"udta": - # moov.udta not found -- create one - data = Atom.render(b"udta", meta) - else: - data = meta - - insert_bytes(fileobj, len(data), offset) - fileobj.seek(offset) - fileobj.write(data) - self.__update_parents(fileobj, path, len(data)) - self.__update_offsets(fileobj, atoms, len(data), offset) - - def __save_existing(self, fileobj, atoms, path, ilst_data, padding_func): - # Replace the old ilst atom. - ilst = path[-1] - offset = ilst.offset - length = ilst.length - - # Use adjacent free atom if there is one - free = _find_padding(path) - if free is not None: - offset = min(offset, free.offset) - length += free.length - - # Always add a padding atom to make things easier - padding_overhead = len(Atom.render(b"free", b"")) - content_size = get_size(fileobj) - (offset + length) - padding_size = length - (len(ilst_data) + padding_overhead) - info = PaddingInfo(padding_size, content_size) - new_padding = info._get_padding(padding_func) - # Limit padding size so we can be sure the free atom overhead is as we - # calculated above (see Atom.render) - new_padding = min(0xFFFFFFFF, new_padding) - - ilst_data += Atom.render(b"free", b"\x00" * new_padding) - - resize_bytes(fileobj, length, len(ilst_data), offset) - delta = len(ilst_data) - length - - fileobj.seek(offset) - fileobj.write(ilst_data) - self.__update_parents(fileobj, path[:-1], delta) - self.__update_offsets(fileobj, atoms, delta, offset) - - def __update_parents(self, fileobj, path, delta): - """Update all parent atoms with the new size.""" - - if delta == 0: - return - - for atom in path: - fileobj.seek(atom.offset) - size = cdata.uint_be(fileobj.read(4)) - if size == 1: # 64bit - # skip name (4B) and read size (8B) - size = cdata.ulonglong_be(fileobj.read(12)[4:]) - fileobj.seek(atom.offset + 8) - fileobj.write(cdata.to_ulonglong_be(size + delta)) - else: # 32bit - fileobj.seek(atom.offset) - fileobj.write(cdata.to_uint_be(size + delta)) - - def __update_offset_table(self, fileobj, fmt, atom, delta, offset): - """Update offset table in the specified atom.""" - if atom.offset > offset: - atom.offset += delta - fileobj.seek(atom.offset + 12) - data = fileobj.read(atom.length - 12) - fmt = fmt % cdata.uint_be(data[:4]) - offsets = struct.unpack(fmt, data[4:]) - offsets = [o + (0, delta)[offset < o] for o in offsets] - fileobj.seek(atom.offset + 16) - fileobj.write(struct.pack(fmt, *offsets)) - - def __update_tfhd(self, fileobj, atom, delta, offset): - if atom.offset > offset: - atom.offset += delta - fileobj.seek(atom.offset + 9) - data = fileobj.read(atom.length - 9) - flags = cdata.uint_be(b"\x00" + data[:3]) - if flags & 1: - o = cdata.ulonglong_be(data[7:15]) - if o > offset: - o += delta - fileobj.seek(atom.offset + 16) - fileobj.write(cdata.to_ulonglong_be(o)) - - def __update_offsets(self, fileobj, atoms, delta, offset): - """Update offset tables in all 'stco' and 'co64' atoms.""" - if delta == 0: - return - moov = atoms[b"moov"] - for atom in moov.findall(b'stco', True): - self.__update_offset_table(fileobj, ">%dI", atom, delta, offset) - for atom in moov.findall(b'co64', True): - self.__update_offset_table(fileobj, ">%dQ", atom, delta, offset) - try: - for atom in atoms[b"moof"].findall(b'tfhd', True): - self.__update_tfhd(fileobj, atom, delta, offset) - except KeyError: - pass - - def __parse_data(self, atom, data): - pos = 0 - while pos < atom.length - 8: - head = data[pos:pos + 12] - if len(head) != 12: - raise MP4MetadataError("truncated atom % r" % atom.name) - length, name = struct.unpack(">I4s", head[:8]) - version = ord(head[8:9]) - flags = struct.unpack(">I", b"\x00" + head[9:12])[0] - if name != b"data": - raise MP4MetadataError( - "unexpected atom %r inside %r" % (name, atom.name)) - - chunk = data[pos + 16:pos + length] - if len(chunk) != length - 16: - raise MP4MetadataError("truncated atom % r" % atom.name) - yield version, flags, chunk - pos += length - - def __add(self, key, value, single=False): - assert isinstance(key, str) - - if single: - self[key] = value - else: - self.setdefault(key, []).extend(value) - - def __render_data(self, key, version, flags, value): - return Atom.render(_key2name(key), b"".join([ - Atom.render( - b"data", struct.pack(">2I", version << 24 | flags, 0) + data) - for data in value])) - - def __parse_freeform(self, atom, data): - length = cdata.uint_be(data[:4]) - mean = data[12:length] - pos = length - length = cdata.uint_be(data[pos:pos + 4]) - name = data[pos + 12:pos + length] - pos += length - value = [] - while pos < atom.length - 8: - length, atom_name = struct.unpack(">I4s", data[pos:pos + 8]) - if atom_name != b"data": - raise MP4MetadataError( - "unexpected atom %r inside %r" % (atom_name, atom.name)) - - version = ord(data[pos + 8:pos + 8 + 1]) - flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0] - value.append(MP4FreeForm(data[pos + 16:pos + length], - dataformat=flags, version=version)) - pos += length - - key = _name2key(atom.name + b":" + mean + b":" + name) - self.__add(key, value) - - def __render_freeform(self, key, value): - if isinstance(value, bytes): - value = [value] - - dummy, mean, name = _key2name(key).split(b":", 2) - mean = struct.pack(">I4sI", len(mean) + 12, b"mean", 0) + mean - name = struct.pack(">I4sI", len(name) + 12, b"name", 0) + name - - data = b"" - for v in value: - flags = AtomDataType.UTF8 - version = 0 - if isinstance(v, MP4FreeForm): - flags = v.dataformat - version = v.version - - data += struct.pack( - ">I4s2I", len(v) + 16, b"data", version << 24 | flags, 0) - data += v - - return Atom.render(b"----", mean + name + data) - - def __parse_pair(self, atom, data): - key = _name2key(atom.name) - values = [struct.unpack(">2H", d[2:6]) for - version, flags, d in self.__parse_data(atom, data)] - self.__add(key, values) - - def __render_pair(self, key, value): - data = [] - for (track, total) in value: - if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: - data.append(struct.pack(">4H", 0, track, total, 0)) - else: - raise MP4MetadataValueError( - "invalid numeric pair %r" % ((track, total),)) - return self.__render_data(key, 0, AtomDataType.IMPLICIT, data) - - def __render_pair_no_trailing(self, key, value): - data = [] - for (track, total) in value: - if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: - data.append(struct.pack(">3H", 0, track, total)) - else: - raise MP4MetadataValueError( - "invalid numeric pair %r" % ((track, total),)) - return self.__render_data(key, 0, AtomDataType.IMPLICIT, data) - - def __parse_genre(self, atom, data): - values = [] - for version, flags, data in self.__parse_data(atom, data): - # version = 0, flags = 0 - if len(data) != 2: - raise MP4MetadataValueError("invalid genre") - genre = cdata.short_be(data) - # Translate to a freeform genre. - try: - genre = GENRES[genre - 1] - except IndexError: - # this will make us write it back at least - raise MP4MetadataValueError("unknown genre") - values.append(genre) - key = _name2key(b"\xa9gen") - self.__add(key, values) - - def __parse_tempo(self, atom, data): - values = [] - for version, flags, data in self.__parse_data(atom, data): - # version = 0, flags = 0 or 21 - if len(data) != 2: - raise MP4MetadataValueError("invalid tempo") - values.append(cdata.ushort_be(data)) - key = _name2key(atom.name) - self.__add(key, values) - - def __render_tempo(self, key, value): - try: - if len(value) == 0: - return self.__render_data(key, 0, AtomDataType.INTEGER, b"") - - if (min(value) < 0) or (max(value) >= 2 ** 16): - raise MP4MetadataValueError( - "invalid 16 bit integers: %r" % value) - except TypeError: - raise MP4MetadataValueError( - "tmpo must be a list of 16 bit integers") - - values = [cdata.to_ushort_be(v) for v in value] - return self.__render_data(key, 0, AtomDataType.INTEGER, values) - - def __parse_bool(self, atom, data): - for version, flags, data in self.__parse_data(atom, data): - if len(data) != 1: - raise MP4MetadataValueError("invalid bool") - - value = bool(ord(data)) - key = _name2key(atom.name) - self.__add(key, value, single=True) - - def __render_bool(self, key, value): - return self.__render_data( - key, 0, AtomDataType.INTEGER, [chr_(bool(value))]) - - def __parse_cover(self, atom, data): - values = [] - pos = 0 - while pos < atom.length - 8: - length, name, imageformat = struct.unpack(">I4sI", - data[pos:pos + 12]) - if name != b"data": - if name == b"name": - pos += length - continue - raise MP4MetadataError( - "unexpected atom %r inside 'covr'" % name) - if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG): - # Sometimes AtomDataType.IMPLICIT or simply wrong. - # In all cases it was jpeg, so default to it - imageformat = MP4Cover.FORMAT_JPEG - cover = MP4Cover(data[pos + 16:pos + length], imageformat) - values.append(cover) - pos += length - - key = _name2key(atom.name) - self.__add(key, values) - - def __render_cover(self, key, value): - atom_data = [] - for cover in value: - try: - imageformat = cover.imageformat - except AttributeError: - imageformat = MP4Cover.FORMAT_JPEG - atom_data.append(Atom.render( - b"data", struct.pack(">2I", imageformat, 0) + cover)) - return Atom.render(_key2name(key), b"".join(atom_data)) - - def __parse_text(self, atom, data, implicit=True): - # implicit = False, for parsing unknown atoms only take utf8 ones. - # For known ones we can assume the implicit are utf8 too. - values = [] - for version, flags, atom_data in self.__parse_data(atom, data): - if implicit: - if flags not in (AtomDataType.IMPLICIT, AtomDataType.UTF8): - raise MP4MetadataError( - "Unknown atom type %r for %r" % (flags, atom.name)) - else: - if flags != AtomDataType.UTF8: - raise MP4MetadataError( - "%r is not text, ignore" % atom.name) - - try: - text = atom_data.decode("utf-8") - except UnicodeDecodeError as e: - raise MP4MetadataError("%s: %s" % (_name2key(atom.name), e)) - - values.append(text) - - key = _name2key(atom.name) - self.__add(key, values) - - def __render_text(self, key, value, flags=AtomDataType.UTF8): - if isinstance(value, string_types): - value = [value] - - encoded = [] - for v in value: - if not isinstance(v, text_type): - if PY3: - raise TypeError("%r not str" % v) - v = v.decode("utf-8") - encoded.append(v.encode("utf-8")) - - return self.__render_data(key, 0, flags, encoded) - - def delete(self, filename): - """Remove the metadata from the given filename.""" - - self._failed_atoms.clear() - self.clear() - self.save(filename, padding=lambda x: 0) - - __atoms = { - b"----": (__parse_freeform, __render_freeform), - b"trkn": (__parse_pair, __render_pair), - b"disk": (__parse_pair, __render_pair_no_trailing), - b"gnre": (__parse_genre, None), - b"tmpo": (__parse_tempo, __render_tempo), - b"cpil": (__parse_bool, __render_bool), - b"pgap": (__parse_bool, __render_bool), - b"pcst": (__parse_bool, __render_bool), - b"covr": (__parse_cover, __render_cover), - b"purl": (__parse_text, __render_text), - b"egid": (__parse_text, __render_text), - } - - # these allow implicit flags and parse as text - for name in [b"\xa9nam", b"\xa9alb", b"\xa9ART", b"aART", b"\xa9wrt", - b"\xa9day", b"\xa9cmt", b"desc", b"purd", b"\xa9grp", - b"\xa9gen", b"\xa9lyr", b"catg", b"keyw", b"\xa9too", - b"cprt", b"soal", b"soaa", b"soar", b"sonm", b"soco", - b"sosn", b"tvsh"]: - __atoms[name] = (__parse_text, __render_text) - - def pprint(self): - - def to_line(key, value): - assert isinstance(key, text_type) - if isinstance(value, text_type): - return u"%s=%s" % (key, value) - return u"%s=%r" % (key, value) - - values = [] - for key, value in sorted(iteritems(self)): - if not isinstance(key, text_type): - key = key.decode("latin-1") - if key == "covr": - values.append(u"%s=%s" % (key, u", ".join( - [u"[%d bytes of data]" % len(data) for data in value]))) - elif isinstance(value, list): - for v in value: - values.append(to_line(key, v)) - else: - values.append(to_line(key, value)) - return u"\n".join(values) - - -class MP4Info(StreamInfo): - """MPEG-4 stream information. - - Attributes: - - * bitrate -- bitrate in bits per second, as an int - * length -- file length in seconds, as a float - * channels -- number of audio channels - * sample_rate -- audio sampling rate in Hz - * bits_per_sample -- bits per sample - * codec (string): - * if starting with ``"mp4a"`` uses an mp4a audio codec - (see the codec parameter in rfc6381 for details e.g. ``"mp4a.40.2"``) - * for everything else see a list of possible values at - http://www.mp4ra.org/codecs.html - - e.g. ``"mp4a"``, ``"alac"``, ``"mp4a.40.2"``, ``"ac-3"`` etc. - * codec_description (string): - Name of the codec used (ALAC, AAC LC, AC-3...). Values might change in - the future, use for display purposes only. - """ - - bitrate = 0 - channels = 0 - sample_rate = 0 - bits_per_sample = 0 - codec = u"" - codec_name = u"" - - def __init__(self, atoms, fileobj): - try: - moov = atoms[b"moov"] - except KeyError: - raise MP4StreamInfoError("not a MP4 file") - - for trak in moov.findall(b"trak"): - hdlr = trak[b"mdia", b"hdlr"] - ok, data = hdlr.read(fileobj) - if not ok: - raise MP4StreamInfoError("Not enough data") - if data[8:12] == b"soun": - break - else: - raise MP4StreamInfoError("track has no audio data") - - mdhd = trak[b"mdia", b"mdhd"] - ok, data = mdhd.read(fileobj) - if not ok: - raise MP4StreamInfoError("Not enough data") - - try: - version, flags, data = parse_full_atom(data) - except ValueError as e: - raise MP4StreamInfoError(e) - - if version == 0: - offset = 8 - fmt = ">2I" - elif version == 1: - offset = 16 - fmt = ">IQ" - else: - raise MP4StreamInfoError("Unknown mdhd version %d" % version) - - end = offset + struct.calcsize(fmt) - unit, length = struct.unpack(fmt, data[offset:end]) - try: - self.length = float(length) / unit - except ZeroDivisionError: - self.length = 0 - - try: - atom = trak[b"mdia", b"minf", b"stbl", b"stsd"] - except KeyError: - pass - else: - self._parse_stsd(atom, fileobj) - - def _parse_stsd(self, atom, fileobj): - """Sets channels, bits_per_sample, sample_rate and optionally bitrate. - - Can raise MP4StreamInfoError. - """ - - assert atom.name == b"stsd" - - ok, data = atom.read(fileobj) - if not ok: - raise MP4StreamInfoError("Invalid stsd") - - try: - version, flags, data = parse_full_atom(data) - except ValueError as e: - raise MP4StreamInfoError(e) - - if version != 0: - raise MP4StreamInfoError("Unsupported stsd version") - - try: - num_entries, offset = cdata.uint32_be_from(data, 0) - except cdata.error as e: - raise MP4StreamInfoError(e) - - if num_entries == 0: - return - - # look at the first entry if there is one - entry_fileobj = cBytesIO(data[offset:]) - try: - entry_atom = Atom(entry_fileobj) - except AtomError as e: - raise MP4StreamInfoError(e) - - try: - entry = AudioSampleEntry(entry_atom, entry_fileobj) - except ASEntryError as e: - raise MP4StreamInfoError(e) - else: - self.channels = entry.channels - self.bits_per_sample = entry.sample_size - self.sample_rate = entry.sample_rate - self.bitrate = entry.bitrate - self.codec = entry.codec - self.codec_description = entry.codec_description - - def pprint(self): - return "MPEG-4 audio (%s), %.2f seconds, %d bps" % ( - self.codec_description, self.length, self.bitrate) - - -class MP4(FileType): - """An MPEG-4 audio file, probably containing AAC. - - If more than one track is present in the file, the first is used. - Only audio ('soun') tracks will be read. - - :ivar info: :class:`MP4Info` - :ivar tags: :class:`MP4Tags` - """ - - MP4Tags = MP4Tags - - _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] - - def load(self, filename): - self.filename = filename - with open(filename, "rb") as fileobj: - try: - atoms = Atoms(fileobj) - except AtomError as err: - reraise(error, err, sys.exc_info()[2]) - - try: - self.info = MP4Info(atoms, fileobj) - except error: - raise - except Exception as err: - reraise(MP4StreamInfoError, err, sys.exc_info()[2]) - - if not MP4Tags._can_load(atoms): - self.tags = None - self._padding = 0 - else: - try: - self.tags = self.MP4Tags(atoms, fileobj) - except error: - raise - except Exception as err: - reraise(MP4MetadataError, err, sys.exc_info()[2]) - else: - self._padding = self.tags._padding - - def add_tags(self): - if self.tags is None: - self.tags = self.MP4Tags() - else: - raise error("an MP4 tag already exists") - - @staticmethod - def score(filename, fileobj, header_data): - return (b"ftyp" in header_data) + (b"mp4" in header_data) - - -Open = MP4 - - -def delete(filename): - """Remove tags from a file.""" - - MP4(filename).delete() diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index de968da940fc35ac7e446528711bd4b0f1e18e24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31145 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 diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc deleted file mode 100644 index 31483c463fa273b25f89cbb8fab1e7d451fa97b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14269 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 NQ`65(e{uSE{s(x{fz$v1 diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc deleted file mode 100644 index f4abf385e2cb9aadc4560b18c414dfbf0ef1a9cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6248 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 L*-CESn(zGw2^KqW diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc deleted file mode 100644 index e7df30cc48b2f9edb33acfd449a0cb20fcf0e36c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 590 zcmYjN%}OId5U!rdY$R(|@u2L%rH?z{=CHe7L_`SQM1-KQG7viHPBLpx_t4!VMD*g% z3-}hkQeQpg=6O$6w}G{os;R24s`{(i>~=d(qt~-P3BU_{5RBmv)lxJZKLQe<B5*-~ z@C0}Y4*(bFv|x~D3#@{i_-xFXV`CCRloT~X?W5Wm8V7I#a8JU#O91l?9QsMf+!1g| zKyfq<eN5mhW^XZ@!rvC~EMn-8h;0QB>j~Vvcj25*5t*dOsC~@`x)PK*U0KSEpjX1! zQY*oGbWqFEQ8yD6ZE9L7`h5xR(1~PIyF+U>mzwds*P$qVrl?`174)3RMjRQV%{#Ja z;ZoOYN;@i)Zl*ID_BUbwMKCsA;v~;}Duwc$|NTDIhMRC^-)>6fh9i-uz7^Vidr8M< zk<K%iRjm8>v%goI&fQF_U%SO|IVx=F#E+VdFWFSsqG}xeRZ-Qyi(%tR*{kP%t!BoG z;iQpr7;f=7km{g;Y=f*On`E1qFBqNR<K5+iUhIzCA{Jl&V7gfESN1E-8z~M}FtTBf G_4Ex6hm83E diff --git a/resources/lib/libraries/mutagen/mp4/_as_entry.py b/resources/lib/libraries/mutagen/mp4/_as_entry.py deleted file mode 100644 index 306d5720..00000000 --- a/resources/lib/libraries/mutagen/mp4/_as_entry.py +++ /dev/null @@ -1,542 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -from mutagen._compat import cBytesIO, xrange -from mutagen.aac import ProgramConfigElement -from mutagen._util import BitReader, BitReaderError, cdata -from mutagen._compat import text_type -from ._util import parse_full_atom -from ._atom import Atom, AtomError - - -class ASEntryError(Exception): - pass - - -class AudioSampleEntry(object): - """Parses an AudioSampleEntry atom. - - Private API. - - Attrs: - channels (int): number of channels - sample_size (int): sample size in bits - sample_rate (int): sample rate in Hz - bitrate (int): bits per second (0 means unknown) - codec (string): - audio codec, either 'mp4a[.*][.*]' (rfc6381) or 'alac' - codec_description (string): descriptive codec name e.g. "AAC LC+SBR" - - Can raise ASEntryError. - """ - - channels = 0 - sample_size = 0 - sample_rate = 0 - bitrate = 0 - codec = None - codec_description = None - - def __init__(self, atom, fileobj): - ok, data = atom.read(fileobj) - if not ok: - raise ASEntryError("too short %r atom" % atom.name) - - fileobj = cBytesIO(data) - r = BitReader(fileobj) - - try: - # SampleEntry - r.skip(6 * 8) # reserved - r.skip(2 * 8) # data_ref_index - - # AudioSampleEntry - r.skip(8 * 8) # reserved - self.channels = r.bits(16) - self.sample_size = r.bits(16) - r.skip(2 * 8) # pre_defined - r.skip(2 * 8) # reserved - self.sample_rate = r.bits(32) >> 16 - except BitReaderError as e: - raise ASEntryError(e) - - assert r.is_aligned() - - try: - extra = Atom(fileobj) - except AtomError as e: - raise ASEntryError(e) - - self.codec = atom.name.decode("latin-1") - self.codec_description = None - - if atom.name == b"mp4a" and extra.name == b"esds": - self._parse_esds(extra, fileobj) - elif atom.name == b"alac" and extra.name == b"alac": - self._parse_alac(extra, fileobj) - elif atom.name == b"ac-3" and extra.name == b"dac3": - self._parse_dac3(extra, fileobj) - - if self.codec_description is None: - self.codec_description = self.codec.upper() - - def _parse_dac3(self, atom, fileobj): - # ETSI TS 102 366 - - assert atom.name == b"dac3" - - ok, data = atom.read(fileobj) - if not ok: - raise ASEntryError("truncated %s atom" % atom.name) - fileobj = cBytesIO(data) - r = BitReader(fileobj) - - # sample_rate in AudioSampleEntry covers values in - # fscod2 and not just fscod, so ignore fscod here. - try: - r.skip(2 + 5 + 3) # fscod, bsid, bsmod - acmod = r.bits(3) - lfeon = r.bits(1) - bit_rate_code = r.bits(5) - r.skip(5) # reserved - except BitReaderError as e: - raise ASEntryError(e) - - self.channels = [2, 1, 2, 3, 3, 4, 4, 5][acmod] + lfeon - - try: - self.bitrate = [ - 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, - 224, 256, 320, 384, 448, 512, 576, 640][bit_rate_code] * 1000 - except IndexError: - pass - - def _parse_alac(self, atom, fileobj): - # https://alac.macosforge.org/trac/browser/trunk/ - # ALACMagicCookieDescription.txt - - assert atom.name == b"alac" - - ok, data = atom.read(fileobj) - if not ok: - raise ASEntryError("truncated %s atom" % atom.name) - - try: - version, flags, data = parse_full_atom(data) - except ValueError as e: - raise ASEntryError(e) - - if version != 0: - raise ASEntryError("Unsupported version %d" % version) - - fileobj = cBytesIO(data) - r = BitReader(fileobj) - - try: - # for some files the AudioSampleEntry values default to 44100/2chan - # and the real info is in the alac cookie, so prefer it - r.skip(32) # frameLength - compatibleVersion = r.bits(8) - if compatibleVersion != 0: - return - self.sample_size = r.bits(8) - r.skip(8 + 8 + 8) - self.channels = r.bits(8) - r.skip(16 + 32) - self.bitrate = r.bits(32) - self.sample_rate = r.bits(32) - except BitReaderError as e: - raise ASEntryError(e) - - def _parse_esds(self, esds, fileobj): - assert esds.name == b"esds" - - ok, data = esds.read(fileobj) - if not ok: - raise ASEntryError("truncated %s atom" % esds.name) - - try: - version, flags, data = parse_full_atom(data) - except ValueError as e: - raise ASEntryError(e) - - if version != 0: - raise ASEntryError("Unsupported version %d" % version) - - fileobj = cBytesIO(data) - r = BitReader(fileobj) - - try: - tag = r.bits(8) - if tag != ES_Descriptor.TAG: - raise ASEntryError("unexpected descriptor: %d" % tag) - assert r.is_aligned() - except BitReaderError as e: - raise ASEntryError(e) - - try: - decSpecificInfo = ES_Descriptor.parse(fileobj) - except DescriptorError as e: - raise ASEntryError(e) - dec_conf_desc = decSpecificInfo.decConfigDescr - - self.bitrate = dec_conf_desc.avgBitrate - self.codec += dec_conf_desc.codec_param - self.codec_description = dec_conf_desc.codec_desc - - decSpecificInfo = dec_conf_desc.decSpecificInfo - if decSpecificInfo is not None: - if decSpecificInfo.channels != 0: - self.channels = decSpecificInfo.channels - - if decSpecificInfo.sample_rate != 0: - self.sample_rate = decSpecificInfo.sample_rate - - -class DescriptorError(Exception): - pass - - -class BaseDescriptor(object): - - TAG = None - - @classmethod - def _parse_desc_length_file(cls, fileobj): - """May raise ValueError""" - - value = 0 - for i in xrange(4): - try: - b = cdata.uint8(fileobj.read(1)) - except cdata.error as e: - raise ValueError(e) - value = (value << 7) | (b & 0x7f) - if not b >> 7: - break - else: - raise ValueError("invalid descriptor length") - - return value - - @classmethod - def parse(cls, fileobj): - """Returns a parsed instance of the called type. - The file position is right after the descriptor after this returns. - - Raises DescriptorError - """ - - try: - length = cls._parse_desc_length_file(fileobj) - except ValueError as e: - raise DescriptorError(e) - pos = fileobj.tell() - instance = cls(fileobj, length) - left = length - (fileobj.tell() - pos) - if left < 0: - raise DescriptorError("descriptor parsing read too much data") - fileobj.seek(left, 1) - return instance - - -class ES_Descriptor(BaseDescriptor): - - TAG = 0x3 - - def __init__(self, fileobj, length): - """Raises DescriptorError""" - - r = BitReader(fileobj) - try: - self.ES_ID = r.bits(16) - self.streamDependenceFlag = r.bits(1) - self.URL_Flag = r.bits(1) - self.OCRstreamFlag = r.bits(1) - self.streamPriority = r.bits(5) - if self.streamDependenceFlag: - self.dependsOn_ES_ID = r.bits(16) - if self.URL_Flag: - URLlength = r.bits(8) - self.URLstring = r.bytes(URLlength) - if self.OCRstreamFlag: - self.OCR_ES_Id = r.bits(16) - - tag = r.bits(8) - except BitReaderError as e: - raise DescriptorError(e) - - if tag != DecoderConfigDescriptor.TAG: - raise DescriptorError("unexpected DecoderConfigDescrTag %d" % tag) - - assert r.is_aligned() - self.decConfigDescr = DecoderConfigDescriptor.parse(fileobj) - - -class DecoderConfigDescriptor(BaseDescriptor): - - TAG = 0x4 - - decSpecificInfo = None - """A DecoderSpecificInfo, optional""" - - def __init__(self, fileobj, length): - """Raises DescriptorError""" - - r = BitReader(fileobj) - - try: - self.objectTypeIndication = r.bits(8) - self.streamType = r.bits(6) - self.upStream = r.bits(1) - self.reserved = r.bits(1) - self.bufferSizeDB = r.bits(24) - self.maxBitrate = r.bits(32) - self.avgBitrate = r.bits(32) - - if (self.objectTypeIndication, self.streamType) != (0x40, 0x5): - return - - # all from here is optional - if length * 8 == r.get_position(): - return - - tag = r.bits(8) - except BitReaderError as e: - raise DescriptorError(e) - - if tag == DecoderSpecificInfo.TAG: - assert r.is_aligned() - self.decSpecificInfo = DecoderSpecificInfo.parse(fileobj) - - @property - def codec_param(self): - """string""" - - param = u".%X" % self.objectTypeIndication - info = self.decSpecificInfo - if info is not None: - param += u".%d" % info.audioObjectType - return param - - @property - def codec_desc(self): - """string or None""" - - info = self.decSpecificInfo - desc = None - if info is not None: - desc = info.description - return desc - - -class DecoderSpecificInfo(BaseDescriptor): - - TAG = 0x5 - - _TYPE_NAMES = [ - None, "AAC MAIN", "AAC LC", "AAC SSR", "AAC LTP", "SBR", - "AAC scalable", "TwinVQ", "CELP", "HVXC", None, None, "TTSI", - "Main synthetic", "Wavetable synthesis", "General MIDI", - "Algorithmic Synthesis and Audio FX", "ER AAC LC", None, "ER AAC LTP", - "ER AAC scalable", "ER Twin VQ", "ER BSAC", "ER AAC LD", "ER CELP", - "ER HVXC", "ER HILN", "ER Parametric", "SSC", "PS", "MPEG Surround", - None, "Layer-1", "Layer-2", "Layer-3", "DST", "ALS", "SLS", - "SLS non-core", "ER AAC ELD", "SMR Simple", "SMR Main", "USAC", - "SAOC", "LD MPEG Surround", "USAC" - ] - - _FREQS = [ - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, - 12000, 11025, 8000, 7350, - ] - - @property - def description(self): - """string or None if unknown""" - - name = None - try: - name = self._TYPE_NAMES[self.audioObjectType] - except IndexError: - pass - if name is None: - return - if self.sbrPresentFlag == 1: - name += "+SBR" - if self.psPresentFlag == 1: - name += "+PS" - return text_type(name) - - @property - def sample_rate(self): - """0 means unknown""" - - if self.sbrPresentFlag == 1: - return self.extensionSamplingFrequency - elif self.sbrPresentFlag == 0: - return self.samplingFrequency - else: - # these are all types that support SBR - aot_can_sbr = (1, 2, 3, 4, 6, 17, 19, 20, 22) - if self.audioObjectType not in aot_can_sbr: - return self.samplingFrequency - # there shouldn't be SBR for > 48KHz - if self.samplingFrequency > 24000: - return self.samplingFrequency - # either samplingFrequency or samplingFrequency * 2 - return 0 - - @property - def channels(self): - """channel count or 0 for unknown""" - - # from ProgramConfigElement() - if hasattr(self, "pce_channels"): - return self.pce_channels - - conf = getattr( - self, "extensionChannelConfiguration", self.channelConfiguration) - - if conf == 1: - if self.psPresentFlag == -1: - return 0 - elif self.psPresentFlag == 1: - return 2 - else: - return 1 - elif conf == 7: - return 8 - elif conf > 7: - return 0 - else: - return conf - - def _get_audio_object_type(self, r): - """Raises BitReaderError""" - - audioObjectType = r.bits(5) - if audioObjectType == 31: - audioObjectTypeExt = r.bits(6) - audioObjectType = 32 + audioObjectTypeExt - return audioObjectType - - def _get_sampling_freq(self, r): - """Raises BitReaderError""" - - samplingFrequencyIndex = r.bits(4) - if samplingFrequencyIndex == 0xf: - samplingFrequency = r.bits(24) - else: - try: - samplingFrequency = self._FREQS[samplingFrequencyIndex] - except IndexError: - samplingFrequency = 0 - return samplingFrequency - - def __init__(self, fileobj, length): - """Raises DescriptorError""" - - r = BitReader(fileobj) - try: - self._parse(r, length) - except BitReaderError as e: - raise DescriptorError(e) - - def _parse(self, r, length): - """Raises BitReaderError""" - - def bits_left(): - return length * 8 - r.get_position() - - self.audioObjectType = self._get_audio_object_type(r) - self.samplingFrequency = self._get_sampling_freq(r) - self.channelConfiguration = r.bits(4) - - self.sbrPresentFlag = -1 - self.psPresentFlag = -1 - if self.audioObjectType in (5, 29): - self.extensionAudioObjectType = 5 - self.sbrPresentFlag = 1 - if self.audioObjectType == 29: - self.psPresentFlag = 1 - self.extensionSamplingFrequency = self._get_sampling_freq(r) - self.audioObjectType = self._get_audio_object_type(r) - if self.audioObjectType == 22: - self.extensionChannelConfiguration = r.bits(4) - else: - self.extensionAudioObjectType = 0 - - if self.audioObjectType in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23): - try: - GASpecificConfig(r, self) - except NotImplementedError: - # unsupported, (warn?) - return - else: - # unsupported - return - - if self.audioObjectType in ( - 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39): - epConfig = r.bits(2) - if epConfig in (2, 3): - # unsupported - return - - if self.extensionAudioObjectType != 5 and bits_left() >= 16: - syncExtensionType = r.bits(11) - if syncExtensionType == 0x2b7: - self.extensionAudioObjectType = self._get_audio_object_type(r) - - if self.extensionAudioObjectType == 5: - self.sbrPresentFlag = r.bits(1) - if self.sbrPresentFlag == 1: - self.extensionSamplingFrequency = \ - self._get_sampling_freq(r) - if bits_left() >= 12: - syncExtensionType = r.bits(11) - if syncExtensionType == 0x548: - self.psPresentFlag = r.bits(1) - - if self.extensionAudioObjectType == 22: - self.sbrPresentFlag = r.bits(1) - if self.sbrPresentFlag == 1: - self.extensionSamplingFrequency = \ - self._get_sampling_freq(r) - self.extensionChannelConfiguration = r.bits(4) - - -def GASpecificConfig(r, info): - """Reads GASpecificConfig which is needed to get the data after that - (there is no length defined to skip it) and to read program_config_element - which can contain channel counts. - - May raise BitReaderError on error or - NotImplementedError if some reserved data was set. - """ - - assert isinstance(info, DecoderSpecificInfo) - - r.skip(1) # frameLengthFlag - dependsOnCoreCoder = r.bits(1) - if dependsOnCoreCoder: - r.skip(14) - extensionFlag = r.bits(1) - if not info.channelConfiguration: - pce = ProgramConfigElement(r) - info.pce_channels = pce.channels - if info.audioObjectType == 6 or info.audioObjectType == 20: - r.skip(3) - if extensionFlag: - if info.audioObjectType == 22: - r.skip(5 + 11) - if info.audioObjectType in (17, 19, 20, 23): - r.skip(1 + 1 + 1) - extensionFlag3 = r.bits(1) - if extensionFlag3 != 0: - raise NotImplementedError("extensionFlag3 set") diff --git a/resources/lib/libraries/mutagen/mp4/_atom.py b/resources/lib/libraries/mutagen/mp4/_atom.py deleted file mode 100644 index f73eb556..00000000 --- a/resources/lib/libraries/mutagen/mp4/_atom.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -import struct - -from mutagen._compat import PY2 - -# This is not an exhaustive list of container atoms, but just the -# ones this module needs to peek inside. -_CONTAINERS = [b"moov", b"udta", b"trak", b"mdia", b"meta", b"ilst", - b"stbl", b"minf", b"moof", b"traf"] -_SKIP_SIZE = {b"meta": 4} - - -class AtomError(Exception): - pass - - -class Atom(object): - """An individual atom. - - Attributes: - children -- list child atoms (or None for non-container atoms) - length -- length of this atom, including length and name - datalength = -- length of this atom without length, name - name -- four byte name of the atom, as a str - offset -- location in the constructor-given fileobj of this atom - - This structure should only be used internally by Mutagen. - """ - - children = None - - def __init__(self, fileobj, level=0): - """May raise AtomError""" - - self.offset = fileobj.tell() - try: - self.length, self.name = struct.unpack(">I4s", fileobj.read(8)) - except struct.error: - raise AtomError("truncated data") - self._dataoffset = self.offset + 8 - if self.length == 1: - try: - self.length, = struct.unpack(">Q", fileobj.read(8)) - except struct.error: - raise AtomError("truncated data") - self._dataoffset += 8 - if self.length < 16: - raise AtomError( - "64 bit atom length can only be 16 and higher") - elif self.length == 0: - if level != 0: - raise AtomError( - "only a top-level atom can have zero length") - # Only the last atom is supposed to have a zero-length, meaning it - # extends to the end of file. - fileobj.seek(0, 2) - self.length = fileobj.tell() - self.offset - fileobj.seek(self.offset + 8, 0) - elif self.length < 8: - raise AtomError( - "atom length can only be 0, 1 or 8 and higher") - - if self.name in _CONTAINERS: - self.children = [] - fileobj.seek(_SKIP_SIZE.get(self.name, 0), 1) - while fileobj.tell() < self.offset + self.length: - self.children.append(Atom(fileobj, level + 1)) - else: - fileobj.seek(self.offset + self.length, 0) - - @property - def datalength(self): - return self.length - (self._dataoffset - self.offset) - - def read(self, fileobj): - """Return if all data could be read and the atom payload""" - - fileobj.seek(self._dataoffset, 0) - data = fileobj.read(self.datalength) - return len(data) == self.datalength, data - - @staticmethod - def render(name, data): - """Render raw atom data.""" - # this raises OverflowError if Py_ssize_t can't handle the atom data - size = len(data) + 8 - if size <= 0xFFFFFFFF: - return struct.pack(">I4s", size, name) + data - else: - return struct.pack(">I4sQ", 1, name, size + 8) + data - - def findall(self, name, recursive=False): - """Recursively find all child atoms by specified name.""" - if self.children is not None: - for child in self.children: - if child.name == name: - yield child - if recursive: - for atom in child.findall(name, True): - yield atom - - def __getitem__(self, remaining): - """Look up a child atom, potentially recursively. - - e.g. atom['udta', 'meta'] => <Atom name='meta' ...> - """ - if not remaining: - return self - elif self.children is None: - raise KeyError("%r is not a container" % self.name) - for child in self.children: - if child.name == remaining[0]: - return child[remaining[1:]] - else: - raise KeyError("%r not found" % remaining[0]) - - def __repr__(self): - cls = self.__class__.__name__ - if self.children is None: - return "<%s name=%r length=%r offset=%r>" % ( - cls, self.name, self.length, self.offset) - else: - children = "\n".join([" " + line for child in self.children - for line in repr(child).splitlines()]) - return "<%s name=%r length=%r offset=%r\n%s>" % ( - cls, self.name, self.length, self.offset, children) - - -class Atoms(object): - """Root atoms in a given file. - - Attributes: - atoms -- a list of top-level atoms as Atom objects - - This structure should only be used internally by Mutagen. - """ - - def __init__(self, fileobj): - self.atoms = [] - fileobj.seek(0, 2) - end = fileobj.tell() - fileobj.seek(0) - while fileobj.tell() + 8 <= end: - self.atoms.append(Atom(fileobj)) - - def path(self, *names): - """Look up and return the complete path of an atom. - - For example, atoms.path('moov', 'udta', 'meta') will return a - list of three atoms, corresponding to the moov, udta, and meta - atoms. - """ - - path = [self] - for name in names: - path.append(path[-1][name, ]) - return path[1:] - - def __contains__(self, names): - try: - self[names] - except KeyError: - return False - return True - - def __getitem__(self, names): - """Look up a child atom. - - 'names' may be a list of atoms (['moov', 'udta']) or a string - specifying the complete path ('moov.udta'). - """ - - if PY2: - if isinstance(names, basestring): - names = names.split(b".") - else: - if isinstance(names, bytes): - names = names.split(b".") - - for child in self.atoms: - if child.name == names[0]: - return child[names[1:]] - else: - raise KeyError("%r not found" % names[0]) - - def __repr__(self): - return "\n".join([repr(child) for child in self.atoms]) diff --git a/resources/lib/libraries/mutagen/mp4/_util.py b/resources/lib/libraries/mutagen/mp4/_util.py deleted file mode 100644 index 9583334a..00000000 --- a/resources/lib/libraries/mutagen/mp4/_util.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -from mutagen._util import cdata - - -def parse_full_atom(data): - """Some atoms are versioned. Split them up in (version, flags, payload). - Can raise ValueError. - """ - - if len(data) < 4: - raise ValueError("not enough data") - - version = ord(data[0:1]) - flags = cdata.uint_be(b"\x00" + data[1:4]) - return version, flags, data[4:] diff --git a/resources/lib/libraries/mutagen/musepack.py b/resources/lib/libraries/mutagen/musepack.py deleted file mode 100644 index 7880958b..00000000 --- a/resources/lib/libraries/mutagen/musepack.py +++ /dev/null @@ -1,270 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Lukas Lalinsky -# Copyright (C) 2012 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Musepack audio streams with APEv2 tags. - -Musepack is an audio format originally based on the MPEG-1 Layer-2 -algorithms. Stream versions 4 through 7 are supported. - -For more information, see http://www.musepack.net/. -""" - -__all__ = ["Musepack", "Open", "delete"] - -import struct - -from ._compat import endswith, xrange -from mutagen import StreamInfo -from mutagen.apev2 import APEv2File, error, delete -from mutagen.id3 import BitPaddedInt -from mutagen._util import cdata - - -class MusepackHeaderError(error): - pass - - -RATES = [44100, 48000, 37800, 32000] - - -def _parse_sv8_int(fileobj, limit=9): - """Reads (max limit) bytes from fileobj until the MSB is zero. - All 7 LSB will be merged to a big endian uint. - - Raises ValueError in case not MSB is zero, or EOFError in - case the file ended before limit is reached. - - Returns (parsed number, number of bytes read) - """ - - num = 0 - for i in xrange(limit): - c = fileobj.read(1) - if len(c) != 1: - raise EOFError - c = bytearray(c) - num = (num << 7) | (c[0] & 0x7F) - if not c[0] & 0x80: - return num, i + 1 - if limit > 0: - raise ValueError - return 0, 0 - - -def _calc_sv8_gain(gain): - # 64.82 taken from mpcdec - return 64.82 - gain / 256.0 - - -def _calc_sv8_peak(peak): - return (10 ** (peak / (256.0 * 20.0)) / 65535.0) - - -class MusepackInfo(StreamInfo): - """Musepack stream information. - - Attributes: - - * channels -- number of audio channels - * length -- file length in seconds, as a float - * sample_rate -- audio sampling rate in Hz - * bitrate -- audio bitrate, in bits per second - * version -- Musepack stream version - - Optional Attributes: - - * title_gain, title_peak -- Replay Gain and peak data for this song - * album_gain, album_peak -- Replay Gain and peak data for this album - - These attributes are only available in stream version 7/8. The - gains are a float, +/- some dB. The peaks are a percentage [0..1] of - the maximum amplitude. This means to get a number comparable to - VorbisGain, you must multiply the peak by 2. - """ - - def __init__(self, fileobj): - header = fileobj.read(4) - if len(header) != 4: - raise MusepackHeaderError("not a Musepack file") - - # Skip ID3v2 tags - if header[:3] == b"ID3": - header = fileobj.read(6) - if len(header) != 6: - raise MusepackHeaderError("not a Musepack file") - size = 10 + BitPaddedInt(header[2:6]) - fileobj.seek(size) - header = fileobj.read(4) - if len(header) != 4: - raise MusepackHeaderError("not a Musepack file") - - if header.startswith(b"MPCK"): - self.__parse_sv8(fileobj) - else: - self.__parse_sv467(fileobj) - - if not self.bitrate and self.length != 0: - fileobj.seek(0, 2) - self.bitrate = int(round(fileobj.tell() * 8 / self.length)) - - def __parse_sv8(self, fileobj): - # SV8 http://trac.musepack.net/trac/wiki/SV8Specification - - key_size = 2 - mandatory_packets = [b"SH", b"RG"] - - def check_frame_key(key): - if ((len(frame_type) != key_size) or - (not b'AA' <= frame_type <= b'ZZ')): - raise MusepackHeaderError("Invalid frame key.") - - frame_type = fileobj.read(key_size) - check_frame_key(frame_type) - - while frame_type not in (b"AP", b"SE") and mandatory_packets: - try: - frame_size, slen = _parse_sv8_int(fileobj) - except (EOFError, ValueError): - raise MusepackHeaderError("Invalid packet size.") - data_size = frame_size - key_size - slen - # packets can be at maximum data_size big and are padded with zeros - - if frame_type == b"SH": - mandatory_packets.remove(frame_type) - self.__parse_stream_header(fileobj, data_size) - elif frame_type == b"RG": - mandatory_packets.remove(frame_type) - self.__parse_replaygain_packet(fileobj, data_size) - else: - fileobj.seek(data_size, 1) - - frame_type = fileobj.read(key_size) - check_frame_key(frame_type) - - if mandatory_packets: - raise MusepackHeaderError("Missing mandatory packets: %s." % - ", ".join(map(repr, mandatory_packets))) - - self.length = float(self.samples) / self.sample_rate - self.bitrate = 0 - - def __parse_stream_header(self, fileobj, data_size): - # skip CRC - fileobj.seek(4, 1) - remaining_size = data_size - 4 - - try: - self.version = bytearray(fileobj.read(1))[0] - except TypeError: - raise MusepackHeaderError("SH packet ended unexpectedly.") - - remaining_size -= 1 - - try: - samples, l1 = _parse_sv8_int(fileobj) - samples_skip, l2 = _parse_sv8_int(fileobj) - except (EOFError, ValueError): - raise MusepackHeaderError( - "SH packet: Invalid sample counts.") - - self.samples = samples - samples_skip - remaining_size -= l1 + l2 - - data = fileobj.read(remaining_size) - if len(data) != remaining_size: - raise MusepackHeaderError("SH packet ended unexpectedly.") - self.sample_rate = RATES[bytearray(data)[0] >> 5] - self.channels = (bytearray(data)[1] >> 4) + 1 - - def __parse_replaygain_packet(self, fileobj, data_size): - data = fileobj.read(data_size) - if data_size < 9: - raise MusepackHeaderError("Invalid RG packet size.") - if len(data) != data_size: - raise MusepackHeaderError("RG packet ended unexpectedly.") - title_gain = cdata.short_be(data[1:3]) - title_peak = cdata.short_be(data[3:5]) - album_gain = cdata.short_be(data[5:7]) - album_peak = cdata.short_be(data[7:9]) - if title_gain: - self.title_gain = _calc_sv8_gain(title_gain) - if title_peak: - self.title_peak = _calc_sv8_peak(title_peak) - if album_gain: - self.album_gain = _calc_sv8_gain(album_gain) - if album_peak: - self.album_peak = _calc_sv8_peak(album_peak) - - def __parse_sv467(self, fileobj): - fileobj.seek(-4, 1) - header = fileobj.read(32) - if len(header) != 32: - raise MusepackHeaderError("not a Musepack file") - - # SV7 - if header.startswith(b"MP+"): - self.version = bytearray(header)[3] & 0xF - if self.version < 7: - raise MusepackHeaderError("not a Musepack file") - frames = cdata.uint_le(header[4:8]) - flags = cdata.uint_le(header[8:12]) - - self.title_peak, self.title_gain = struct.unpack( - "<Hh", header[12:16]) - self.album_peak, self.album_gain = struct.unpack( - "<Hh", header[16:20]) - self.title_gain /= 100.0 - self.album_gain /= 100.0 - self.title_peak /= 65535.0 - self.album_peak /= 65535.0 - - self.sample_rate = RATES[(flags >> 16) & 0x0003] - self.bitrate = 0 - # SV4-SV6 - else: - header_dword = cdata.uint_le(header[0:4]) - self.version = (header_dword >> 11) & 0x03FF - if self.version < 4 or self.version > 6: - raise MusepackHeaderError("not a Musepack file") - self.bitrate = (header_dword >> 23) & 0x01FF - self.sample_rate = 44100 - if self.version >= 5: - frames = cdata.uint_le(header[4:8]) - else: - frames = cdata.ushort_le(header[6:8]) - if self.version < 6: - frames -= 1 - self.channels = 2 - self.length = float(frames * 1152 - 576) / self.sample_rate - - def pprint(self): - rg_data = [] - if hasattr(self, "title_gain"): - rg_data.append(u"%+0.2f (title)" % self.title_gain) - if hasattr(self, "album_gain"): - rg_data.append(u"%+0.2f (album)" % self.album_gain) - rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or "" - - return u"Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % ( - self.version, self.length, self.sample_rate, self.bitrate, rg_data) - - -class Musepack(APEv2File): - _Info = MusepackInfo - _mimes = ["audio/x-musepack", "audio/x-mpc"] - - @staticmethod - def score(filename, fileobj, header): - filename = filename.lower() - - return (header.startswith(b"MP+") + header.startswith(b"MPCK") + - endswith(filename, b".mpc")) - - -Open = Musepack diff --git a/resources/lib/libraries/mutagen/ogg.py b/resources/lib/libraries/mutagen/ogg.py deleted file mode 100644 index 9961a966..00000000 --- a/resources/lib/libraries/mutagen/ogg.py +++ /dev/null @@ -1,548 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg bitstreams and pages. - -This module reads and writes a subset of the Ogg bitstream format -version 0. It does *not* read or write Ogg Vorbis files! For that, -you should use mutagen.oggvorbis. - -This implementation is based on the RFC 3533 standard found at -http://www.xiph.org/ogg/doc/rfc3533.txt. -""" - -import struct -import sys -import zlib - -from mutagen import FileType -from mutagen._util import cdata, resize_bytes, MutagenError -from ._compat import cBytesIO, reraise, chr_, izip, xrange - - -class error(IOError, MutagenError): - """Ogg stream parsing errors.""" - - pass - - -class OggPage(object): - """A single Ogg page (not necessarily a single encoded packet). - - A page is a header of 26 bytes, followed by the length of the - data, followed by the data. - - The constructor is givin a file-like object pointing to the start - of an Ogg page. After the constructor is finished it is pointing - to the start of the next page. - - Attributes: - - * version -- stream structure version (currently always 0) - * position -- absolute stream position (default -1) - * serial -- logical stream serial number (default 0) - * sequence -- page sequence number within logical stream (default 0) - * offset -- offset this page was read from (default None) - * complete -- if the last packet on this page is complete (default True) - * packets -- list of raw packet data (default []) - - Note that if 'complete' is false, the next page's 'continued' - property must be true (so set both when constructing pages). - - If a file-like object is supplied to the constructor, the above - attributes will be filled in based on it. - """ - - version = 0 - __type_flags = 0 - position = 0 - serial = 0 - sequence = 0 - offset = None - complete = True - - def __init__(self, fileobj=None): - self.packets = [] - - if fileobj is None: - return - - self.offset = fileobj.tell() - - header = fileobj.read(27) - if len(header) == 0: - raise EOFError - - try: - (oggs, self.version, self.__type_flags, - self.position, self.serial, self.sequence, - crc, segments) = struct.unpack("<4sBBqIIiB", header) - except struct.error: - raise error("unable to read full header; got %r" % header) - - if oggs != b"OggS": - raise error("read %r, expected %r, at 0x%x" % ( - oggs, b"OggS", fileobj.tell() - 27)) - - if self.version != 0: - raise error("version %r unsupported" % self.version) - - total = 0 - lacings = [] - lacing_bytes = fileobj.read(segments) - if len(lacing_bytes) != segments: - raise error("unable to read %r lacing bytes" % segments) - for c in bytearray(lacing_bytes): - total += c - if c < 255: - lacings.append(total) - total = 0 - if total: - lacings.append(total) - self.complete = False - - self.packets = [fileobj.read(l) for l in lacings] - if [len(p) for p in self.packets] != lacings: - raise error("unable to read full data") - - def __eq__(self, other): - """Two Ogg pages are the same if they write the same data.""" - try: - return (self.write() == other.write()) - except AttributeError: - return False - - __hash__ = object.__hash__ - - def __repr__(self): - attrs = ['version', 'position', 'serial', 'sequence', 'offset', - 'complete', 'continued', 'first', 'last'] - values = ["%s=%r" % (attr, getattr(self, attr)) for attr in attrs] - return "<%s %s, %d bytes in %d packets>" % ( - type(self).__name__, " ".join(values), sum(map(len, self.packets)), - len(self.packets)) - - def write(self): - """Return a string encoding of the page header and data. - - A ValueError is raised if the data is too big to fit in a - single page. - """ - - data = [ - struct.pack("<4sBBqIIi", b"OggS", self.version, self.__type_flags, - self.position, self.serial, self.sequence, 0) - ] - - lacing_data = [] - for datum in self.packets: - quot, rem = divmod(len(datum), 255) - lacing_data.append(b"\xff" * quot + chr_(rem)) - lacing_data = b"".join(lacing_data) - if not self.complete and lacing_data.endswith(b"\x00"): - lacing_data = lacing_data[:-1] - data.append(chr_(len(lacing_data))) - data.append(lacing_data) - data.extend(self.packets) - data = b"".join(data) - - # Python's CRC is swapped relative to Ogg's needs. - # crc32 returns uint prior to py2.6 on some platforms, so force uint - crc = (~zlib.crc32(data.translate(cdata.bitswap), -1)) & 0xffffffff - # Although we're using to_uint_be, this actually makes the CRC - # a proper le integer, since Python's CRC is byteswapped. - crc = cdata.to_uint_be(crc).translate(cdata.bitswap) - data = data[:22] + crc + data[26:] - return data - - @property - def size(self): - """Total frame size.""" - - size = 27 # Initial header size - for datum in self.packets: - quot, rem = divmod(len(datum), 255) - size += quot + 1 - if not self.complete and rem == 0: - # Packet contains a multiple of 255 bytes and is not - # terminated, so we don't have a \x00 at the end. - size -= 1 - size += sum(map(len, self.packets)) - return size - - def __set_flag(self, bit, val): - mask = 1 << bit - if val: - self.__type_flags |= mask - else: - self.__type_flags &= ~mask - - continued = property( - lambda self: cdata.test_bit(self.__type_flags, 0), - lambda self, v: self.__set_flag(0, v), - doc="The first packet is continued from the previous page.") - - first = property( - lambda self: cdata.test_bit(self.__type_flags, 1), - lambda self, v: self.__set_flag(1, v), - doc="This is the first page of a logical bitstream.") - - last = property( - lambda self: cdata.test_bit(self.__type_flags, 2), - lambda self, v: self.__set_flag(2, v), - doc="This is the last page of a logical bitstream.") - - @staticmethod - def renumber(fileobj, serial, start): - """Renumber pages belonging to a specified logical stream. - - fileobj must be opened with mode r+b or w+b. - - Starting at page number 'start', renumber all pages belonging - to logical stream 'serial'. Other pages will be ignored. - - fileobj must point to the start of a valid Ogg page; any - occuring after it and part of the specified logical stream - will be numbered. No adjustment will be made to the data in - the pages nor the granule position; only the page number, and - so also the CRC. - - If an error occurs (e.g. non-Ogg data is found), fileobj will - be left pointing to the place in the stream the error occured, - but the invalid data will be left intact (since this function - does not change the total file size). - """ - - number = start - while True: - try: - page = OggPage(fileobj) - except EOFError: - break - else: - if page.serial != serial: - # Wrong stream, skip this page. - continue - # Changing the number can't change the page size, - # so seeking back based on the current size is safe. - fileobj.seek(-page.size, 1) - page.sequence = number - fileobj.write(page.write()) - fileobj.seek(page.offset + page.size, 0) - number += 1 - - @staticmethod - def to_packets(pages, strict=False): - """Construct a list of packet data from a list of Ogg pages. - - If strict is true, the first page must start a new packet, - and the last page must end the last packet. - """ - - serial = pages[0].serial - sequence = pages[0].sequence - packets = [] - - if strict: - if pages[0].continued: - raise ValueError("first packet is continued") - if not pages[-1].complete: - raise ValueError("last packet does not complete") - elif pages and pages[0].continued: - packets.append([b""]) - - for page in pages: - if serial != page.serial: - raise ValueError("invalid serial number in %r" % page) - elif sequence != page.sequence: - raise ValueError("bad sequence number in %r" % page) - else: - sequence += 1 - - if page.continued: - packets[-1].append(page.packets[0]) - else: - packets.append([page.packets[0]]) - packets.extend([p] for p in page.packets[1:]) - - return [b"".join(p) for p in packets] - - @classmethod - def _from_packets_try_preserve(cls, packets, old_pages): - """Like from_packets but in case the size and number of the packets - is the same as in the given pages the layout of the pages will - be copied (the page size and number will match). - - If the packets don't match this behaves like:: - - OggPage.from_packets(packets, sequence=old_pages[0].sequence) - """ - - old_packets = cls.to_packets(old_pages) - - if [len(p) for p in packets] != [len(p) for p in old_packets]: - # doesn't match, fall back - return cls.from_packets(packets, old_pages[0].sequence) - - new_data = b"".join(packets) - new_pages = [] - for old in old_pages: - new = OggPage() - new.sequence = old.sequence - new.complete = old.complete - new.continued = old.continued - new.position = old.position - for p in old.packets: - data, new_data = new_data[:len(p)], new_data[len(p):] - new.packets.append(data) - new_pages.append(new) - assert not new_data - - return new_pages - - @staticmethod - def from_packets(packets, sequence=0, default_size=4096, - wiggle_room=2048): - """Construct a list of Ogg pages from a list of packet data. - - The algorithm will generate pages of approximately - default_size in size (rounded down to the nearest multiple of - 255). However, it will also allow pages to increase to - approximately default_size + wiggle_room if allowing the - wiggle room would finish a packet (only one packet will be - finished in this way per page; if the next packet would fit - into the wiggle room, it still starts on a new page). - - This method reduces packet fragmentation when packet sizes are - slightly larger than the default page size, while still - ensuring most pages are of the average size. - - Pages are numbered started at 'sequence'; other information is - uninitialized. - """ - - chunk_size = (default_size // 255) * 255 - - pages = [] - - page = OggPage() - page.sequence = sequence - - for packet in packets: - page.packets.append(b"") - while packet: - data, packet = packet[:chunk_size], packet[chunk_size:] - if page.size < default_size and len(page.packets) < 255: - page.packets[-1] += data - else: - # If we've put any packet data into this page yet, - # we need to mark it incomplete. However, we can - # also have just started this packet on an already - # full page, in which case, just start the new - # page with this packet. - if page.packets[-1]: - page.complete = False - if len(page.packets) == 1: - page.position = -1 - else: - page.packets.pop(-1) - pages.append(page) - page = OggPage() - page.continued = not pages[-1].complete - page.sequence = pages[-1].sequence + 1 - page.packets.append(data) - - if len(packet) < wiggle_room: - page.packets[-1] += packet - packet = b"" - - if page.packets: - pages.append(page) - - return pages - - @classmethod - def replace(cls, fileobj, old_pages, new_pages): - """Replace old_pages with new_pages within fileobj. - - old_pages must have come from reading fileobj originally. - new_pages are assumed to have the 'same' data as old_pages, - and so the serial and sequence numbers will be copied, as will - the flags for the first and last pages. - - fileobj will be resized and pages renumbered as necessary. As - such, it must be opened r+b or w+b. - """ - - if not len(old_pages) or not len(new_pages): - raise ValueError("empty pages list not allowed") - - # Number the new pages starting from the first old page. - first = old_pages[0].sequence - for page, seq in izip(new_pages, - xrange(first, first + len(new_pages))): - page.sequence = seq - page.serial = old_pages[0].serial - - new_pages[0].first = old_pages[0].first - new_pages[0].last = old_pages[0].last - new_pages[0].continued = old_pages[0].continued - - new_pages[-1].first = old_pages[-1].first - new_pages[-1].last = old_pages[-1].last - new_pages[-1].complete = old_pages[-1].complete - if not new_pages[-1].complete and len(new_pages[-1].packets) == 1: - new_pages[-1].position = -1 - - new_data = [cls.write(p) for p in new_pages] - - # Add dummy data or merge the remaining data together so multiple - # new pages replace an old one - pages_diff = len(old_pages) - len(new_data) - if pages_diff > 0: - new_data.extend([b""] * pages_diff) - elif pages_diff < 0: - new_data[pages_diff - 1:] = [b"".join(new_data[pages_diff - 1:])] - - # Replace pages one by one. If the sizes match no resize happens. - offset_adjust = 0 - new_data_end = None - assert len(old_pages) == len(new_data) - for old_page, data in izip(old_pages, new_data): - offset = old_page.offset + offset_adjust - data_size = len(data) - resize_bytes(fileobj, old_page.size, data_size, offset) - fileobj.seek(offset, 0) - fileobj.write(data) - new_data_end = offset + data_size - offset_adjust += (data_size - old_page.size) - - # Finally, if there's any discrepency in length, we need to - # renumber the pages for the logical stream. - if len(old_pages) != len(new_pages): - fileobj.seek(new_data_end, 0) - serial = new_pages[-1].serial - sequence = new_pages[-1].sequence + 1 - cls.renumber(fileobj, serial, sequence) - - @staticmethod - def find_last(fileobj, serial): - """Find the last page of the stream 'serial'. - - If the file is not multiplexed this function is fast. If it is, - it must read the whole the stream. - - This finds the last page in the actual file object, or the last - page in the stream (with eos set), whichever comes first. - """ - - # For non-muxed streams, look at the last page. - try: - fileobj.seek(-256 * 256, 2) - except IOError: - # The file is less than 64k in length. - fileobj.seek(0) - data = fileobj.read() - try: - index = data.rindex(b"OggS") - except ValueError: - raise error("unable to find final Ogg header") - bytesobj = cBytesIO(data[index:]) - best_page = None - try: - page = OggPage(bytesobj) - except error: - pass - else: - if page.serial == serial: - if page.last: - return page - else: - best_page = page - else: - best_page = None - - # The stream is muxed, so use the slow way. - fileobj.seek(0) - try: - page = OggPage(fileobj) - while not page.last: - page = OggPage(fileobj) - while page.serial != serial: - page = OggPage(fileobj) - best_page = page - return page - except error: - return best_page - except EOFError: - return best_page - - -class OggFileType(FileType): - """An generic Ogg file.""" - - _Info = None - _Tags = None - _Error = None - _mimes = ["application/ogg", "application/x-ogg"] - - def load(self, filename): - """Load file information from a filename.""" - - self.filename = filename - with open(filename, "rb") as fileobj: - try: - self.info = self._Info(fileobj) - self.tags = self._Tags(fileobj, self.info) - self.info._post_tags(fileobj) - except error as e: - reraise(self._Error, e, sys.exc_info()[2]) - except EOFError: - raise self._Error("no appropriate stream found") - - def delete(self, filename=None): - """Remove tags from a file. - - If no filename is given, the one most recently loaded is used. - """ - - if filename is None: - filename = self.filename - - self.tags.clear() - # TODO: we should delegate the deletion to the subclass and not through - # _inject. - with open(filename, "rb+") as fileobj: - try: - self.tags._inject(fileobj, lambda x: 0) - except error as e: - reraise(self._Error, e, sys.exc_info()[2]) - except EOFError: - raise self._Error("no appropriate stream found") - - def add_tags(self): - raise self._Error - - def save(self, filename=None, padding=None): - """Save a tag to a file. - - If no filename is given, the one most recently loaded is used. - """ - - if filename is None: - filename = self.filename - fileobj = open(filename, "rb+") - try: - try: - self.tags._inject(fileobj, padding) - except error as e: - reraise(self._Error, e, sys.exc_info()[2]) - except EOFError: - raise self._Error("no appropriate stream found") - finally: - fileobj.close() diff --git a/resources/lib/libraries/mutagen/oggflac.py b/resources/lib/libraries/mutagen/oggflac.py deleted file mode 100644 index b86226ca..00000000 --- a/resources/lib/libraries/mutagen/oggflac.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg FLAC comments. - -This module handles FLAC files wrapped in an Ogg bitstream. The first -FLAC stream found is used. For 'naked' FLACs, see mutagen.flac. - -This module is based off the specification at -http://flac.sourceforge.net/ogg_mapping.html. -""" - -__all__ = ["OggFLAC", "Open", "delete"] - -import struct - -from ._compat import cBytesIO - -from mutagen import StreamInfo -from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError -from mutagen._vorbis import VCommentDict -from mutagen.ogg import OggPage, OggFileType, error as OggError - - -class error(OggError): - pass - - -class OggFLACHeaderError(error): - pass - - -class OggFLACStreamInfo(StreamInfo): - """Ogg FLAC stream info.""" - - length = 0 - """File length in seconds, as a float""" - - channels = 0 - """Number of channels""" - - sample_rate = 0 - """Sample rate in Hz""" - - def __init__(self, fileobj): - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x7FFLAC"): - page = OggPage(fileobj) - major, minor, self.packets, flac = struct.unpack( - ">BBH4s", page.packets[0][5:13]) - if flac != b"fLaC": - raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac) - elif (major, minor) != (1, 0): - raise OggFLACHeaderError( - "unknown mapping version: %d.%d" % (major, minor)) - self.serial = page.serial - - # Skip over the block header. - stringobj = cBytesIO(page.packets[0][17:]) - - try: - flac_info = FLACStreamInfo(stringobj) - except FLACError as e: - raise OggFLACHeaderError(e) - - for attr in ["min_blocksize", "max_blocksize", "sample_rate", - "channels", "bits_per_sample", "total_samples", "length"]: - setattr(self, attr, getattr(flac_info, attr)) - - def _post_tags(self, fileobj): - if self.length: - return - page = OggPage.find_last(fileobj, self.serial) - self.length = page.position / float(self.sample_rate) - - def pprint(self): - return u"Ogg FLAC, %.2f seconds, %d Hz" % ( - self.length, self.sample_rate) - - -class OggFLACVComment(VCommentDict): - - def __init__(self, fileobj, info): - # data should be pointing at the start of an Ogg page, after - # the first FLAC page. - pages = [] - complete = False - while not complete: - page = OggPage(fileobj) - if page.serial == info.serial: - pages.append(page) - complete = page.complete or (len(page.packets) > 1) - comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) - super(OggFLACVComment, self).__init__(comment, framing=False) - - def _inject(self, fileobj, padding_func): - """Write tag data into the FLAC Vorbis comment packet/page.""" - - # Ogg FLAC has no convenient data marker like Vorbis, but the - # second packet - and second page - must be the comment data. - fileobj.seek(0) - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x7FFLAC"): - page = OggPage(fileobj) - - first_page = page - while not (page.sequence == 1 and page.serial == first_page.serial): - page = OggPage(fileobj) - - old_pages = [page] - while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): - page = OggPage(fileobj) - if page.serial == first_page.serial: - old_pages.append(page) - - packets = OggPage.to_packets(old_pages, strict=False) - - # Set the new comment block. - data = self.write(framing=False) - data = packets[0][:1] + struct.pack(">I", len(data))[-3:] + data - packets[0] = data - - new_pages = OggPage.from_packets(packets, old_pages[0].sequence) - OggPage.replace(fileobj, old_pages, new_pages) - - -class OggFLAC(OggFileType): - """An Ogg FLAC file.""" - - _Info = OggFLACStreamInfo - _Tags = OggFLACVComment - _Error = OggFLACHeaderError - _mimes = ["audio/x-oggflac"] - - info = None - """A `OggFLACStreamInfo`""" - - tags = None - """A `VCommentDict`""" - - def save(self, filename=None): - return super(OggFLAC, self).save(filename) - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"OggS") * ( - (b"FLAC" in header) + (b"fLaC" in header))) - - -Open = OggFLAC - - -def delete(filename): - """Remove tags from a file.""" - - OggFLAC(filename).delete() diff --git a/resources/lib/libraries/mutagen/oggopus.py b/resources/lib/libraries/mutagen/oggopus.py deleted file mode 100644 index 7154e479..00000000 --- a/resources/lib/libraries/mutagen/oggopus.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2012, 2013 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg Opus comments. - -This module handles Opus files wrapped in an Ogg bitstream. The -first Opus stream found is used. - -Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01 -""" - -__all__ = ["OggOpus", "Open", "delete"] - -import struct - -from mutagen import StreamInfo -from mutagen._compat import BytesIO -from mutagen._util import get_size -from mutagen._tags import PaddingInfo -from mutagen._vorbis import VCommentDict -from mutagen.ogg import OggPage, OggFileType, error as OggError - - -class error(OggError): - pass - - -class OggOpusHeaderError(error): - pass - - -class OggOpusInfo(StreamInfo): - """Ogg Opus stream information.""" - - length = 0 - """File length in seconds, as a float""" - - channels = 0 - """Number of channels""" - - def __init__(self, fileobj): - page = OggPage(fileobj) - while not page.packets[0].startswith(b"OpusHead"): - page = OggPage(fileobj) - - self.serial = page.serial - - if not page.first: - raise OggOpusHeaderError( - "page has ID header, but doesn't start a stream") - - (version, self.channels, pre_skip, orig_sample_rate, output_gain, - channel_map) = struct.unpack("<BBHIhB", page.packets[0][8:19]) - - self.__pre_skip = pre_skip - - # only the higher 4 bits change on incombatible changes - major = version >> 4 - if major != 0: - raise OggOpusHeaderError("version %r unsupported" % major) - - def _post_tags(self, fileobj): - page = OggPage.find_last(fileobj, self.serial) - self.length = (page.position - self.__pre_skip) / float(48000) - - def pprint(self): - return u"Ogg Opus, %.2f seconds" % (self.length) - - -class OggOpusVComment(VCommentDict): - """Opus comments embedded in an Ogg bitstream.""" - - def __get_comment_pages(self, fileobj, info): - # find the first tags page with the right serial - page = OggPage(fileobj) - while ((info.serial != page.serial) or - not page.packets[0].startswith(b"OpusTags")): - page = OggPage(fileobj) - - # get all comment pages - pages = [page] - while not (pages[-1].complete or len(pages[-1].packets) > 1): - page = OggPage(fileobj) - if page.serial == pages[0].serial: - pages.append(page) - - return pages - - def __init__(self, fileobj, info): - pages = self.__get_comment_pages(fileobj, info) - data = OggPage.to_packets(pages)[0][8:] # Strip OpusTags - fileobj = BytesIO(data) - super(OggOpusVComment, self).__init__(fileobj, framing=False) - self._padding = len(data) - self._size - - # in case the LSB of the first byte after v-comment is 1, preserve the - # following data - padding_flag = fileobj.read(1) - if padding_flag and ord(padding_flag) & 0x1: - self._pad_data = padding_flag + fileobj.read() - self._padding = 0 # we have to preserve, so no padding - else: - self._pad_data = b"" - - def _inject(self, fileobj, padding_func): - fileobj.seek(0) - info = OggOpusInfo(fileobj) - old_pages = self.__get_comment_pages(fileobj, info) - - packets = OggPage.to_packets(old_pages) - vcomment_data = b"OpusTags" + self.write(framing=False) - - if self._pad_data: - # if we have padding data to preserver we can't add more padding - # as long as we don't know the structure of what follows - packets[0] = vcomment_data + self._pad_data - else: - content_size = get_size(fileobj) - len(packets[0]) # approx - padding_left = len(packets[0]) - len(vcomment_data) - info = PaddingInfo(padding_left, content_size) - new_padding = info._get_padding(padding_func) - packets[0] = vcomment_data + b"\x00" * new_padding - - new_pages = OggPage._from_packets_try_preserve(packets, old_pages) - OggPage.replace(fileobj, old_pages, new_pages) - - -class OggOpus(OggFileType): - """An Ogg Opus file.""" - - _Info = OggOpusInfo - _Tags = OggOpusVComment - _Error = OggOpusHeaderError - _mimes = ["audio/ogg", "audio/ogg; codecs=opus"] - - info = None - """A `OggOpusInfo`""" - - tags = None - """A `VCommentDict`""" - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"OggS") * (b"OpusHead" in header)) - - -Open = OggOpus - - -def delete(filename): - """Remove tags from a file.""" - - OggOpus(filename).delete() diff --git a/resources/lib/libraries/mutagen/oggspeex.py b/resources/lib/libraries/mutagen/oggspeex.py deleted file mode 100644 index 9b16930b..00000000 --- a/resources/lib/libraries/mutagen/oggspeex.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg Speex comments. - -This module handles Speex files wrapped in an Ogg bitstream. The -first Speex stream found is used. - -Read more about Ogg Speex at http://www.speex.org/. This module is -based on the specification at http://www.speex.org/manual2/node7.html -and clarifications after personal communication with Jean-Marc, -http://lists.xiph.org/pipermail/speex-dev/2006-July/004676.html. -""" - -__all__ = ["OggSpeex", "Open", "delete"] - -from mutagen import StreamInfo -from mutagen._vorbis import VCommentDict -from mutagen.ogg import OggPage, OggFileType, error as OggError -from mutagen._util import cdata, get_size -from mutagen._tags import PaddingInfo - - -class error(OggError): - pass - - -class OggSpeexHeaderError(error): - pass - - -class OggSpeexInfo(StreamInfo): - """Ogg Speex stream information.""" - - length = 0 - """file length in seconds, as a float""" - - channels = 0 - """number of channels""" - - bitrate = 0 - """nominal bitrate in bits per second. - - The reference encoder does not set the bitrate; in this case, - the bitrate will be 0. - """ - - def __init__(self, fileobj): - page = OggPage(fileobj) - while not page.packets[0].startswith(b"Speex "): - page = OggPage(fileobj) - if not page.first: - raise OggSpeexHeaderError( - "page has ID header, but doesn't start a stream") - self.sample_rate = cdata.uint_le(page.packets[0][36:40]) - self.channels = cdata.uint_le(page.packets[0][48:52]) - self.bitrate = max(0, cdata.int_le(page.packets[0][52:56])) - self.serial = page.serial - - def _post_tags(self, fileobj): - page = OggPage.find_last(fileobj, self.serial) - self.length = page.position / float(self.sample_rate) - - def pprint(self): - return u"Ogg Speex, %.2f seconds" % self.length - - -class OggSpeexVComment(VCommentDict): - """Speex comments embedded in an Ogg bitstream.""" - - def __init__(self, fileobj, info): - pages = [] - complete = False - while not complete: - page = OggPage(fileobj) - if page.serial == info.serial: - pages.append(page) - complete = page.complete or (len(page.packets) > 1) - data = OggPage.to_packets(pages)[0] - super(OggSpeexVComment, self).__init__(data, framing=False) - self._padding = len(data) - self._size - - def _inject(self, fileobj, padding_func): - """Write tag data into the Speex comment packet/page.""" - - fileobj.seek(0) - - # Find the first header page, with the stream info. - # Use it to get the serial number. - page = OggPage(fileobj) - while not page.packets[0].startswith(b"Speex "): - page = OggPage(fileobj) - - # Look for the next page with that serial number, it'll start - # the comment packet. - serial = page.serial - page = OggPage(fileobj) - while page.serial != serial: - page = OggPage(fileobj) - - # Then find all the pages with the comment packet. - old_pages = [page] - while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): - page = OggPage(fileobj) - if page.serial == old_pages[0].serial: - old_pages.append(page) - - packets = OggPage.to_packets(old_pages, strict=False) - - content_size = get_size(fileobj) - len(packets[0]) # approx - vcomment_data = self.write(framing=False) - padding_left = len(packets[0]) - len(vcomment_data) - - info = PaddingInfo(padding_left, content_size) - new_padding = info._get_padding(padding_func) - - # Set the new comment packet. - packets[0] = vcomment_data + b"\x00" * new_padding - - new_pages = OggPage._from_packets_try_preserve(packets, old_pages) - OggPage.replace(fileobj, old_pages, new_pages) - - -class OggSpeex(OggFileType): - """An Ogg Speex file.""" - - _Info = OggSpeexInfo - _Tags = OggSpeexVComment - _Error = OggSpeexHeaderError - _mimes = ["audio/x-speex"] - - info = None - """A `OggSpeexInfo`""" - - tags = None - """A `VCommentDict`""" - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"OggS") * (b"Speex " in header)) - - -Open = OggSpeex - - -def delete(filename): - """Remove tags from a file.""" - - OggSpeex(filename).delete() diff --git a/resources/lib/libraries/mutagen/oggtheora.py b/resources/lib/libraries/mutagen/oggtheora.py deleted file mode 100644 index 122e7d4b..00000000 --- a/resources/lib/libraries/mutagen/oggtheora.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg Theora comments. - -This module handles Theora files wrapped in an Ogg bitstream. The -first Theora stream found is used. - -Based on the specification at http://theora.org/doc/Theora_I_spec.pdf. -""" - -__all__ = ["OggTheora", "Open", "delete"] - -import struct - -from mutagen import StreamInfo -from mutagen._vorbis import VCommentDict -from mutagen._util import cdata, get_size -from mutagen._tags import PaddingInfo -from mutagen.ogg import OggPage, OggFileType, error as OggError - - -class error(OggError): - pass - - -class OggTheoraHeaderError(error): - pass - - -class OggTheoraInfo(StreamInfo): - """Ogg Theora stream information.""" - - length = 0 - """File length in seconds, as a float""" - - fps = 0 - """Video frames per second, as a float""" - - bitrate = 0 - """Bitrate in bps (int)""" - - def __init__(self, fileobj): - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x80theora"): - page = OggPage(fileobj) - if not page.first: - raise OggTheoraHeaderError( - "page has ID header, but doesn't start a stream") - data = page.packets[0] - vmaj, vmin = struct.unpack("2B", data[7:9]) - if (vmaj, vmin) != (3, 2): - raise OggTheoraHeaderError( - "found Theora version %d.%d != 3.2" % (vmaj, vmin)) - fps_num, fps_den = struct.unpack(">2I", data[22:30]) - self.fps = fps_num / float(fps_den) - self.bitrate = cdata.uint_be(b"\x00" + data[37:40]) - self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F - self.serial = page.serial - - def _post_tags(self, fileobj): - page = OggPage.find_last(fileobj, self.serial) - position = page.position - mask = (1 << self.granule_shift) - 1 - frames = (position >> self.granule_shift) + (position & mask) - self.length = frames / float(self.fps) - - def pprint(self): - return u"Ogg Theora, %.2f seconds, %d bps" % (self.length, - self.bitrate) - - -class OggTheoraCommentDict(VCommentDict): - """Theora comments embedded in an Ogg bitstream.""" - - def __init__(self, fileobj, info): - pages = [] - complete = False - while not complete: - page = OggPage(fileobj) - if page.serial == info.serial: - pages.append(page) - complete = page.complete or (len(page.packets) > 1) - data = OggPage.to_packets(pages)[0][7:] - super(OggTheoraCommentDict, self).__init__(data, framing=False) - self._padding = len(data) - self._size - - def _inject(self, fileobj, padding_func): - """Write tag data into the Theora comment packet/page.""" - - fileobj.seek(0) - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x81theora"): - page = OggPage(fileobj) - - old_pages = [page] - while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): - page = OggPage(fileobj) - if page.serial == old_pages[0].serial: - old_pages.append(page) - - packets = OggPage.to_packets(old_pages, strict=False) - - content_size = get_size(fileobj) - len(packets[0]) # approx - vcomment_data = b"\x81theora" + self.write(framing=False) - padding_left = len(packets[0]) - len(vcomment_data) - - info = PaddingInfo(padding_left, content_size) - new_padding = info._get_padding(padding_func) - - packets[0] = vcomment_data + b"\x00" * new_padding - - new_pages = OggPage._from_packets_try_preserve(packets, old_pages) - OggPage.replace(fileobj, old_pages, new_pages) - - -class OggTheora(OggFileType): - """An Ogg Theora file.""" - - _Info = OggTheoraInfo - _Tags = OggTheoraCommentDict - _Error = OggTheoraHeaderError - _mimes = ["video/x-theora"] - - info = None - """A `OggTheoraInfo`""" - - tags = None - """A `VCommentDict`""" - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"OggS") * - ((b"\x80theora" in header) + (b"\x81theora" in header)) * 2) - - -Open = OggTheora - - -def delete(filename): - """Remove tags from a file.""" - - OggTheora(filename).delete() diff --git a/resources/lib/libraries/mutagen/oggvorbis.py b/resources/lib/libraries/mutagen/oggvorbis.py deleted file mode 100644 index b058a0c1..00000000 --- a/resources/lib/libraries/mutagen/oggvorbis.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""Read and write Ogg Vorbis comments. - -This module handles Vorbis files wrapped in an Ogg bitstream. The -first Vorbis stream found is used. - -Read more about Ogg Vorbis at http://vorbis.com/. This module is based -on the specification at http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html. -""" - -__all__ = ["OggVorbis", "Open", "delete"] - -import struct - -from mutagen import StreamInfo -from mutagen._vorbis import VCommentDict -from mutagen._util import get_size -from mutagen._tags import PaddingInfo -from mutagen.ogg import OggPage, OggFileType, error as OggError - - -class error(OggError): - pass - - -class OggVorbisHeaderError(error): - pass - - -class OggVorbisInfo(StreamInfo): - """Ogg Vorbis stream information.""" - - length = 0 - """File length in seconds, as a float""" - - channels = 0 - """Number of channels""" - - bitrate = 0 - """Nominal ('average') bitrate in bits per second, as an int""" - - sample_rate = 0 - """Sample rate in Hz""" - - def __init__(self, fileobj): - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x01vorbis"): - page = OggPage(fileobj) - if not page.first: - raise OggVorbisHeaderError( - "page has ID header, but doesn't start a stream") - (self.channels, self.sample_rate, max_bitrate, nominal_bitrate, - min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28]) - self.serial = page.serial - - max_bitrate = max(0, max_bitrate) - min_bitrate = max(0, min_bitrate) - nominal_bitrate = max(0, nominal_bitrate) - - if nominal_bitrate == 0: - self.bitrate = (max_bitrate + min_bitrate) // 2 - elif max_bitrate and max_bitrate < nominal_bitrate: - # If the max bitrate is less than the nominal, we know - # the nominal is wrong. - self.bitrate = max_bitrate - elif min_bitrate > nominal_bitrate: - self.bitrate = min_bitrate - else: - self.bitrate = nominal_bitrate - - def _post_tags(self, fileobj): - page = OggPage.find_last(fileobj, self.serial) - self.length = page.position / float(self.sample_rate) - - def pprint(self): - return u"Ogg Vorbis, %.2f seconds, %d bps" % ( - self.length, self.bitrate) - - -class OggVCommentDict(VCommentDict): - """Vorbis comments embedded in an Ogg bitstream.""" - - def __init__(self, fileobj, info): - pages = [] - complete = False - while not complete: - page = OggPage(fileobj) - if page.serial == info.serial: - pages.append(page) - complete = page.complete or (len(page.packets) > 1) - data = OggPage.to_packets(pages)[0][7:] # Strip off "\x03vorbis". - super(OggVCommentDict, self).__init__(data) - self._padding = len(data) - self._size - - def _inject(self, fileobj, padding_func): - """Write tag data into the Vorbis comment packet/page.""" - - # Find the old pages in the file; we'll need to remove them, - # plus grab any stray setup packet data out of them. - fileobj.seek(0) - page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x03vorbis"): - page = OggPage(fileobj) - - old_pages = [page] - while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): - page = OggPage(fileobj) - if page.serial == old_pages[0].serial: - old_pages.append(page) - - packets = OggPage.to_packets(old_pages, strict=False) - - content_size = get_size(fileobj) - len(packets[0]) # approx - vcomment_data = b"\x03vorbis" + self.write() - padding_left = len(packets[0]) - len(vcomment_data) - - info = PaddingInfo(padding_left, content_size) - new_padding = info._get_padding(padding_func) - - # Set the new comment packet. - packets[0] = vcomment_data + b"\x00" * new_padding - - new_pages = OggPage._from_packets_try_preserve(packets, old_pages) - OggPage.replace(fileobj, old_pages, new_pages) - - -class OggVorbis(OggFileType): - """An Ogg Vorbis file.""" - - _Info = OggVorbisInfo - _Tags = OggVCommentDict - _Error = OggVorbisHeaderError - _mimes = ["audio/vorbis", "audio/x-vorbis"] - - info = None - """A `OggVorbisInfo`""" - - tags = None - """A `VCommentDict`""" - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"OggS") * (b"\x01vorbis" in header)) - - -Open = OggVorbis - - -def delete(filename): - """Remove tags from a file.""" - - OggVorbis(filename).delete() diff --git a/resources/lib/libraries/mutagen/optimfrog.py b/resources/lib/libraries/mutagen/optimfrog.py deleted file mode 100644 index 0d85a818..00000000 --- a/resources/lib/libraries/mutagen/optimfrog.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Lukas Lalinsky -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""OptimFROG audio streams with APEv2 tags. - -OptimFROG is a lossless audio compression program. Its main goal is to -reduce at maximum the size of audio files, while permitting bit -identical restoration for all input. It is similar with the ZIP -compression, but it is highly specialized to compress audio data. - -Only versions 4.5 and higher are supported. - -For more information, see http://www.losslessaudio.org/ -""" - -__all__ = ["OptimFROG", "Open", "delete"] - -import struct - -from ._compat import endswith -from mutagen import StreamInfo -from mutagen.apev2 import APEv2File, error, delete - - -class OptimFROGHeaderError(error): - pass - - -class OptimFROGInfo(StreamInfo): - """OptimFROG stream information. - - Attributes: - - * channels - number of audio channels - * length - file length in seconds, as a float - * sample_rate - audio sampling rate in Hz - """ - - def __init__(self, fileobj): - header = fileobj.read(76) - if (len(header) != 76 or not header.startswith(b"OFR ") or - struct.unpack("<I", header[4:8])[0] not in [12, 15]): - raise OptimFROGHeaderError("not an OptimFROG file") - (total_samples, total_samples_high, sample_type, self.channels, - self.sample_rate) = struct.unpack("<IHBBI", header[8:20]) - total_samples += total_samples_high << 32 - self.channels += 1 - if self.sample_rate: - self.length = float(total_samples) / (self.channels * - self.sample_rate) - else: - self.length = 0.0 - - def pprint(self): - return u"OptimFROG, %.2f seconds, %d Hz" % (self.length, - self.sample_rate) - - -class OptimFROG(APEv2File): - _Info = OptimFROGInfo - - @staticmethod - def score(filename, fileobj, header): - filename = filename.lower() - - return (header.startswith(b"OFR") + endswith(filename, b".ofr") + - endswith(filename, b".ofs")) - -Open = OptimFROG diff --git a/resources/lib/libraries/mutagen/trueaudio.py b/resources/lib/libraries/mutagen/trueaudio.py deleted file mode 100644 index 1c8d56c4..00000000 --- a/resources/lib/libraries/mutagen/trueaudio.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2006 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""True Audio audio stream information and tags. - -True Audio is a lossless format designed for real-time encoding and -decoding. This module is based on the documentation at -http://www.true-audio.com/TTA_Lossless_Audio_Codec\_-_Format_Description - -True Audio files use ID3 tags. -""" - -__all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] - -from ._compat import endswith -from mutagen import StreamInfo -from mutagen.id3 import ID3FileType, delete -from mutagen._util import cdata, MutagenError - - -class error(RuntimeError, MutagenError): - pass - - -class TrueAudioHeaderError(error, IOError): - pass - - -class TrueAudioInfo(StreamInfo): - """True Audio stream information. - - Attributes: - - * length - audio length, in seconds - * sample_rate - audio sample rate, in Hz - """ - - def __init__(self, fileobj, offset): - fileobj.seek(offset or 0) - header = fileobj.read(18) - if len(header) != 18 or not header.startswith(b"TTA"): - raise TrueAudioHeaderError("TTA header not found") - self.sample_rate = cdata.int_le(header[10:14]) - samples = cdata.uint_le(header[14:18]) - self.length = float(samples) / self.sample_rate - - def pprint(self): - return u"True Audio, %.2f seconds, %d Hz." % ( - self.length, self.sample_rate) - - -class TrueAudio(ID3FileType): - """A True Audio file. - - :ivar info: :class:`TrueAudioInfo` - :ivar tags: :class:`ID3 <mutagen.id3.ID3>` - """ - - _Info = TrueAudioInfo - _mimes = ["audio/x-tta"] - - @staticmethod - def score(filename, fileobj, header): - return (header.startswith(b"ID3") + header.startswith(b"TTA") + - endswith(filename.lower(), b".tta") * 2) - - -Open = TrueAudio - - -class EasyTrueAudio(TrueAudio): - """Like MP3, but uses EasyID3 for tags. - - :ivar info: :class:`TrueAudioInfo` - :ivar tags: :class:`EasyID3 <mutagen.easyid3.EasyID3>` - """ - - from mutagen.easyid3 import EasyID3 as ID3 - ID3 = ID3 diff --git a/resources/lib/libraries/mutagen/wavpack.py b/resources/lib/libraries/mutagen/wavpack.py deleted file mode 100644 index 80710f6d..00000000 --- a/resources/lib/libraries/mutagen/wavpack.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2006 Joe Wreschnig -# 2014 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation. - -"""WavPack reading and writing. - -WavPack is a lossless format that uses APEv2 tags. Read - -* http://www.wavpack.com/ -* http://www.wavpack.com/file_format.txt - -for more information. -""" - -__all__ = ["WavPack", "Open", "delete"] - -from mutagen import StreamInfo -from mutagen.apev2 import APEv2File, error, delete -from mutagen._util import cdata - - -class WavPackHeaderError(error): - pass - -RATES = [6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, - 48000, 64000, 88200, 96000, 192000] - - -class _WavPackHeader(object): - - def __init__(self, block_size, version, track_no, index_no, total_samples, - block_index, block_samples, flags, crc): - - self.block_size = block_size - self.version = version - self.track_no = track_no - self.index_no = index_no - self.total_samples = total_samples - self.block_index = block_index - self.block_samples = block_samples - self.flags = flags - self.crc = crc - - @classmethod - def from_fileobj(cls, fileobj): - """A new _WavPackHeader or raises WavPackHeaderError""" - - header = fileobj.read(32) - if len(header) != 32 or not header.startswith(b"wvpk"): - raise WavPackHeaderError("not a WavPack header: %r" % header) - - block_size = cdata.uint_le(header[4:8]) - version = cdata.ushort_le(header[8:10]) - track_no = ord(header[10:11]) - index_no = ord(header[11:12]) - samples = cdata.uint_le(header[12:16]) - if samples == 2 ** 32 - 1: - samples = -1 - block_index = cdata.uint_le(header[16:20]) - block_samples = cdata.uint_le(header[20:24]) - flags = cdata.uint_le(header[24:28]) - crc = cdata.uint_le(header[28:32]) - - return _WavPackHeader(block_size, version, track_no, index_no, - samples, block_index, block_samples, flags, crc) - - -class WavPackInfo(StreamInfo): - """WavPack stream information. - - Attributes: - - * channels - number of audio channels (1 or 2) - * length - file length in seconds, as a float - * sample_rate - audio sampling rate in Hz - * version - WavPack stream version - """ - - def __init__(self, fileobj): - try: - header = _WavPackHeader.from_fileobj(fileobj) - except WavPackHeaderError: - raise WavPackHeaderError("not a WavPack file") - - self.version = header.version - self.channels = bool(header.flags & 4) or 2 - self.sample_rate = RATES[(header.flags >> 23) & 0xF] - - if header.total_samples == -1 or header.block_index != 0: - # TODO: we could make this faster by using the tag size - # and search backwards for the last block, then do - # last.block_index + last.block_samples - initial.block_index - samples = header.block_samples - while 1: - fileobj.seek(header.block_size - 32 + 8, 1) - try: - header = _WavPackHeader.from_fileobj(fileobj) - except WavPackHeaderError: - break - samples += header.block_samples - else: - samples = header.total_samples - - self.length = float(samples) / self.sample_rate - - def pprint(self): - return u"WavPack, %.2f seconds, %d Hz" % (self.length, - self.sample_rate) - - -class WavPack(APEv2File): - _Info = WavPackInfo - _mimes = ["audio/x-wavpack"] - - @staticmethod - def score(filename, fileobj, header): - return header.startswith(b"wvpk") * 2 - - -Open = WavPack diff --git a/service.py b/service.py index 5844cb99..3292a122 100644 --- a/service.py +++ b/service.py @@ -15,9 +15,12 @@ import xbmcaddon __addon__ = xbmcaddon.Addon(id='plugin.video.emby') __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') +sys.path.insert(0, __libraries__) + if not xbmcvfs.exists(__pcache__ + '/'): from resources.lib.helper.utils import copytree From 158a736360b6419fa20f0b48e2fb2cf16e83a992 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Wed, 30 Jan 2019 06:43:14 -0600 Subject: [PATCH 06/12] Update webservice with cherrypy Fix playback issues that was causing Kodi to hang up --- libraries/backports/__init__.py | 1 + libraries/backports/functools_lru_cache.py | 184 ++ libraries/cheroot/__init__.py | 6 + libraries/cheroot/__main__.py | 6 + libraries/cheroot/_compat.py | 66 + libraries/cheroot/cli.py | 233 ++ libraries/cheroot/errors.py | 58 + libraries/cheroot/makefile.py | 387 +++ libraries/cheroot/server.py | 2001 +++++++++++++++ libraries/cheroot/ssl/__init__.py | 51 + libraries/cheroot/ssl/builtin.py | 162 ++ libraries/cheroot/ssl/pyopenssl.py | 267 ++ libraries/cheroot/test/__init__.py | 1 + libraries/cheroot/test/conftest.py | 27 + libraries/cheroot/test/helper.py | 169 ++ libraries/cheroot/test/test.pem | 38 + libraries/cheroot/test/test__compat.py | 49 + libraries/cheroot/test/test_conn.py | 897 +++++++ libraries/cheroot/test/test_core.py | 405 +++ libraries/cheroot/test/test_server.py | 193 ++ libraries/cheroot/test/webtest.py | 581 +++++ libraries/cheroot/testing.py | 144 ++ libraries/cheroot/workers/__init__.py | 1 + libraries/cheroot/workers/threadpool.py | 271 ++ libraries/cheroot/wsgi.py | 423 ++++ libraries/cherrypy/__init__.py | 362 +++ libraries/cherrypy/__main__.py | 5 + libraries/cherrypy/_cpchecker.py | 325 +++ libraries/cherrypy/_cpcompat.py | 162 ++ libraries/cherrypy/_cpconfig.py | 300 +++ libraries/cherrypy/_cpdispatch.py | 686 +++++ libraries/cherrypy/_cperror.py | 619 +++++ libraries/cherrypy/_cplogging.py | 482 ++++ libraries/cherrypy/_cpmodpy.py | 356 +++ libraries/cherrypy/_cpnative_server.py | 160 ++ libraries/cherrypy/_cpreqbody.py | 1000 ++++++++ libraries/cherrypy/_cprequest.py | 930 +++++++ libraries/cherrypy/_cpserver.py | 252 ++ libraries/cherrypy/_cptools.py | 509 ++++ libraries/cherrypy/_cptree.py | 313 +++ libraries/cherrypy/_cpwsgi.py | 467 ++++ libraries/cherrypy/_cpwsgi_server.py | 110 + libraries/cherrypy/_helper.py | 344 +++ libraries/cherrypy/daemon.py | 107 + libraries/cherrypy/favicon.ico | Bin 0 -> 1406 bytes libraries/cherrypy/lib/__init__.py | 96 + libraries/cherrypy/lib/auth_basic.py | 120 + libraries/cherrypy/lib/auth_digest.py | 464 ++++ libraries/cherrypy/lib/caching.py | 482 ++++ libraries/cherrypy/lib/covercp.py | 391 +++ libraries/cherrypy/lib/cpstats.py | 696 ++++++ libraries/cherrypy/lib/cptools.py | 640 +++++ libraries/cherrypy/lib/encoding.py | 436 ++++ libraries/cherrypy/lib/gctools.py | 218 ++ libraries/cherrypy/lib/httputil.py | 581 +++++ libraries/cherrypy/lib/jsontools.py | 88 + libraries/cherrypy/lib/locking.py | 47 + libraries/cherrypy/lib/profiler.py | 221 ++ libraries/cherrypy/lib/reprconf.py | 514 ++++ libraries/cherrypy/lib/sessions.py | 919 +++++++ libraries/cherrypy/lib/static.py | 390 +++ libraries/cherrypy/lib/xmlrpcutil.py | 61 + libraries/cherrypy/process/__init__.py | 17 + libraries/cherrypy/process/plugins.py | 752 ++++++ libraries/cherrypy/process/servers.py | 416 ++++ libraries/cherrypy/process/win32.py | 183 ++ libraries/cherrypy/process/wspbus.py | 590 +++++ libraries/cherrypy/scaffold/__init__.py | 63 + libraries/cherrypy/scaffold/apache-fcgi.conf | 22 + libraries/cherrypy/scaffold/example.conf | 3 + libraries/cherrypy/scaffold/site.conf | 14 + .../static/made_with_cherrypy_small.png | Bin 0 -> 6347 bytes libraries/cherrypy/test/__init__.py | 24 + libraries/cherrypy/test/_test_decorators.py | 39 + libraries/cherrypy/test/_test_states_demo.py | 69 + libraries/cherrypy/test/benchmark.py | 425 ++++ libraries/cherrypy/test/checkerdemo.py | 49 + libraries/cherrypy/test/fastcgi.conf | 18 + libraries/cherrypy/test/fcgi.conf | 14 + libraries/cherrypy/test/helper.py | 542 ++++ libraries/cherrypy/test/logtest.py | 228 ++ libraries/cherrypy/test/modfastcgi.py | 136 + libraries/cherrypy/test/modfcgid.py | 124 + libraries/cherrypy/test/modpy.py | 164 ++ libraries/cherrypy/test/modwsgi.py | 154 ++ libraries/cherrypy/test/sessiondemo.py | 161 ++ libraries/cherrypy/test/static/404.html | 5 + libraries/cherrypy/test/static/dirback.jpg | Bin 0 -> 16585 bytes libraries/cherrypy/test/static/index.html | 1 + libraries/cherrypy/test/style.css | 1 + libraries/cherrypy/test/test.pem | 38 + libraries/cherrypy/test/test_auth_basic.py | 135 + libraries/cherrypy/test/test_auth_digest.py | 134 + libraries/cherrypy/test/test_bus.py | 274 ++ libraries/cherrypy/test/test_caching.py | 392 +++ libraries/cherrypy/test/test_compat.py | 34 + libraries/cherrypy/test/test_config.py | 303 +++ libraries/cherrypy/test/test_config_server.py | 126 + libraries/cherrypy/test/test_conn.py | 873 +++++++ libraries/cherrypy/test/test_core.py | 823 ++++++ .../test/test_dynamicobjectmapping.py | 424 ++++ libraries/cherrypy/test/test_encoding.py | 426 ++++ libraries/cherrypy/test/test_etags.py | 84 + libraries/cherrypy/test/test_http.py | 307 +++ libraries/cherrypy/test/test_httputil.py | 80 + libraries/cherrypy/test/test_iterator.py | 196 ++ libraries/cherrypy/test/test_json.py | 102 + libraries/cherrypy/test/test_logging.py | 209 ++ libraries/cherrypy/test/test_mime.py | 134 + libraries/cherrypy/test/test_misc_tools.py | 210 ++ libraries/cherrypy/test/test_native.py | 35 + libraries/cherrypy/test/test_objectmapping.py | 430 ++++ libraries/cherrypy/test/test_params.py | 61 + libraries/cherrypy/test/test_plugins.py | 14 + libraries/cherrypy/test/test_proxy.py | 154 ++ libraries/cherrypy/test/test_refleaks.py | 66 + libraries/cherrypy/test/test_request_obj.py | 932 +++++++ libraries/cherrypy/test/test_routes.py | 80 + libraries/cherrypy/test/test_session.py | 512 ++++ .../cherrypy/test/test_sessionauthenticate.py | 61 + libraries/cherrypy/test/test_states.py | 473 ++++ libraries/cherrypy/test/test_static.py | 434 ++++ libraries/cherrypy/test/test_tools.py | 468 ++++ libraries/cherrypy/test/test_tutorials.py | 210 ++ libraries/cherrypy/test/test_virtualhost.py | 113 + libraries/cherrypy/test/test_wsgi_ns.py | 93 + .../cherrypy/test/test_wsgi_unix_socket.py | 93 + libraries/cherrypy/test/test_wsgi_vhost.py | 35 + libraries/cherrypy/test/test_wsgiapps.py | 120 + libraries/cherrypy/test/test_xmlrpc.py | 183 ++ libraries/cherrypy/test/webtest.py | 11 + libraries/cherrypy/tutorial/README.rst | 16 + libraries/cherrypy/tutorial/__init__.py | 3 + libraries/cherrypy/tutorial/custom_error.html | 14 + libraries/cherrypy/tutorial/pdf_file.pdf | Bin 0 -> 85698 bytes .../cherrypy/tutorial/tut01_helloworld.py | 34 + .../cherrypy/tutorial/tut02_expose_methods.py | 32 + .../cherrypy/tutorial/tut03_get_and_post.py | 51 + .../cherrypy/tutorial/tut04_complex_site.py | 103 + .../tutorial/tut05_derived_objects.py | 80 + .../cherrypy/tutorial/tut06_default_method.py | 61 + libraries/cherrypy/tutorial/tut07_sessions.py | 41 + .../tutorial/tut08_generators_and_yield.py | 44 + libraries/cherrypy/tutorial/tut09_files.py | 105 + .../cherrypy/tutorial/tut10_http_errors.py | 84 + libraries/cherrypy/tutorial/tutorial.conf | 4 + libraries/contextlib2.py | 436 ++++ libraries/more_itertools/__init__.py | 2 + libraries/more_itertools/more.py | 2211 +++++++++++++++++ libraries/more_itertools/recipes.py | 565 +++++ libraries/more_itertools/tests/__init__.py | 0 libraries/more_itertools/tests/test_more.py | 2074 ++++++++++++++++ .../more_itertools/tests/test_recipes.py | 616 +++++ libraries/portend.py | 212 ++ libraries/tempora/__init__.py | 505 ++++ libraries/tempora/schedule.py | 202 ++ libraries/tempora/tests/test_schedule.py | 118 + libraries/tempora/timing.py | 219 ++ libraries/tempora/utc.py | 36 + libraries/zc/__init__.py | 1 + libraries/zc/lockfile/README.txt | 70 + libraries/zc/lockfile/__init__.py | 104 + libraries/zc/lockfile/tests.py | 193 ++ resources/lib/webservice.py | 223 +- 164 files changed, 42855 insertions(+), 174 deletions(-) create mode 100644 libraries/backports/__init__.py create mode 100644 libraries/backports/functools_lru_cache.py create mode 100644 libraries/cheroot/__init__.py create mode 100644 libraries/cheroot/__main__.py create mode 100644 libraries/cheroot/_compat.py create mode 100644 libraries/cheroot/cli.py create mode 100644 libraries/cheroot/errors.py create mode 100644 libraries/cheroot/makefile.py create mode 100644 libraries/cheroot/server.py create mode 100644 libraries/cheroot/ssl/__init__.py create mode 100644 libraries/cheroot/ssl/builtin.py create mode 100644 libraries/cheroot/ssl/pyopenssl.py create mode 100644 libraries/cheroot/test/__init__.py create mode 100644 libraries/cheroot/test/conftest.py create mode 100644 libraries/cheroot/test/helper.py create mode 100644 libraries/cheroot/test/test.pem create mode 100644 libraries/cheroot/test/test__compat.py create mode 100644 libraries/cheroot/test/test_conn.py create mode 100644 libraries/cheroot/test/test_core.py create mode 100644 libraries/cheroot/test/test_server.py create mode 100644 libraries/cheroot/test/webtest.py create mode 100644 libraries/cheroot/testing.py create mode 100644 libraries/cheroot/workers/__init__.py create mode 100644 libraries/cheroot/workers/threadpool.py create mode 100644 libraries/cheroot/wsgi.py create mode 100644 libraries/cherrypy/__init__.py create mode 100644 libraries/cherrypy/__main__.py create mode 100644 libraries/cherrypy/_cpchecker.py create mode 100644 libraries/cherrypy/_cpcompat.py create mode 100644 libraries/cherrypy/_cpconfig.py create mode 100644 libraries/cherrypy/_cpdispatch.py create mode 100644 libraries/cherrypy/_cperror.py create mode 100644 libraries/cherrypy/_cplogging.py create mode 100644 libraries/cherrypy/_cpmodpy.py create mode 100644 libraries/cherrypy/_cpnative_server.py create mode 100644 libraries/cherrypy/_cpreqbody.py create mode 100644 libraries/cherrypy/_cprequest.py create mode 100644 libraries/cherrypy/_cpserver.py create mode 100644 libraries/cherrypy/_cptools.py create mode 100644 libraries/cherrypy/_cptree.py create mode 100644 libraries/cherrypy/_cpwsgi.py create mode 100644 libraries/cherrypy/_cpwsgi_server.py create mode 100644 libraries/cherrypy/_helper.py create mode 100644 libraries/cherrypy/daemon.py create mode 100644 libraries/cherrypy/favicon.ico create mode 100644 libraries/cherrypy/lib/__init__.py create mode 100644 libraries/cherrypy/lib/auth_basic.py create mode 100644 libraries/cherrypy/lib/auth_digest.py create mode 100644 libraries/cherrypy/lib/caching.py create mode 100644 libraries/cherrypy/lib/covercp.py create mode 100644 libraries/cherrypy/lib/cpstats.py create mode 100644 libraries/cherrypy/lib/cptools.py create mode 100644 libraries/cherrypy/lib/encoding.py create mode 100644 libraries/cherrypy/lib/gctools.py create mode 100644 libraries/cherrypy/lib/httputil.py create mode 100644 libraries/cherrypy/lib/jsontools.py create mode 100644 libraries/cherrypy/lib/locking.py create mode 100644 libraries/cherrypy/lib/profiler.py create mode 100644 libraries/cherrypy/lib/reprconf.py create mode 100644 libraries/cherrypy/lib/sessions.py create mode 100644 libraries/cherrypy/lib/static.py create mode 100644 libraries/cherrypy/lib/xmlrpcutil.py create mode 100644 libraries/cherrypy/process/__init__.py create mode 100644 libraries/cherrypy/process/plugins.py create mode 100644 libraries/cherrypy/process/servers.py create mode 100644 libraries/cherrypy/process/win32.py create mode 100644 libraries/cherrypy/process/wspbus.py create mode 100644 libraries/cherrypy/scaffold/__init__.py create mode 100644 libraries/cherrypy/scaffold/apache-fcgi.conf create mode 100644 libraries/cherrypy/scaffold/example.conf create mode 100644 libraries/cherrypy/scaffold/site.conf create mode 100644 libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png create mode 100644 libraries/cherrypy/test/__init__.py create mode 100644 libraries/cherrypy/test/_test_decorators.py create mode 100644 libraries/cherrypy/test/_test_states_demo.py create mode 100644 libraries/cherrypy/test/benchmark.py create mode 100644 libraries/cherrypy/test/checkerdemo.py create mode 100644 libraries/cherrypy/test/fastcgi.conf create mode 100644 libraries/cherrypy/test/fcgi.conf create mode 100644 libraries/cherrypy/test/helper.py create mode 100644 libraries/cherrypy/test/logtest.py create mode 100644 libraries/cherrypy/test/modfastcgi.py create mode 100644 libraries/cherrypy/test/modfcgid.py create mode 100644 libraries/cherrypy/test/modpy.py create mode 100644 libraries/cherrypy/test/modwsgi.py create mode 100644 libraries/cherrypy/test/sessiondemo.py create mode 100644 libraries/cherrypy/test/static/404.html create mode 100644 libraries/cherrypy/test/static/dirback.jpg create mode 100644 libraries/cherrypy/test/static/index.html create mode 100644 libraries/cherrypy/test/style.css create mode 100644 libraries/cherrypy/test/test.pem create mode 100644 libraries/cherrypy/test/test_auth_basic.py create mode 100644 libraries/cherrypy/test/test_auth_digest.py create mode 100644 libraries/cherrypy/test/test_bus.py create mode 100644 libraries/cherrypy/test/test_caching.py create mode 100644 libraries/cherrypy/test/test_compat.py create mode 100644 libraries/cherrypy/test/test_config.py create mode 100644 libraries/cherrypy/test/test_config_server.py create mode 100644 libraries/cherrypy/test/test_conn.py create mode 100644 libraries/cherrypy/test/test_core.py create mode 100644 libraries/cherrypy/test/test_dynamicobjectmapping.py create mode 100644 libraries/cherrypy/test/test_encoding.py create mode 100644 libraries/cherrypy/test/test_etags.py create mode 100644 libraries/cherrypy/test/test_http.py create mode 100644 libraries/cherrypy/test/test_httputil.py create mode 100644 libraries/cherrypy/test/test_iterator.py create mode 100644 libraries/cherrypy/test/test_json.py create mode 100644 libraries/cherrypy/test/test_logging.py create mode 100644 libraries/cherrypy/test/test_mime.py create mode 100644 libraries/cherrypy/test/test_misc_tools.py create mode 100644 libraries/cherrypy/test/test_native.py create mode 100644 libraries/cherrypy/test/test_objectmapping.py create mode 100644 libraries/cherrypy/test/test_params.py create mode 100644 libraries/cherrypy/test/test_plugins.py create mode 100644 libraries/cherrypy/test/test_proxy.py create mode 100644 libraries/cherrypy/test/test_refleaks.py create mode 100644 libraries/cherrypy/test/test_request_obj.py create mode 100644 libraries/cherrypy/test/test_routes.py create mode 100644 libraries/cherrypy/test/test_session.py create mode 100644 libraries/cherrypy/test/test_sessionauthenticate.py create mode 100644 libraries/cherrypy/test/test_states.py create mode 100644 libraries/cherrypy/test/test_static.py create mode 100644 libraries/cherrypy/test/test_tools.py create mode 100644 libraries/cherrypy/test/test_tutorials.py create mode 100644 libraries/cherrypy/test/test_virtualhost.py create mode 100644 libraries/cherrypy/test/test_wsgi_ns.py create mode 100644 libraries/cherrypy/test/test_wsgi_unix_socket.py create mode 100644 libraries/cherrypy/test/test_wsgi_vhost.py create mode 100644 libraries/cherrypy/test/test_wsgiapps.py create mode 100644 libraries/cherrypy/test/test_xmlrpc.py create mode 100644 libraries/cherrypy/test/webtest.py create mode 100644 libraries/cherrypy/tutorial/README.rst create mode 100644 libraries/cherrypy/tutorial/__init__.py create mode 100644 libraries/cherrypy/tutorial/custom_error.html create mode 100644 libraries/cherrypy/tutorial/pdf_file.pdf create mode 100644 libraries/cherrypy/tutorial/tut01_helloworld.py create mode 100644 libraries/cherrypy/tutorial/tut02_expose_methods.py create mode 100644 libraries/cherrypy/tutorial/tut03_get_and_post.py create mode 100644 libraries/cherrypy/tutorial/tut04_complex_site.py create mode 100644 libraries/cherrypy/tutorial/tut05_derived_objects.py create mode 100644 libraries/cherrypy/tutorial/tut06_default_method.py create mode 100644 libraries/cherrypy/tutorial/tut07_sessions.py create mode 100644 libraries/cherrypy/tutorial/tut08_generators_and_yield.py create mode 100644 libraries/cherrypy/tutorial/tut09_files.py create mode 100644 libraries/cherrypy/tutorial/tut10_http_errors.py create mode 100644 libraries/cherrypy/tutorial/tutorial.conf create mode 100644 libraries/contextlib2.py create mode 100644 libraries/more_itertools/__init__.py create mode 100644 libraries/more_itertools/more.py create mode 100644 libraries/more_itertools/recipes.py create mode 100644 libraries/more_itertools/tests/__init__.py create mode 100644 libraries/more_itertools/tests/test_more.py create mode 100644 libraries/more_itertools/tests/test_recipes.py create mode 100644 libraries/portend.py create mode 100644 libraries/tempora/__init__.py create mode 100644 libraries/tempora/schedule.py create mode 100644 libraries/tempora/tests/test_schedule.py create mode 100644 libraries/tempora/timing.py create mode 100644 libraries/tempora/utc.py create mode 100644 libraries/zc/__init__.py create mode 100644 libraries/zc/lockfile/README.txt create mode 100644 libraries/zc/lockfile/__init__.py create mode 100644 libraries/zc/lockfile/tests.py diff --git a/libraries/backports/__init__.py b/libraries/backports/__init__.py new file mode 100644 index 00000000..69e3be50 --- /dev/null +++ b/libraries/backports/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/libraries/backports/functools_lru_cache.py b/libraries/backports/functools_lru_cache.py new file mode 100644 index 00000000..707c6c76 --- /dev/null +++ b/libraries/backports/functools_lru_cache.py @@ -0,0 +1,184 @@ +from __future__ import absolute_import + +import functools +from collections import namedtuple +from threading import RLock + +_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + + +@functools.wraps(functools.update_wrapper) +def update_wrapper(wrapper, + wrapped, + assigned = functools.WRAPPER_ASSIGNMENTS, + updated = functools.WRAPPER_UPDATES): + """ + Patch two bugs in functools.update_wrapper. + """ + # workaround for http://bugs.python.org/issue3445 + assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr)) + wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated) + # workaround for https://bugs.python.org/issue17482 + wrapper.__wrapped__ = wrapped + return wrapper + + +class _HashedSeq(list): + __slots__ = 'hashvalue' + + def __init__(self, tup, hash=hash): + self[:] = tup + self.hashvalue = hash(tup) + + def __hash__(self): + return self.hashvalue + + +def _make_key(args, kwds, typed, + kwd_mark=(object(),), + fasttypes=set([int, str, frozenset, type(None)]), + sorted=sorted, tuple=tuple, type=type, len=len): + 'Make a cache key from optionally typed positional and keyword arguments' + key = args + if kwds: + sorted_items = sorted(kwds.items()) + key += kwd_mark + for item in sorted_items: + key += item + if typed: + key += tuple(type(v) for v in args) + if kwds: + key += tuple(type(v) for k, v in sorted_items) + elif len(key) == 1 and type(key[0]) in fasttypes: + return key[0] + return _HashedSeq(key) + + +def lru_cache(maxsize=100, typed=False): + """Least-recently-used cache decorator. + + If *maxsize* is set to None, the LRU features are disabled and the cache + can grow without bound. + + If *typed* is True, arguments of different types will be cached separately. + For example, f(3.0) and f(3) will be treated as distinct calls with + distinct results. + + Arguments to the cached function must be hashable. + + View the cache statistics named tuple (hits, misses, maxsize, currsize) with + f.cache_info(). Clear the cache and statistics with f.cache_clear(). + Access the underlying function with f.__wrapped__. + + See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + + """ + + # Users should only access the lru_cache through its public API: + # cache_info, cache_clear, and f.__wrapped__ + # The internals of the lru_cache are encapsulated for thread safety and + # to allow the implementation to change (including a possible C version). + + def decorating_function(user_function): + + cache = dict() + stats = [0, 0] # make statistics updateable non-locally + HITS, MISSES = 0, 1 # names for the stats fields + make_key = _make_key + cache_get = cache.get # bound method to lookup key or return None + _len = len # localize the global len() function + lock = RLock() # because linkedlist updates aren't threadsafe + root = [] # root of the circular doubly linked list + root[:] = [root, root, None, None] # initialize by pointing to self + nonlocal_root = [root] # make updateable non-locally + PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields + + if maxsize == 0: + + def wrapper(*args, **kwds): + # no caching, just do a statistics update after a successful call + result = user_function(*args, **kwds) + stats[MISSES] += 1 + return result + + elif maxsize is None: + + def wrapper(*args, **kwds): + # simple caching without ordering or size limit + key = make_key(args, kwds, typed) + result = cache_get(key, root) # root used here as a unique not-found sentinel + if result is not root: + stats[HITS] += 1 + return result + result = user_function(*args, **kwds) + cache[key] = result + stats[MISSES] += 1 + return result + + else: + + def wrapper(*args, **kwds): + # size limited caching that tracks accesses by recency + key = make_key(args, kwds, typed) if kwds or typed else args + with lock: + link = cache_get(key) + if link is not None: + # record recent use of the key by moving it to the front of the list + root, = nonlocal_root + link_prev, link_next, key, result = link + link_prev[NEXT] = link_next + link_next[PREV] = link_prev + last = root[PREV] + last[NEXT] = root[PREV] = link + link[PREV] = last + link[NEXT] = root + stats[HITS] += 1 + return result + result = user_function(*args, **kwds) + with lock: + root, = nonlocal_root + if key in cache: + # getting here means that this same key was added to the + # cache while the lock was released. since the link + # update is already done, we need only return the + # computed result and update the count of misses. + pass + elif _len(cache) >= maxsize: + # use the old root to store the new key and result + oldroot = root + oldroot[KEY] = key + oldroot[RESULT] = result + # empty the oldest link and make it the new root + root = nonlocal_root[0] = oldroot[NEXT] + oldkey = root[KEY] + root[KEY] = root[RESULT] = None + # now update the cache dictionary for the new links + del cache[oldkey] + cache[key] = oldroot + else: + # put result in a new link at the front of the list + last = root[PREV] + link = [last, root, key, result] + last[NEXT] = root[PREV] = cache[key] = link + stats[MISSES] += 1 + return result + + def cache_info(): + """Report cache statistics""" + with lock: + return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) + + def cache_clear(): + """Clear the cache and cache statistics""" + with lock: + cache.clear() + root = nonlocal_root[0] + root[:] = [root, root, None, None] + stats[:] = [0, 0] + + wrapper.__wrapped__ = user_function + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return update_wrapper(wrapper, user_function) + + return decorating_function diff --git a/libraries/cheroot/__init__.py b/libraries/cheroot/__init__.py new file mode 100644 index 00000000..a313660e --- /dev/null +++ b/libraries/cheroot/__init__.py @@ -0,0 +1,6 @@ +"""High-performance, pure-Python HTTP server used by CherryPy.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__version__ = '6.4.0' diff --git a/libraries/cheroot/__main__.py b/libraries/cheroot/__main__.py new file mode 100644 index 00000000..d2e27c10 --- /dev/null +++ b/libraries/cheroot/__main__.py @@ -0,0 +1,6 @@ +"""Stub for accessing the Cheroot CLI tool.""" + +from .cli import main + +if __name__ == '__main__': + main() diff --git a/libraries/cheroot/_compat.py b/libraries/cheroot/_compat.py new file mode 100644 index 00000000..e98f91f9 --- /dev/null +++ b/libraries/cheroot/_compat.py @@ -0,0 +1,66 @@ +"""Compatibility code for using Cheroot with various versions of Python.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +import six + +if six.PY3: + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b.decode(encoding) +else: + # Python 2 + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses + # this to signal that it wants to pass a string with embedded \uXXXX + # escapes, but without having to prefix it with u'' for Python 2, + # but no prefix for Python 3. + if encoding == 'escape': + return six.u( + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: six.unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 + # is almost always what was intended. + return n.decode(encoding) + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b + + +def assert_native(n): + """Check whether the input is of nativ ``str`` type. + + Raises: + TypeError: in case of failed check + + """ + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) diff --git a/libraries/cheroot/cli.py b/libraries/cheroot/cli.py new file mode 100644 index 00000000..6d59fb5c --- /dev/null +++ b/libraries/cheroot/cli.py @@ -0,0 +1,233 @@ +"""Command line tool for starting a Cheroot WSGI/HTTP server instance. + +Basic usage:: + + # Start a server on 127.0.0.1:8000 with the default settings + # for the WSGI app myapp/wsgi.py:application() + cheroot myapp.wsgi + + # Start a server on 0.0.0.0:9000 with 8 threads + # for the WSGI app myapp/wsgi.py:main_app() + cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8 + + # Start a server for the cheroot.server.Gateway subclass + # myapp/gateway.py:HTTPGateway + cheroot myapp.gateway:HTTPGateway + + # Start a server on the UNIX socket /var/spool/myapp.sock + cheroot myapp.wsgi --bind /var/spool/myapp.sock + + # Start a server on the abstract UNIX socket CherootServer + cheroot myapp.wsgi --bind @CherootServer +""" + +import argparse +from importlib import import_module +import os +import sys +import contextlib + +import six + +from . import server +from . import wsgi + + +__metaclass__ = type + + +class BindLocation: + """A class for storing the bind location for a Cheroot instance.""" + + +class TCPSocket(BindLocation): + """TCPSocket.""" + + def __init__(self, address, port): + """Initialize. + + Args: + address (str): Host name or IP address + port (int): TCP port number + """ + self.bind_addr = address, port + + +class UnixSocket(BindLocation): + """UnixSocket.""" + + def __init__(self, path): + """Initialize.""" + self.bind_addr = path + + +class AbstractSocket(BindLocation): + """AbstractSocket.""" + + def __init__(self, addr): + """Initialize.""" + self.bind_addr = '\0{}'.format(self.abstract_socket) + + +class Application: + """Application.""" + + @classmethod + def resolve(cls, full_path): + """Read WSGI app/Gateway path string and import application module.""" + mod_path, _, app_path = full_path.partition(':') + app = getattr(import_module(mod_path), app_path or 'application') + + with contextlib.suppress(TypeError): + if issubclass(app, server.Gateway): + return GatewayYo(app) + + return cls(app) + + def __init__(self, wsgi_app): + """Initialize.""" + if not callable(wsgi_app): + raise TypeError( + 'Application must be a callable object or ' + 'cheroot.server.Gateway subclass' + ) + self.wsgi_app = wsgi_app + + def server_args(self, parsed_args): + """Return keyword args for Server class.""" + args = { + arg: value + for arg, value in vars(parsed_args).items() + if not arg.startswith('_') and value is not None + } + args.update(vars(self)) + return args + + def server(self, parsed_args): + """Server.""" + return wsgi.Server(**self.server_args(parsed_args)) + + +class GatewayYo: + """Gateway.""" + + def __init__(self, gateway): + """Init.""" + self.gateway = gateway + + def server(self, parsed_args): + """Server.""" + server_args = vars(self) + server_args['bind_addr'] = parsed_args['bind_addr'] + if parsed_args.max is not None: + server_args['maxthreads'] = parsed_args.max + if parsed_args.numthreads is not None: + server_args['minthreads'] = parsed_args.numthreads + return server.HTTPServer(**server_args) + + +def parse_wsgi_bind_location(bind_addr_string): + """Convert bind address string to a BindLocation.""" + # try and match for an IP/hostname and port + match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string)) + try: + addr = match.hostname + port = match.port + if addr is not None or port is not None: + return TCPSocket(addr, port) + except ValueError: + pass + + # else, assume a UNIX socket path + # if the string begins with an @ symbol, use an abstract socket + if bind_addr_string.startswith('@'): + return AbstractSocket(bind_addr_string[1:]) + return UnixSocket(path=bind_addr_string) + + +def parse_wsgi_bind_addr(bind_addr_string): + """Convert bind address string to bind address parameter.""" + return parse_wsgi_bind_location(bind_addr_string).bind_addr + + +_arg_spec = { + '_wsgi_app': dict( + metavar='APP_MODULE', + type=Application.resolve, + help='WSGI application callable or cheroot.server.Gateway subclass', + ), + '--bind': dict( + metavar='ADDRESS', + dest='bind_addr', + type=parse_wsgi_bind_addr, + default='[::1]:8000', + help='Network interface to listen on (default: [::1]:8000)', + ), + '--chdir': dict( + metavar='PATH', + type=os.chdir, + help='Set the working directory', + ), + '--server-name': dict( + dest='server_name', + type=str, + help='Web server name to be advertised via Server HTTP header', + ), + '--threads': dict( + metavar='INT', + dest='numthreads', + type=int, + help='Minimum number of worker threads', + ), + '--max-threads': dict( + metavar='INT', + dest='max', + type=int, + help='Maximum number of worker threads', + ), + '--timeout': dict( + metavar='INT', + dest='timeout', + type=int, + help='Timeout in seconds for accepted connections', + ), + '--shutdown-timeout': dict( + metavar='INT', + dest='shutdown_timeout', + type=int, + help='Time in seconds to wait for worker threads to cleanly exit', + ), + '--request-queue-size': dict( + metavar='INT', + dest='request_queue_size', + type=int, + help='Maximum number of queued connections', + ), + '--accepted-queue-size': dict( + metavar='INT', + dest='accepted_queue_size', + type=int, + help='Maximum number of active requests in queue', + ), + '--accepted-queue-timeout': dict( + metavar='INT', + dest='accepted_queue_timeout', + type=int, + help='Timeout in seconds for putting requests into queue', + ), +} + + +def main(): + """Create a new Cheroot instance with arguments from the command line.""" + parser = argparse.ArgumentParser( + description='Start an instance of the Cheroot WSGI/HTTP server.') + for arg, spec in _arg_spec.items(): + parser.add_argument(arg, **spec) + raw_args = parser.parse_args() + + # ensure cwd in sys.path + '' in sys.path or sys.path.insert(0, '') + + # create a server based on the arguments provided + raw_args._wsgi_app.server(raw_args).safe_start() diff --git a/libraries/cheroot/errors.py b/libraries/cheroot/errors.py new file mode 100644 index 00000000..82412b42 --- /dev/null +++ b/libraries/cheroot/errors.py @@ -0,0 +1,58 @@ +"""Collection of exceptions raised and/or processed by Cheroot.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import errno +import sys + + +class MaxSizeExceeded(Exception): + """Exception raised when a client sends more data then acceptable within limit. + + Depends on ``request.body.maxbytes`` config option if used within CherryPy + """ + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + + +socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR') + +socket_errors_to_ignore = plat_specific_errors( + 'EPIPE', + 'EBADF', 'WSAEBADF', + 'ENOTSOCK', 'WSAENOTSOCK', + 'ETIMEDOUT', 'WSAETIMEDOUT', + 'ECONNREFUSED', 'WSAECONNREFUSED', + 'ECONNRESET', 'WSAECONNRESET', + 'ECONNABORTED', 'WSAECONNABORTED', + 'ENETRESET', 'WSAENETRESET', + 'EHOSTDOWN', 'EHOSTUNREACH', +) +socket_errors_to_ignore.append('timed out') +socket_errors_to_ignore.append('The read operation timed out') +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +if sys.platform == 'darwin': + socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE')) + socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE')) diff --git a/libraries/cheroot/makefile.py b/libraries/cheroot/makefile.py new file mode 100644 index 00000000..a76f2eda --- /dev/null +++ b/libraries/cheroot/makefile.py @@ -0,0 +1,387 @@ +"""Socket file object.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket + +try: + # prefer slower Python-based io module + import _pyio as io +except ImportError: + # Python 2.6 + import io + +import six + +from . import errors + + +class BufferedWriter(io.BufferedWriter): + """Faux file object attached to a socket object.""" + + def write(self, b): + """Write bytes to buffer.""" + self._checkClosed() + if isinstance(b, str): + raise TypeError("can't write str to binary stream") + + with self._write_lock: + self._write_buf.extend(b) + self._flush_unlocked() + return len(b) + + def _flush_unlocked(self): + self._checkClosed('flush of closed file') + while self._write_buf: + try: + # ssl sockets only except 'bytes', not bytearrays + # so perhaps we should conditionally wrap this for perf? + n = self.raw.write(bytes(self._write_buf)) + except io.BlockingIOError as e: + n = e.characters_written + del self._write_buf[:n] + + +def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): + """File object attached to a socket object.""" + if 'r' in mode: + return io.BufferedReader(socket.SocketIO(sock, mode), bufsize) + else: + return BufferedWriter(socket.SocketIO(sock, mode), bufsize) + + +class MakeFile_PY2(getattr(socket, '_fileobject', object)): + """Faux file object attached to a socket object.""" + + def __init__(self, *args, **kwargs): + """Initialize faux file object.""" + self.bytes_read = 0 + self.bytes_written = 0 + socket._fileobject.__init__(self, *args, **kwargs) + + def write(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error as e: + if e.args[0] not in errors.socket_errors_nonblocking: + raise + + def send(self, data): + """Send some part of message to the socket.""" + bytes_sent = self._sock.send(data) + self.bytes_written += bytes_sent + return bytes_sent + + def flush(self): + """Write all data from buffer to socket and reset write buffer.""" + if self._wbuf: + buffer = ''.join(self._wbuf) + self._wbuf = [] + self.write(buffer) + + def recv(self, size): + """Receive message of a size from the socket.""" + while True: + try: + data = self._sock.recv(size) + self.bytes_read += len(data) + return data + except socket.error as e: + what = ( + e.args[0] not in errors.socket_errors_nonblocking + and e.args[0] not in errors.socket_error_eintr + ) + if what: + raise + + class FauxSocket: + """Faux socket with the minimal interface required by pypy.""" + + def _reuse(self): + pass + + _fileobject_uses_str_type = six.PY2 and isinstance( + socket._fileobject(FauxSocket())._rbuf, six.string_types) + + # FauxSocket is no longer needed + del FauxSocket + + if not _fileobject_uses_str_type: + def read(self, size=-1): + """Read data from the socket to buffer.""" + # Use max, disallow tiny reads in a loop as they are very + # inefficient. + # We never leave read() with any leftover data from a new recv() + # call in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned + # by recv() minimizes memory usage and fragmentation that occurs + # when rbufsize is large compared to the typical return value of + # recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and + # return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, 'recv(%d) returned %d bytes' % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + # assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + data = None + recv = self.recv + while data != '\n': + data = recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + + buf.seek(0, 2) # seek end + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when + # returning a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + # assert buf_len == buf.tell() + return buf.getvalue() + else: + def read(self, size=-1): + """Read data from the socket to buffer.""" + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = '' + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return ''.join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == '' + buffers = [] + while data != '\n': + data = self.recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return ''.join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + +MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3 diff --git a/libraries/cheroot/server.py b/libraries/cheroot/server.py new file mode 100644 index 00000000..44070490 --- /dev/null +++ b/libraries/cheroot/server.py @@ -0,0 +1,2001 @@ +""" +A high-speed, production ready, thread pooled, generic HTTP server. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = HTTPServer(...) + server.start() + -> while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return + +For running a server you can invoke :func:`start() <HTTPServer.start()>` (it +will run the server forever) or use invoking :func:`prepare() +<HTTPServer.prepare()>` and :func:`serve() <HTTPServer.serve()>` like this:: + + server = HTTPServer(...) + server.prepare() + try: + threading.Thread(target=server.serve).start() + + # waiting/detecting some appropriate stop condition here + ... + + finally: + server.stop() + +And now for a trivial doctest to exercise the test suite + +>>> 'HTTPServer' in globals() +True + +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import io +import re +import email.utils +import socket +import sys +import time +import traceback as traceback_ +import logging +import platform +import xbmc + +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + +import six +from six.moves import queue +from six.moves import urllib + +from . import errors, __version__ +from ._compat import bton, ntou +from .workers import threadpool +from .makefile import MakeFile + + +__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'Gateway', 'get_ssl_adapter_class') + +""" +Special KODI case: +Android does not have support for grp and pwd +But Python has issues reporting that this is running on Android (it shows as Linux2). +We're instead using xbmc library to detect that. +""" +IS_WINDOWS = platform.system() == 'Windows' +IS_ANDROID = xbmc.getCondVisibility('system.platform.linux') and xbmc.getCondVisibility('system.platform.android') + +if not (IS_WINDOWS or IS_ANDROID): + import grp + import pwd + import struct + + +if IS_WINDOWS and hasattr(socket, 'AF_INET6'): + if not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 + if not hasattr(socket, 'IPV6_V6ONLY'): + socket.IPV6_V6ONLY = 27 + + +if not hasattr(socket, 'SO_PEERCRED'): + """ + NOTE: the value for SO_PEERCRED can be architecture specific, in + which case the getsockopt() will hopefully fail. The arch + specific value could be derived from platform.processor() + """ + socket.SO_PEERCRED = 17 + + +LF = b'\n' +CRLF = b'\r\n' +TAB = b'\t' +SPACE = b' ' +COLON = b':' +SEMICOLON = b';' +EMPTY = b'' +ASTERISK = b'*' +FORWARD_SLASH = b'/' +QUOTED_SLASH = b'%2F' +QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH) + + +comma_separated_headers = [ + b'Accept', b'Accept-Charset', b'Accept-Encoding', + b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control', + b'Connection', b'Content-Encoding', b'Content-Language', b'Expect', + b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE', + b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning', + b'WWW-Authenticate', +] + + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +class HeaderReader: + """Object for reading headers from an HTTP request. + + Interface and default implementation. + """ + + def __call__(self, rfile, hdict=None): + """ + Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP + spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError('Illegal header line.') + v = v.strip() + k = self._transform_key(k) + hname = k + + if not self._allow_header(k): + continue + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = b', '.join((existing, v)) + hdict[hname] = v + + return hdict + + def _allow_header(self, key_name): + return True + + def _transform_key(self, key_name): + # TODO: what about TE and WWW-Authenticate? + return key_name.strip().title() + + +class DropUnderscoreHeaderReader(HeaderReader): + """Custom HeaderReader to exclude any headers with underscores in them.""" + + def _allow_header(self, key_name): + orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name) + return orig and '_' not in key_name + + +class SizeCheckWrapper: + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + """Initialize SizeCheckWrapper instance. + + Args: + rfile (file): file of a limited size + maxlen (int): maximum length of the file being read + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded() + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See https://github.com/cherrypy/cherrypy/issues/421 + if len(data) < 256 or data[-1:] == LF: + return EMPTY.join(res) + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + next = __next__ + + +class KnownLengthRFile: + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + """Initialize KnownLengthRFile instance. + + Args: + rfile (file): file of a known size + content_length (int): length of the file being read + + """ + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.remaining -= len(data) + return data + + next = __next__ + + +class ChunkedRFile: + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + """Initialize ChunkedRFile instance. + + Args: + rfile (file): file encoded with the 'chunked' transfer encoding + maxlen (int): maximum length of the file being read + bufsize (int): size of the buffer used to read the file + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded( + 'Request Entity Too Large', self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError('Bad chunked transfer size: ' + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +# if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError('Request Entity Too Large') + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + 'got ' + repr(crlf) + ')') + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + self.buffer = EMPTY + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + self.buffer = EMPTY + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + """Read HTTP headers and yield them. + + Returns: + Generator: yields CRLF separated lines. + + """ + if not self.closed: + raise ValueError( + 'Cannot read trailers until the request body has been read.') + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError('Request Entity Too Large') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + yield line + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + +class HTTPRequest: + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + header_reader = HeaderReader() + """ + A HeaderReader instance or compatible reader. + """ + + def __init__(self, server, conn, proxy_mode=False, strict_mode=True): + """Initialize HTTP request container instance. + + Args: + server (HTTPServer): web server object receiving this request + conn (HTTPConnection): HTTP connection object for this request + proxy_mode (bool): whether this HTTPServer should behave as a PROXY + server for certain requests + strict_mode (bool): whether we should return a 400 Bad Request when + we encounter a request that a HTTP compliant client should not be + making + """ + self.server = server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = b'http' + if self.server.ssl_adapter is not None: + self.scheme = b'https' + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = '' + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + self.proxy_mode = proxy_mode + self.strict_mode = strict_mode + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except errors.MaxSizeExceeded: + self.simple_response( + '414 Request-URI Too Long', + 'The Request-URI sent with the request exceeds the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except errors.MaxSizeExceeded: + self.simple_response( + '413 Request Entity Too Large', + 'The headers sent with the request exceed the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): + """Read and parse first line of the HTTP request. + + Returns: + bool: True if the request line is valid or False if it's malformed. + + """ + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response( + '400 Bad Request', 'HTTP requires CRLF terminators') + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + if not req_protocol.startswith(b'HTTP/'): + self.simple_response( + '400 Bad Request', 'Malformed Request-Line: bad protocol' + ) + return False + rp = req_protocol[5:].split(b'.', 1) + rp = tuple(map(int, rp)) # Minor.Major must be threat as integers + if rp > (1, 1): + self.simple_response( + '505 HTTP Version Not Supported', 'Cannot fulfill request' + ) + return False + except (ValueError, IndexError): + self.simple_response('400 Bad Request', 'Malformed Request-Line') + return False + + self.uri = uri + self.method = method.upper() + + if self.strict_mode and method != self.method: + resp = ( + 'Malformed method name: According to RFC 2616 ' + '(section 5.1.1) and its successors ' + 'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) ' + 'method names are case-sensitive and uppercase.' + ) + self.simple_response('400 Bad Request', resp) + return False + + try: + if six.PY2: # FIXME: Figure out better way to do this + # Ref: https://stackoverflow.com/a/196392/595220 (like this?) + """This is a dummy check for unicode in URI.""" + ntou(bton(uri, 'ascii'), 'ascii') + scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) + except UnicodeError: + self.simple_response('400 Bad Request', 'Malformed Request-URI') + return False + + if self.method == b'OPTIONS': + # TODO: cover this branch with tests + path = (uri + # https://tools.ietf.org/html/rfc7230#section-5.3.4 + if self.proxy_mode or uri == ASTERISK + else path) + elif self.method == b'CONNECT': + # TODO: cover this branch with tests + if not self.proxy_mode: + self.simple_response('405 Method Not Allowed') + return False + + # `urlsplit()` above parses "example.com:3128" as path part of URI. + # this is a workaround, which makes it detect netloc correctly + uri_split = urllib.parse.urlsplit(b'//' + uri) + _scheme, _authority, _path, _qs, _fragment = uri_split + _port = EMPTY + try: + _port = uri_split.port + except ValueError: + pass + + # FIXME: use third-party validation to make checks against RFC + # the validation doesn't take into account, that urllib parses + # invalid URIs without raising errors + # https://tools.ietf.org/html/rfc7230#section-5.3.3 + invalid_path = ( + _authority != uri + or not _port + or any((_scheme, _path, _qs, _fragment)) + ) + if invalid_path: + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI: request-' + 'target must match authority-form.') + return False + + authority = path = _authority + scheme = qs = fragment = EMPTY + else: + uri_is_absolute_form = (scheme or authority) + + disallowed_absolute = ( + self.strict_mode + and not self.proxy_mode + and uri_is_absolute_form + ) + if disallowed_absolute: + # https://tools.ietf.org/html/rfc7230#section-5.3.2 + # (absolute form) + """Absolute URI is only allowed within proxies.""" + self.simple_response( + '400 Bad Request', + 'Absolute URI not allowed if server is not a proxy.', + ) + return False + + invalid_path = ( + self.strict_mode + and not uri.startswith(FORWARD_SLASH) + and not uri_is_absolute_form + ) + if invalid_path: + # https://tools.ietf.org/html/rfc7230#section-5.3.1 + # (origin_form) and + """Path should start with a forward slash.""" + resp = ( + 'Invalid path in Request-URI: request-target must contain ' + 'origin-form which starts with absolute-path (URI ' + 'starting with a slash "/").' + ) + self.simple_response('400 Bad Request', resp) + return False + + if fragment: + self.simple_response('400 Bad Request', + 'Illegal #fragment in Request-URI.') + return False + + if path is None: + # FIXME: It looks like this case cannot happen + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI.') + return False + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not + # "/this/path". + try: + # TODO: Figure out whether exception can really happen here. + # It looks like it's caught on urlsplit() call above. + atoms = [ + urllib.parse.unquote_to_bytes(x) + for x in QUOTED_SLASH_REGEX.split(path) + ] + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + path = QUOTED_SLASH.join(atoms) + + if not path.startswith(FORWARD_SLASH): + path = FORWARD_SLASH + path + + if scheme is not EMPTY: + self.scheme = scheme + self.authority = authority + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response('505 HTTP Version Not Supported') + return False + + self.request_protocol = req_protocol + self.response_protocol = 'HTTP/%s.%s' % min(rp, sp) + + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + # then all the http headers + try: + self.header_reader(self.rfile, self.inheaders) + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + + try: + cl = int(self.inheaders.get(b'Content-Length', 0)) + except ValueError: + self.simple_response( + '400 Bad Request', + 'Malformed Content-Length Header.') + return False + + if mrbs and cl > mrbs: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the maximum ' + 'allowed bytes.') + return False + + # Persistent connection support + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 + if self.inheaders.get(b'Connection', b'') == b'close': + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get(b'Connection', b'') != b'Keep-Alive': + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == 'HTTP/1.1': + te = self.inheaders.get(b'Transfer-Encoding') + if te: + te = [x.strip().lower() for x in te.split(b',') if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == b'chunked': + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response('501 Unimplemented') + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get(b'Expect', b'') == b'100-continue': + # Don't use simple_response here, because it emits headers + # we don't want. See + # https://github.com/cherrypy/cherrypy/issues/951 + msg = self.server.protocol.encode('ascii') + msg += b' 100 Continue\r\n\r\n' + try: + self.conn.wfile.write(msg) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return True + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get(b'Content-Length', 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the ' + 'maximum allowed bytes.') + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + self.ready and self.ensure_headers_sent() + + if self.chunked_write: + self.conn.wfile.write(b'0\r\n\r\n') + + def simple_response(self, status, msg=''): + """Write a simple response back to the client.""" + status = str(status) + proto_status = '%s %s\r\n' % (self.server.protocol, status) + content_length = 'Content-Length: %s\r\n' % len(msg) + content_type = 'Content-Type: text/plain\r\n' + buf = [ + proto_status.encode('ISO-8859-1'), + content_length.encode('ISO-8859-1'), + content_type.encode('ISO-8859-1'), + ] + + if status[:3] in ('413', '414'): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append(b'Connection: close\r\n') + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = '400 Bad Request' + + buf.append(CRLF) + if msg: + if isinstance(msg, six.text_type): + msg = msg.encode('ISO-8859-1') + buf.append(msg) + + try: + self.conn.wfile.write(EMPTY.join(buf)) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + + def ensure_headers_sent(self): + """Ensure headers are sent to the client if not already sent.""" + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + chunk_size_hex = hex(len(chunk))[2:].encode('ascii') + buf = [chunk_size_hex, CRLF, chunk, CRLF] + self.conn.wfile.write(EMPTY.join(buf)) + else: + self.conn.wfile.write(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif b'content-length' not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + needs_chunked = ( + self.response_protocol == 'HTTP/1.1' + and self.method != b'HEAD' + ) + if needs_chunked: + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append((b'Transfer-Encoding', b'chunked')) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if b'connection' not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append((b'Connection', b'close')) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append((b'Connection', b'Keep-Alive')) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if b'date' not in hkeys: + self.outheaders.append(( + b'Date', + email.utils.formatdate(usegmt=True).encode('ISO-8859-1'), + )) + + if b'server' not in hkeys: + self.outheaders.append(( + b'Server', + self.server.server_name.encode('ISO-8859-1'), + )) + + proto = self.server.protocol.encode('ascii') + buf = [proto + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.write(EMPTY.join(buf)) + + +class HTTPConnection: + """An HTTP connection (active socket).""" + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = io.DEFAULT_BUFFER_SIZE + wbufsize = io.DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + peercreds_enabled = False + peercreds_resolve_enabled = False + + def __init__(self, server, sock, makefile=MakeFile): + """Initialize HTTPConnection instance. + + Args: + server (HTTPServer): web server object receiving this request + socket (socket._socketobject): the raw socket object (usually + TCP) for this connection + makefile (file): a fileobject class for reading from the socket + """ + self.server = server + self.socket = sock + self.rfile = makefile(sock, 'rb', self.rbufsize) + self.wfile = makefile(sock, 'wb', self.wbufsize) + self.requests_seen = 0 + + self.peercreds_enabled = self.server.peercreds_enabled + self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled + + # LRU cached methods: + # Ref: https://stackoverflow.com/a/14946506/595220 + self.resolve_peer_creds = ( + lru_cache(maxsize=1)(self.resolve_peer_creds) + ) + self.get_peer_creds = ( + lru_cache(maxsize=1)(self.get_peer_creds) + ) + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error as ex: + errnum = ex.args[0] + # sadly SSL sockets return a different (longer) time out string + timeout_errs = 'timed out', 'The read operation timed out' + if errnum in timeout_errs: + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See https://github.com/cherrypy/cherrypy/issues/853 + if (not request_seen) or (req and req.started_request): + self._conditional_error(req, '408 Request Timeout') + elif errnum not in errors.socket_errors_to_ignore: + self.server.error_log('socket.error %s' % repr(errnum), + level=logging.WARNING, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + except (KeyboardInterrupt, SystemExit): + raise + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + except Exception as ex: + self.server.error_log( + repr(ex), level=logging.ERROR, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + + linger = False + + def _handle_no_ssl(self, req): + if not req or req.sent_headers: + return + # Unwrap wfile + self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize) + msg = ( + 'The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + req.simple_response('400 Bad Request', msg) + self.linger = True + + def _conditional_error(self, req, response): + """Respond with an error. + + Don't bother writing if a response + has already started being written. + """ + if not req or req.sent_headers: + return + + try: + req.simple_response(response) + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + self._close_kernel_socket() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + def get_peer_creds(self): # LRU cached on per-instance basis, see __init__ + """Return the PID/UID/GID tuple of the peer socket for UNIX sockets. + + This function uses SO_PEERCRED to query the UNIX PID, UID, GID + of the peer, which is only available if the bind address is + a UNIX domain socket. + + Raises: + NotImplementedError: in case of unsupported socket type + RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled + + """ + PEERCRED_STRUCT_DEF = '3i' + + if IS_WINDOWS or self.socket.family != socket.AF_UNIX: + raise NotImplementedError( + 'SO_PEERCRED is only supported in Linux kernel and WSL' + ) + elif not self.peercreds_enabled: + raise RuntimeError( + 'Peer creds lookup is disabled within this server' + ) + + try: + peer_creds = self.socket.getsockopt( + socket.SOL_SOCKET, socket.SO_PEERCRED, + struct.calcsize(PEERCRED_STRUCT_DEF) + ) + except socket.error as socket_err: + """Non-Linux kernels don't support SO_PEERCRED. + + Refs: + http://welz.org.za/notes/on-peer-cred.html + https://github.com/daveti/tcpSockHack + msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025 + """ + six.raise_from( # 3.6+: raise RuntimeError from socket_err + RuntimeError, + socket_err, + ) + else: + pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds) + return pid, uid, gid + + @property + def peer_pid(self): + """Return the id of the connected peer process.""" + pid, _, _ = self.get_peer_creds() + return pid + + @property + def peer_uid(self): + """Return the user id of the connected peer process.""" + _, uid, _ = self.get_peer_creds() + return uid + + @property + def peer_gid(self): + """Return the group id of the connected peer process.""" + _, _, gid = self.get_peer_creds() + return gid + + def resolve_peer_creds(self): # LRU cached on per-instance basis + """Return the username and group tuple of the peercreds if available. + + Raises: + NotImplementedError: in case of unsupported OS + RuntimeError: in case of UID/GID lookup unsupported or disabled + + """ + if (IS_WINDOWS or IS_ANDROID): + raise NotImplementedError( + 'UID/GID lookup can only be done under UNIX-like OS' + ) + elif not self.peercreds_resolve_enabled: + raise RuntimeError( + 'UID/GID lookup is disabled within this server' + ) + + user = pwd.getpwuid(self.peer_uid).pw_name # [0] + group = grp.getgrgid(self.peer_gid).gr_name # [0] + + return user, group + + @property + def peer_user(self): + """Return the username of the connected peer process.""" + user, _ = self.resolve_peer_creds() + return user + + @property + def peer_group(self): + """Return the group of the connected peer process.""" + _, group = self.resolve_peer_creds() + return group + + def _close_kernel_socket(self): + """Close kernel socket in outdated Python versions. + + On old Python versions, + Python's socket module does NOT call close on the kernel + socket when you call socket.close(). We do so manually here + because we want this server to send a FIN TCP segment + immediately. Note this must be called *before* calling + socket.close(), because the latter drops its reference to + the kernel socket. + """ + if six.PY2 and hasattr(self.socket, '_sock'): + self.socket._sock.close() + + +def prevent_socket_inheritance(sock): + """Stub inheritance prevention. + + Dummy function, since neither fcntl nor ctypes are available. + """ + pass + + +class HTTPServer: + """An HTTP server.""" + + _bind_addr = '127.0.0.1' + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create. + + (default -1 = no limit)""" + + server_name = None + """The name of the server; defaults to ``self.version``.""" + + protocol = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections. + + (default 5).""" + + shutdown_timeout = 5 + """The total time to wait for worker threads to cleanly exit. + + Specified in seconds.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = 'Cheroot/' + __version__ + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``. + """ + + ready = False + """Internal flag which indicating the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of ssl.Adapter (or a subclass). + + You must have the corresponding SSL driver library installed. + """ + + peercreds_enabled = False + """If True, peer cred lookup can be performed via UNIX domain socket.""" + + peercreds_resolve_enabled = False + """If True, username/group will be looked up in the OS from peercreds.""" + + def __init__( + self, bind_addr, gateway, + minthreads=10, maxthreads=-1, server_name=None, + peercreds_enabled=False, peercreds_resolve_enabled=False, + ): + """Initialize HTTPServer instance. + + Args: + bind_addr (tuple): network interface to listen to + gateway (Gateway): gateway for processing HTTP requests + minthreads (int): minimum number of threads for HTTP thread pool + maxthreads (int): maximum number of threads for HTTP thread pool + server_name (str): web server name to be advertised via Server + HTTP header + """ + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = threadpool.ThreadPool( + self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = self.version + self.server_name = server_name + self.peercreds_enabled = peercreds_enabled + self.peercreds_resolve_enabled = ( + peercreds_resolve_enabled and peercreds_enabled + ) + self.clear_stats() + + def clear_stats(self): + """Reset server stat counters..""" + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, 'qsize', None), + 'Threads': lambda s: len(getattr(self.requests, '_threads', [])), + 'Threads Idle': lambda s: getattr(self.requests, 'idle', None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum( + [w['Requests'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) for w in s['Worker Threads'].values()], + 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum( + [w['Work Time'](w) for w in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats + + def runtime(self): + """Return server uptime.""" + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + """Render Server instance representing bind address.""" + return '%s.%s(%r)' % (self.__module__, self.__class__.__name__, + self.bind_addr) + + @property + def bind_addr(self): + """Return the interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string. + + Systemd socket activation is automatic and doesn't require tempering + with this variable. + """ + return self._bind_addr + + @bind_addr.setter + def bind_addr(self, value): + """Set the interface on which to listen for connections.""" + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + 'to listen on all active interfaces.') + self._bind_addr = value + + def safe_start(self): + """Run the server forever, and stop it cleanly on exit.""" + try: + self.start() + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.error_log('Keyboard Interrupt: shutting down') + self.stop() + raise + except SystemExit: + self.error_log('SystemExit raised: shutting down') + self.stop() + raise + + def prepare(self): + """Prepare server to serving requests. + + It binds a socket's port, setups the socket to ``listen()`` and does + other preparing things. + """ + self._interrupt = None + + if self.software is None: + self.software = '%s Server' % self.version + + # Select the appropriate socket + self.socket = None + if os.getenv('LISTEN_PID', None): + # systemd socket activation + self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) + elif isinstance(self.bind_addr, six.string_types): + # AF_UNIX socket + + # So we can reuse the socket... + try: + os.unlink(self.bind_addr) + except Exception: + pass + + # So everyone can access the socket... + try: + os.chmod(self.bind_addr, 0o777) + except Exception: + pass + + info = [ + (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 + # addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo( + host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + sock_type = socket.AF_INET + bind_addr = self.bind_addr + + if ':' in host: + sock_type = socket.AF_INET6 + bind_addr = bind_addr + (0, 0) + + info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)] + + if not self.socket: + msg = 'No socket could be created' + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + break + except socket.error as serr: + msg = '%s -- (%s: %s)' % (msg, sa, serr) + if self.socket: + self.socket.close() + self.socket = None + + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + + def serve(self): + """Serve requests, after invoking :func:`prepare()`.""" + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.error_log('Error in HTTPServer.tick', level=logging.ERROR, + traceback=True) + + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def start(self): + """Run the server forever. + + It is shortcut for invoking :func:`prepare()` then :func:`serve()`. + """ + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrypy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self.prepare() + self.serve() + + def error_log(self, msg='', level=20, traceback=False): + """Write error message to log. + + Args: + msg (str): error message + level (int): logging level + traceback (bool): add traceback to output or not + """ + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = traceback_.format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + if not IS_WINDOWS: + # Windows has different semantics for SO_REUSEADDR, + # so don't set it. + # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + host, port = self.bind_addr[:2] + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See + # https://github.com/cherrypy/cherrypy/issues/871. + listening_ipv6 = ( + hasattr(socket, 'AF_INET6') + and family == socket.AF_INET6 + and host in ('::', '::0', '::0.0.0.0') + ) + if listening_ipv6: + try: + self.socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + # TODO: keep requested bind_addr separate real bound_addr (port is + # different in case of ephemeral port 0) + self.bind_addr = self.socket.getsockname() + if family in ( + # Windows doesn't have socket.AF_UNIX, so not using it in check + socket.AF_INET, + socket.AF_INET6, + ): + """UNIX domain sockets are strings or bytes. + + In case of bytes with a leading null-byte it's an abstract socket. + """ + self.bind_addr = self.bind_addr[:2] + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + mf = MakeFile + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except errors.NoSSLError: + msg = ('The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.') + buf = ['%s 400 Bad Request\r\n' % self.protocol, + 'Content-Length: %s\r\n' % len(msg), + 'Content-Type: text/plain\r\n\r\n', + msg] + + sock_to_make = s if six.PY3 else s._sock + wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) + try: + wfile.write(''.join(buf).encode('ISO-8859-1')) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return + if not s: + return + mf = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, mf) + + if not isinstance(self.bind_addr, six.string_types): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + try: + self.requests.put(conn) + except queue.Full: + # Just drop the conn. TODO: write 503 back? + conn.close() + return + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error as ex: + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if ex.args[0] in errors.socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See + # https://github.com/cherrypy/cherrypy/issues/707. + return + if ex.args[0] in errors.socket_errors_nonblocking: + # Just try again. See + # https://github.com/cherrypy/cherrypy/issues/479. + return + if ex.args[0] in errors.socket_errors_to_ignore: + # Our socket was closed. + # See https://github.com/cherrypy/cherrypy/issues/686. + return + raise + + @property + def interrupt(self): + """Flag interrupt of the server.""" + return self._interrupt + + @interrupt.setter + def interrupt(self, interrupt): + """Perform the shutdown of this server and save the exception.""" + self._interrupt = True + self.stop() + self._interrupt = interrupt + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, 'socket', None) + if sock: + if not isinstance(self.bind_addr, six.string_types): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + # Changed to use error code and not message + # See + # https://github.com/cherrypy/cherrypy/issues/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See + # https://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, 'close'): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway: + """Base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + """Initialize Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplementedError + + +# These may either be ssl.Adapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter', +} + + +def get_ssl_adapter_class(name='builtin'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, six.string_types): + last_dot = adapter.rfind('.') + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter diff --git a/libraries/cheroot/ssl/__init__.py b/libraries/cheroot/ssl/__init__.py new file mode 100644 index 00000000..ec1a0d90 --- /dev/null +++ b/libraries/cheroot/ssl/__init__.py @@ -0,0 +1,51 @@ +"""Implementation of the SSL adapter base interface.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from abc import ABCMeta, abstractmethod + +from six import add_metaclass + + +@add_metaclass(ABCMeta) +class Adapter: + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> + socket file object`` + """ + + @abstractmethod + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up certificates, private key ciphers and reset context.""" + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + self.ciphers = ciphers + self.context = None + + @abstractmethod + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + @abstractmethod + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + raise NotImplementedError + + @abstractmethod + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + raise NotImplementedError + + @abstractmethod + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + raise NotImplementedError diff --git a/libraries/cheroot/ssl/builtin.py b/libraries/cheroot/ssl/builtin.py new file mode 100644 index 00000000..a19f7eef --- /dev/null +++ b/libraries/cheroot/ssl/builtin.py @@ -0,0 +1,162 @@ +""" +A library for integrating Python's builtin ``ssl`` library with Cheroot. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``HTTPServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +import six + +from . import Adapter +from .. import errors +from ..makefile import MakeFile + + +if six.PY3: + generic_socket_error = OSError +else: + import socket + generic_socket_error = socket.error + del socket + + +def _assert_ssl_exc_contains(exc, *msgs): + """Check whether SSL exception contains either of messages provided.""" + if len(msgs) < 1: + raise TypeError( + '_assert_ssl_exc_contains() requires ' + 'at least one message to be passed.' + ) + err_msg_lower = exc.args[1].lower() + return any(m.lower() in err_msg_lower for m in msgs) + + +class BuiltinSSLAdapter(Adapter): + """A wrapper for integrating Python's builtin ssl module with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """The filename of the certificate chain file.""" + + context = None + """The ssl.SSLContext that will be used to wrap sockets.""" + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up context in addition to base class properties if available.""" + if ssl is None: + raise ImportError('You must install the ssl module to use HTTPS.') + + super(BuiltinSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + self.context = ssl.create_default_context( + purpose=ssl.Purpose.CLIENT_AUTH, + cafile=certificate_chain + ) + self.context.load_cert_chain(certificate, private_key) + if self.ciphers is not None: + self.context.set_ciphers(ciphers) + + def bind(self, sock): + """Wrap and return the given socket.""" + return super(BuiltinSSLAdapter, self).bind(sock) + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + EMPTY_RESULT = None, {} + try: + s = self.context.wrap_socket( + sock, do_handshake_on_connect=True, server_side=True, + ) + except ssl.SSLError as ex: + if ex.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return EMPTY_RESULT + elif ex.errno == ssl.SSL_ERROR_SSL: + if _assert_ssl_exc_contains(ex, 'http request'): + # The client is speaking HTTP to an HTTPS server. + raise errors.NoSSLError + + # Check if it's one of the known errors + # Errors that are caught by PyOpenSSL, but thrown by + # built-in ssl + _block_errors = ( + 'unknown protocol', 'unknown ca', 'unknown_ca', + 'unknown error', + 'https proxy request', 'inappropriate fallback', + 'wrong version number', + 'no shared cipher', 'certificate unknown', + 'ccs received early', + ) + if _assert_ssl_exc_contains(ex, *_block_errors): + # Accepted error, let's pass + return EMPTY_RESULT + elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'): + # This error is thrown by builtin SSL after a timeout + # when client is speaking HTTP to an HTTPS server. + # The connection can safely be dropped. + return EMPTY_RESULT + raise + except generic_socket_error as exc: + """It is unclear why exactly this happens. + + It's reproducible only under Python 2 with openssl>1.0 and stdlib + ``ssl`` wrapper, and only with CherryPy. + So it looks like some healthcheck tries to connect to this socket + during startup (from the same process). + + + Ref: https://github.com/cherrypy/cherrypy/issues/1618 + """ + if six.PY2 and exc.args == (0, 'Error'): + return EMPTY_RESULT + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + 'wsgi.url_scheme': 'https', + 'HTTPS': 'on', + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + """Return socket file object.""" + return MakeFile(sock, mode, bufsize) diff --git a/libraries/cheroot/ssl/pyopenssl.py b/libraries/cheroot/ssl/pyopenssl.py new file mode 100644 index 00000000..2185f851 --- /dev/null +++ b/libraries/cheroot/ssl/pyopenssl.py @@ -0,0 +1,267 @@ +""" +A library for integrating pyOpenSSL with Cheroot. + +The OpenSSL module must be importable for SSL functionality. +You can obtain it from `here <https://launchpad.net/pyopenssl>`_. + +To use this module, set HTTPServer.ssl_adapter to an instance of +ssl.Adapter. There are two ways to use SSL: + +Method One +---------- + + * ``ssl_adapter.context``: an instance of SSL.Context. + +If this is not None, it is assumed to be an SSL.Context instance, +and will be passed to SSL.Connection on bind(). The developer is +responsible for forming a valid Context object. This approach is +to be preferred for more flexibility, e.g. if the cert and key are +streams instead of files, or need decryption, or SSL.SSLv3_METHOD +is desired instead of the default SSL.SSLv23_METHOD, etc. Consult +the pyOpenSSL documentation for complete options. + +Method Two (shortcut) +--------------------- + + * ``ssl_adapter.certificate``: the filename of the server SSL certificate. + * ``ssl_adapter.private_key``: the filename of the server's private key file. + +Both are None by default. If ssl_adapter.context is None, but .private_key +and .certificate are both given and valid, they will be read, and the +context will be automatically created from them. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket +import threading +import time + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +from . import Adapter +from .. import errors, server as cheroot_server +from ..makefile import MakeFile + + +class SSL_fileobject(MakeFile): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + errnum = e.args[0] + if is_reader and errnum in errors.socket_errors_to_ignore: + return '' + raise socket.error(errnum) + except SSL.Error as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise errors.NoSSLError() + + raise errors.FatalSSLAlert(*e.args) + + if time.time() - start > self.ssl_timeout: + raise socket.timeout('timed out') + + def recv(self, size): + """Receive message of a size from the socket.""" + return self._safe_call(True, super(SSL_fileobject, self).recv, size) + + def sendall(self, *args, **kwargs): + """Send whole message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).sendall, + *args, **kwargs) + + def send(self, *args, **kwargs): + """Send some part of message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).send, + *args, **kwargs) + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. + """ + + def __init__(self, *args): + """Initialize SSLConnection instance.""" + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + def shutdown(self, *args): + """Shutdown the SSL connection. + + Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args. + """ + self._lock.acquire() + try: + return self._ssl_conn.shutdown() + finally: + self._lock.release() + + +class pyOpenSSLAdapter(Adapter): + """A wrapper for integrating pyOpenSSL with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """Optional. The filename of CA's intermediate certificate bundle. + + This is needed for cheaper "chained root" SSL certificates, and should be + left as None if not required.""" + + context = None + """An instance of SSL.Context.""" + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Initialize OpenSSL Adapter instance.""" + if SSL is None: + raise ImportError('You must install pyOpenSSL to use HTTPS.') + + super(pyOpenSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + self._environ = None + + def bind(self, sock): + """Wrap and return the given socket.""" + if self.context is None: + self.context = self.get_context() + conn = SSLConnection(self.context, sock) + self._environ = self.get_environ() + return conn + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + return sock, self._environ.copy() + + def get_context(self): + """Return an SSL.Context from self attributes.""" + # See https://code.activestate.com/recipes/442473/ + c = SSL.Context(SSL.SSLv23_METHOD) + c.use_privatekey_file(self.private_key) + if self.certificate_chain: + c.load_verify_locations(self.certificate_chain) + c.use_certificate_file(self.certificate) + return c + + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + ssl_environ = { + 'HTTPS': 'on', + # pyOpenSSL doesn't provide access to any of these AFAICT + # 'SSL_PROTOCOL': 'SSLv2', + # SSL_CIPHER string The cipher specification name + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + + if self.certificate: + # Server certificate attributes + cert = open(self.certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), + # 'SSL_SERVER_V_START': + # Validity of server's certificate (start time), + # 'SSL_SERVER_V_END': + # Validity of server's certificate (end time), + }) + + for prefix, dn in [('I', cert.get_issuer()), + ('S', cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "<X509Name object '/C=US/ST=...'>" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind('=') + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind('/') + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + f = SSL_fileobject(sock, mode, bufsize) + f.ssl_timeout = timeout + return f + else: + return cheroot_server.CP_fileobject(sock, mode, bufsize) diff --git a/libraries/cheroot/test/__init__.py b/libraries/cheroot/test/__init__.py new file mode 100644 index 00000000..e2a7b348 --- /dev/null +++ b/libraries/cheroot/test/__init__.py @@ -0,0 +1 @@ +"""Cheroot test suite.""" diff --git a/libraries/cheroot/test/conftest.py b/libraries/cheroot/test/conftest.py new file mode 100644 index 00000000..9f5f9284 --- /dev/null +++ b/libraries/cheroot/test/conftest.py @@ -0,0 +1,27 @@ +"""Pytest configuration module. + +Contains fixtures, which are tightly bound to the Cheroot framework +itself, useless for end-users' app testing. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ..testing import ( # noqa: F401 + native_server, wsgi_server, +) +from ..testing import get_server_client + + +@pytest.fixture # noqa: F811 +def wsgi_server_client(wsgi_server): + """Create a test client out of given WSGI server.""" + return get_server_client(wsgi_server) + + +@pytest.fixture # noqa: F811 +def native_server_client(native_server): + """Create a test client out of given HTTP server.""" + return get_server_client(native_server) diff --git a/libraries/cheroot/test/helper.py b/libraries/cheroot/test/helper.py new file mode 100644 index 00000000..38f40b26 --- /dev/null +++ b/libraries/cheroot/test/helper.py @@ -0,0 +1,169 @@ +"""A library of helper functions for the Cheroot test suite.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import datetime +import logging +import os +import sys +import time +import threading +import types + +from six.moves import http_client + +import six + +import cheroot.server +import cheroot.wsgi + +from cheroot.test import webtest + +log = logging.getLogger(__name__) +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + + +config = { + 'bind_addr': ('127.0.0.1', 54583), + 'server': 'wsgi', + 'wsgi_app': None, +} + + +class CherootWebCase(webtest.WebCase): + """Helper class for a web app test suite.""" + + script_name = '' + scheme = 'http' + + available_servers = { + 'wsgi': cheroot.wsgi.Server, + 'native': cheroot.server.HTTPServer, + } + + @classmethod + def setup_class(cls): + """Create and run one HTTP server per class.""" + conf = config.copy() + conf.update(getattr(cls, 'config', {})) + + s_class = conf.pop('server', 'wsgi') + server_factory = cls.available_servers.get(s_class) + if server_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + cls.httpserver = server_factory(**conf) + + cls.HOST, cls.PORT = cls.httpserver.bind_addr + if cls.httpserver.ssl_adapter is None: + ssl = '' + cls.scheme = 'http' + else: + ssl = ' (ssl)' + cls.HTTP_CONN = http_client.HTTPSConnection + cls.scheme = 'https' + + v = sys.version.split()[0] + log.info('Python version used to run this test script: %s' % v) + log.info('Cheroot version: %s' % cheroot.__version__) + log.info('HTTP server version: %s%s' % (cls.httpserver.protocol, ssl)) + log.info('PID: %s' % os.getpid()) + + if hasattr(cls, 'setup_server'): + # Clear the wsgi server so that + # it can be updated with the new root + cls.setup_server() + cls.start() + + @classmethod + def teardown_class(cls): + """Cleanup HTTP server.""" + if hasattr(cls, 'setup_server'): + cls.stop() + + @classmethod + def start(cls): + """Load and start the HTTP server.""" + threading.Thread(target=cls.httpserver.safe_start).start() + while not cls.httpserver.ready: + time.sleep(0.1) + + @classmethod + def stop(cls): + """Terminate HTTP server.""" + cls.httpserver.stop() + td = getattr(cls, 'teardown', None) + if td: + td() + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +class Request: + """HTTP request container.""" + + def __init__(self, environ): + """Initialize HTTP request.""" + self.environ = environ + + +class Response: + """HTTP response container.""" + + def __init__(self): + """Initialize HTTP response.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.body = None + + def output(self): + """Generate iterable response body object.""" + if self.body is None: + return [] + elif isinstance(self.body, six.text_type): + return [self.body.encode('iso-8859-1')] + elif isinstance(self.body, six.binary_type): + return [self.body] + else: + return [x.encode('iso-8859-1') for x in self.body] + + +class Controller: + """WSGI app for tests.""" + + def __call__(self, environ, start_response): + """WSGI request handler.""" + req, resp = Request(environ), Response() + try: + # Python 3 supports unicode attribute names + # Python 2 encodes them + handler = self.handlers[environ['PATH_INFO']] + except KeyError: + resp.status = '404 Not Found' + else: + output = handler(req, resp) + if (output is not None and + not any(resp.status.startswith(status_code) + for status_code in ('204', '304'))): + resp.body = output + try: + resp.headers.setdefault('Content-Length', str(len(output))) + except TypeError: + if not isinstance(output, types.GeneratorType): + raise + start_response(resp.status, resp.headers.items()) + return resp.output() diff --git a/libraries/cheroot/test/test.pem b/libraries/cheroot/test/test.pem new file mode 100644 index 00000000..47a47042 --- /dev/null +++ b/libraries/cheroot/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/libraries/cheroot/test/test__compat.py b/libraries/cheroot/test/test__compat.py new file mode 100644 index 00000000..d34e5eb8 --- /dev/null +++ b/libraries/cheroot/test/test__compat.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Test suite for cross-python compatibility helpers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest +import six + +from cheroot._compat import ntob, ntou, bton + + +@pytest.mark.parametrize( + 'func,inp,out', + [ + (ntob, 'bar', b'bar'), + (ntou, 'bar', u'bar'), + (bton, b'bar', 'bar'), + ], +) +def test_compat_functions_positive(func, inp, out): + """Check that compat functions work with correct input.""" + assert func(inp, encoding='utf-8') == out + + +@pytest.mark.parametrize( + 'func', + [ + ntob, + ntou, + ], +) +def test_compat_functions_negative_nonnative(func): + """Check that compat functions fail loudly for incorrect input.""" + non_native_test_str = b'bar' if six.PY3 else u'bar' + with pytest.raises(TypeError): + func(non_native_test_str, encoding='utf-8') + + +@pytest.mark.skip(reason='This test does not work now') +@pytest.mark.skipif( + six.PY3, + reason='This code path only appears in Python 2 version.', +) +def test_ntou_escape(): + """Check that ntou supports escape-encoding under Python 2.""" + expected = u'' + actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape') + assert actual == expected diff --git a/libraries/cheroot/test/test_conn.py b/libraries/cheroot/test/test_conn.py new file mode 100644 index 00000000..f543dd9b --- /dev/null +++ b/libraries/cheroot/test/test_conn.py @@ -0,0 +1,897 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket +import time + +from six.moves import range, http_client, urllib + +import six +import pytest + +from cheroot.test import helper, webtest + + +timeout = 1 +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + + +class Controller(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello, world!' + + def pov(req, resp): + """Render pov value.""" + return pov + + def stream(req, resp): + """Render streaming response.""" + if 'set_cl' in req.environ['QUERY_STRING']: + resp.headers['Content-Length'] = str(10) + + def content(): + for x in range(10): + yield str(x) + + return content() + + def upload(req, resp): + """Process file upload and render thank.""" + if not req.environ['REQUEST_METHOD'] == 'POST': + raise AssertionError("'POST' != request.method %r" % + req.environ['REQUEST_METHOD']) + return "thanks for '%s'" % req.environ['wsgi.input'].read() + + def custom_204(req, resp): + """Render response with status 204.""" + resp.status = '204' + return 'Code = 204' + + def custom_304(req, resp): + """Render response with status 304.""" + resp.status = '304' + return 'Code = 304' + + def err_before_read(req, resp): + """Render response with status 500.""" + resp.status = '500 Internal Server Error' + return 'ok' + + def one_megabyte_of_a(req, resp): + """Render 1MB response.""" + return ['a' * 1024] * 1024 + + def wrong_cl_buffered(req, resp): + """Render buffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return 'I have too many bytes' + + def wrong_cl_unbuffered(req, resp): + """Render unbuffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return ['I too', ' have too many bytes'] + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + if six.PY3: + return string.encode('utf-8').decode('latin-1') + return string + + handlers = { + '/hello': hello, + '/pov': pov, + '/page1': pov, + '/page2': pov, + '/page3': pov, + '/stream': stream, + '/upload': upload, + '/custom/204': custom_204, + '/custom/304': custom_304, + '/err_before_read': err_before_read, + '/one_megabyte_of_a': one_megabyte_of_a, + '/wrong_cl_buffered': wrong_cl_buffered, + '/wrong_cl_unbuffered': wrong_cl_unbuffered, + } + + +@pytest.fixture +def testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + app = Controller() + + def _timeout(req, resp): + return str(wsgi_server.timeout) + app.handlers['/timeout'] = _timeout + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = app + wsgi_server.max_request_body_size = 1001 + wsgi_server.timeout = timeout + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +def header_exists(header_name, headers): + """Check that a header is present.""" + return header_name.lower() in (k.lower() for (k, _) in headers) + + +def header_has_value(header_name, header_value, headers): + """Check that a header with a given value is present.""" + return header_name.lower() in (k.lower() for (k, v) in headers + if v == header_value) + + +def test_HTTP11_persistent_connections(test_client): + """Test persistent HTTP/1.1 connections.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make another request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page1', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test client-side close. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', http_conn=http_connection, + headers=[('Connection', 'close')] + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http_client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ) +) +def test_streaming_11(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.1 protocol.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # 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. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection + ) + assert header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'close', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'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. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection + ) + assert not header_exists('Content-Length', actual_headers) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + chunked_response = False + for k, v in actual_headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + assert not header_has_value('Connection', 'close', actual_headers) + else: + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should + # error. + with pytest.raises(http_client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + # Try HEAD. + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864. + # TODO: figure out how can this be possible on an closed connection + # (chunked_response case) + status_line, actual_headers, actual_resp_body = test_client.head( + '/stream', http_conn=http_connection + ) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'' + assert not header_exists('Transfer-Encoding', actual_headers) + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ) +) +def test_streaming_10(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.0 protocol.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert Keep-Alive. + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + # 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. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert header_exists('Content-Length', actual_headers) + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + else: + # When a Content-Length is not provided, + # the server should close the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert not header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http_client.NotConnected): + test_client.get( + '/pov', http_conn=http_connection, + protocol='HTTP/1.0', + ) + + test_client.server_instance.protocol = original_server_protocol + + +@pytest.mark.parametrize( + 'http_server_protocol', + ( + 'HTTP/1.0', + 'HTTP/1.1', + ) +) +def test_keepalive(test_client, http_server_protocol): + """Test Keep-Alive enabled connections.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = http_server_protocol + + http_client_protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Test a normal HTTP/1.0 request. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test a keep-alive HTTP/1.0 request. + + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', headers=[('Connection', 'Keep-Alive')], + http_conn=http_connection, protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + # Remove the keep-alive header again. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', http_conn=http_connection, + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + test_client.server_instance.protocol = original_server_protocol + + +@pytest.mark.parametrize( + 'timeout_before_headers', + ( + True, + False, + ) +) +def test_HTTP11_Timeout(test_client, timeout_before_headers): + """Check timeout without sending any data. + + The server will close the conn with a 408. + """ + conn = test_client.get_connection() + conn.auto_open = False + conn.connect() + + if not timeout_before_headers: + # Connect but send half the headers only. + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % conn.host).encode('ascii')) + # else: Connect but send nothing. + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 408 + conn.close() + + +def test_HTTP11_Timeout_after_request(test_client): + """Check timeout after at least one request has succeeded. + + The server should close the connection without 408. + """ + fail_msg = "Writing to timed out socket didn't fail as it should have: %s" + + # Make an initial request + conn = test_client.get_connection() + conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = str(timeout).encode() + assert actual_body == expected_body + + # Make a second request on the same socket + conn._output(b'GET /hello HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = b'Hello, world!' + assert actual_body == expected_body + + # 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(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except (socket.error, http_client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Make another request on a new socket, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + + # Make another request on the same socket, + # but timeout on the headers + 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') + try: + response.begin() + except (socket.error, http_client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + conn.close() + + +def test_HTTP11_pipelining(test_client): + """Test HTTP/1.1 pipelining. + + httplib doesn't support this directly. + """ + conn = test_client.get_connection() + + # Put request 1 + conn.putrequest('GET', '/hello', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output( + ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1') + ) + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method='GET') + # there is a bug in python3 regarding the buffering of + # ``conn.sock``. Until that bug get's fixed we will + # monkey patch the ``reponse`` instance. + # https://bugs.python.org/issue23377 + if six.PY3: + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + assert response.status == 200 + assert body == b'Hello, world!' + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + assert response.status == 200 + assert body == b'Hello, world!' + + conn.close() + + +def test_100_Continue(test_client): + """Test 100-continue header processing.""" + conn = test_client.get_connection() + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conn.send(b"d'oh") + response = conn.response_class(conn.sock, method='POST') + version, status, reason = response._read_status() + assert status != 100 + conn.close() + + # Now try a page with an Expect header... + conn.connect() + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.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') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + while True: + line = response.fp.readline().strip() + if line: + pytest.fail( + '100 Continue should not output any headers. Got %r' % + line) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + +@pytest.mark.parametrize( + 'max_request_body_size', + ( + 0, + 1001, + ) +) +def test_readall_or_close(test_client, max_request_body_size): + """Test a max_request_body_size of 0 (the default) and 1001.""" + old_max = test_client.server_instance.max_request_body_size + + test_client.server_instance.max_request_body_size = max_request_body_size + + conn = test_client.get_connection() + + # Get a POST page with an error + conn.putrequest('POST', '/err_before_read', skip_host=True) + conn.putheader('Host', conn.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') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + conn.send(b'x' * 1000) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 500 + + # Now try a working page with an Expect header... + conn._output(b'POST /upload HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('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') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + test_client.server_instance.max_request_body_size = old_max + + +def test_No_Message_Body(test_client): + """Test HTTP queries with an empty response body.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make a 204 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/204', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 204 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + # Make a 304 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/304', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 304 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + +@pytest.mark.xfail( + reason='Server does not correctly read trailers/ending of the previous ' + 'HTTP request, thus the second request fails as the server tries ' + r"to parse b'Content-Type: application/json\r\n' as a " + 'Request-Line. This results in HTTP status code 400, instead of 413' + 'Ref: https://github.com/cherrypy/cheroot/issues/69' +) +def test_Chunked_Encoding(test_client): + """Test HTTP uploads with chunked transfer-encoding.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + # Try a normal chunked request (with extensions) + body = ( + b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' + b'Content-Type: application/json\r\n' + b'\r\n' + ) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.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.endheaders() + conn.send(body) + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode() + assert actual_resp_body == expected_resp_body + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n' + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.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() + conn.send(body) + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + conn.close() + + +def test_Content_Length_in(test_client): + """Try a non-chunked request where Content-Length exceeds limit. + + (server.max_request_body_size). + Assert error before body send. + """ + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '9999') + conn.endheaders() + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + expected_resp_body = ( + b'The entity sent with the request exceeds ' + b'the maximum allowed bytes.' + ) + assert actual_resp_body == expected_resp_body + conn.close() + + +def test_Content_Length_not_int(test_client): + """Test that malicious Content-Length header returns 400.""" + status_line, actual_headers, actual_resp_body = test_client.post( + '/upload', + headers=[ + ('Content-Type', 'text/plain'), + ('Content-Length', 'not-an-integer'), + ], + ) + actual_status = int(status_line[:3]) + + assert actual_status == 400 + assert actual_resp_body == b'Malformed Content-Length Header.' + + +@pytest.mark.parametrize( + 'uri,expected_resp_status,expected_resp_body', + ( + ('/wrong_cl_buffered', 500, + (b'The requested resource returned more bytes than the ' + b'declared Content-Length.')), + ('/wrong_cl_unbuffered', 200, b'I too'), + ) +) +def test_Content_Length_out( + test_client, + uri, expected_resp_status, expected_resp_body +): + """Test response with Content-Length less than the response body. + + (non-chunked response) + """ + conn = test_client.get_connection() + conn.putrequest('GET', uri, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + + assert actual_status == expected_resp_status + assert actual_resp_body == expected_resp_body + + conn.close() + + +@pytest.mark.xfail( + reason='Sometimes this test fails due to low timeout. ' + 'Ref: https://github.com/cherrypy/cherrypy/issues/598' +) +def test_598(test_client): + """Test serving large file with a read timeout in place.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + remote_data_conn = urllib.request.urlopen( + '%s://%s:%s/one_megabyte_of_a' + % ('http', conn.host, conn.port) + ) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + buf += data + remaining -= len(data) + + assert len(buf) == 1024 * 1024 + assert buf == b'a' * 1024 * 1024 + assert remaining == 0 + remote_data_conn.close() + + +@pytest.mark.parametrize( + 'invalid_terminator', + ( + b'\n\n', + b'\r\n\n', + ) +) +def test_No_CRLF(test_client, invalid_terminator): + """Test HTTP queries with no valid CRLF terminators.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + # (b'%s' % b'') is not supported in Python 3.4, so just use + + conn.send(b'GET /hello HTTP/1.1' + invalid_terminator) + response = conn.response_class(conn.sock, method='GET') + response.begin() + actual_resp_body = response.read() + expected_resp_body = b'HTTP requires CRLF terminators' + assert actual_resp_body == expected_resp_body + conn.close() diff --git a/libraries/cheroot/test/test_core.py b/libraries/cheroot/test/test_core.py new file mode 100644 index 00000000..7c91b13e --- /dev/null +++ b/libraries/cheroot/test/test_core.py @@ -0,0 +1,405 @@ +"""Tests for managing HTTP issues (malformed requests, etc).""" +# -*- coding: utf-8 -*- +# vim: set fileencoding=utf-8 : + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import errno +import socket + +import pytest +import six +from six.moves import urllib + +from cheroot.test import helper + + +HTTP_BAD_REQUEST = 400 +HTTP_LENGTH_REQUIRED = 411 +HTTP_NOT_FOUND = 404 +HTTP_OK = 200 +HTTP_VERSION_NOT_SUPPORTED = 505 + + +class HelloController(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello world!' + + def body_required(req, resp): + """Render Hello world or set 411.""" + if req.environ.get('Content-Length', None) is None: + resp.status = '411 Length Required' + return + return 'Hello world!' + + def query_string(req, resp): + """Render QUERY_STRING value.""" + return req.environ.get('QUERY_STRING', '') + + def asterisk(req, resp): + """Render request method value.""" + method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND') + tmpl = 'Got asterisk URI path with {method} method' + return tmpl.format(**locals()) + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + if six.PY3: + return string.encode('utf-8').decode('latin-1') + return string + + handlers = { + '/hello': hello, + '/no_body': hello, + '/body_required': body_required, + '/query_string': query_string, + _munge('/привіт'): hello, + _munge('/Юххууу'): hello, + '/\xa0Ðblah key 0 900 4 data': hello, + '/*': asterisk, + } + + +def _get_http_response(connection, method='GET'): + c = connection + kwargs = {'strict': c.strict} if hasattr(c, 'strict') else {} + # Python 3.2 removed the 'strict' feature, saying: + # "http.client now always assumes HTTP/1.x compliant servers." + return c.response_class(c.sock, method=method, **kwargs) + + +@pytest.fixture +def testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = HelloController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +def test_http_connect_request(test_client): + """Check that CONNECT query results in Method Not Allowed status.""" + status_line = test_client.connect('/anything')[0] + actual_status = int(status_line[:3]) + assert actual_status == 405 + + +def test_normal_request(test_client): + """Check that normal GET query succeeds.""" + status_line, _, actual_resp_body = test_client.get('/hello') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + +def test_query_string_request(test_client): + """Check that GET param is parsed well.""" + status_line, _, actual_resp_body = test_client.get( + '/query_string?test=True' + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'test=True' + + +@pytest.mark.parametrize( + 'uri', + ( + '/hello', # plain + '/query_string?test=True', # query + '/{0}?{1}={2}'.format( # quoted unicode + *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо')) + ), + ) +) +def test_parse_acceptable_uri(test_client, uri): + """Check that server responds with OK to valid GET queries.""" + status_line = test_client.get(uri)[0] + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + + +@pytest.mark.xfail(six.PY2, reason='Fails on Python 2') +def test_parse_uri_unsafe_uri(test_client): + """Test that malicious URI does not allow HTTP injection. + + This effectively checks that sending GET request with URL + + /%A0%D0blah%20key%200%20900%204%20data + + is not converted into + + GET / + blah key 0 900 4 data + HTTP/1.1 + + which would be a security issue otherwise. + """ + c = test_client.get_connection() + resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1') + quoted = urllib.parse.quote(resource) + assert quoted == '/%A0%D0blah%20key%200%20900%204%20data' + request = 'GET {quoted} HTTP/1.1'.format(**locals()) + c._output(request.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_OK + assert response.fp.read(12) == b'Hello world!' + c.close() + + +def test_parse_uri_invalid_uri(test_client): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should only contain US-ASCII. + """ + c = test_client.get_connection() + c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_BAD_REQUEST + assert response.fp.read(21) == b'Malformed Request-URI' + c.close() + + +@pytest.mark.parametrize( + 'uri', + ( + 'hello', # ascii + 'привіт', # non-ascii + ) +) +def test_parse_no_leading_slash_invalid(test_client, uri): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should have leading slash (be absolute). + """ + status_line, _, actual_resp_body = test_client.get( + urllib.parse.quote(uri) + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + assert b'starting with a slash' in actual_resp_body + + +def test_parse_uri_absolute_uri(test_client): + """Check that server responds with Bad Request to Absolute URI. + + Only proxy servers should allow this. + """ + status_line, _, actual_resp_body = test_client.get('http://google.com/') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Absolute URI not allowed if server is not a proxy.' + assert actual_resp_body == expected_body + + +def test_parse_uri_asterisk_uri(test_client): + """Check that server responds with OK to OPTIONS with "*" Absolute URI.""" + status_line, _, actual_resp_body = test_client.options('*') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + expected_body = b'Got asterisk URI path with OPTIONS method' + assert actual_resp_body == expected_body + + +def test_parse_uri_fragment_uri(test_client): + """Check that server responds with Bad Request to URI with fragment.""" + status_line, _, actual_resp_body = test_client.get( + '/hello?test=something#fake', + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Illegal #fragment in Request-URI.' + assert actual_resp_body == expected_body + + +def test_no_content_length(test_client): + """Test POST query with an empty body being successful.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. + c = test_client.get_connection() + c.request('POST', '/no_body') + response = c.getresponse() + actual_resp_body = response.fp.read() + actual_status = response.status + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + +def test_content_length_required(test_client): + """Test POST query with body failing because of missing Content-Length.""" + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + + c = test_client.get_connection() + c.request('POST', '/body_required') + response = c.getresponse() + response.fp.read() + + actual_status = response.status + assert actual_status == HTTP_LENGTH_REQUIRED + + +@pytest.mark.parametrize( + 'request_line,status_code,expected_body', + ( + (b'GET /', # missing proto + HTTP_BAD_REQUEST, b'Malformed Request-Line'), + (b'GET / HTTPS/1.1', # invalid proto + HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol'), + (b'GET / HTTP/2.15', # invalid ver + HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request'), + ) +) +def test_malformed_request_line( + test_client, request_line, + status_code, expected_body +): + """Test missing or invalid HTTP version in Request-Line.""" + c = test_client.get_connection() + c._output(request_line) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == status_code + assert response.fp.read(len(expected_body)) == expected_body + c.close() + + +def test_malformed_http_method(test_client): + """Test non-uppercase HTTP method.""" + c = test_client.get_connection() + c.putrequest('GeT', '/malformed_method_case') + c.putheader('Content-Type', 'text/plain') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(21) + assert actual_resp_body == b'Malformed method name' + + +def test_malformed_header(test_client): + """Check that broken HTTP header results in Bad Request.""" + c = test_client.get_connection() + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941 + c._output(b'Re, 1.2.3.4#015#012') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(20) + assert actual_resp_body == b'Illegal header line.' + + +def test_request_line_split_issue_1220(test_client): + """Check that HTTP request line of exactly 256 chars length is OK.""" + Request_URI = ( + '/hello?' + 'intervenant-entreprise-evenement_classaction=' + 'evenement-mailremerciements' + '&_path=intervenant-entreprise-evenement' + '&intervenant-entreprise-evenement_action-id=19404' + '&intervenant-entreprise-evenement_id=19404' + '&intervenant-entreprise_id=28092' + ) + assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256 + + actual_resp_body = test_client.get(Request_URI)[2] + assert actual_resp_body == b'Hello world!' + + +def test_garbage_in(test_client): + """Test that server sends an error for garbage received over TCP.""" + # Connect without SSL regardless of server.scheme + + c = test_client.get_connection() + c._output(b'gjkgjklsgjklsgjkljklsg') + c._send_output() + response = c.response_class(c.sock, method='GET') + try: + response.begin() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(22) + assert actual_resp_body == b'Malformed Request-Line' + c.close() + except socket.error as ex: + # "Connection reset by peer" is also acceptable. + if ex.errno != errno.ECONNRESET: + raise + + +class CloseController: + """Controller for testing the close callback.""" + + def __call__(self, environ, start_response): + """Get the req to know header sent status.""" + self.req = start_response.__self__.req + resp = CloseResponse(self.close) + start_response(resp.status, resp.headers.items()) + return resp + + def close(self): + """Close, writing hello.""" + self.req.write(b'hello') + + +class CloseResponse: + """Dummy empty response to trigger the no body status.""" + + def __init__(self, close): + """Use some defaults to ensure we have a header.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.close = close + + def __getitem__(self, index): + """Ensure we don't have a body.""" + raise IndexError() + + def output(self): + """Return self to hook the close method.""" + return self + + +@pytest.fixture +def testing_server_close(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = CloseController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +def test_send_header_before_closing(testing_server_close): + """Test we are actually sending the headers before calling 'close'.""" + _, _, resp_body = testing_server_close.server_client.get('/') + assert resp_body == b'hello' diff --git a/libraries/cheroot/test/test_server.py b/libraries/cheroot/test/test_server.py new file mode 100644 index 00000000..c53f7a81 --- /dev/null +++ b/libraries/cheroot/test/test_server.py @@ -0,0 +1,193 @@ +"""Tests for the HTTP server.""" +# -*- coding: utf-8 -*- +# vim: set fileencoding=utf-8 : + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import socket +import tempfile +import threading +import time + +import pytest + +from .._compat import bton +from ..server import Gateway, HTTPServer +from ..testing import ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + EPHEMERAL_PORT, + get_server_client, +) + + +def make_http_server(bind_addr): + """Create and start an HTTP server bound to bind_addr.""" + httpserver = HTTPServer( + bind_addr=bind_addr, + gateway=Gateway, + ) + + threading.Thread(target=httpserver.safe_start).start() + + while not httpserver.ready: + time.sleep(0.1) + + return httpserver + + +non_windows_sock_test = pytest.mark.skipif( + not hasattr(socket, 'AF_UNIX'), + reason='UNIX domain sockets are only available under UNIX-based OS', +) + + +@pytest.fixture +def http_server(): + """Provision a server creator as a fixture.""" + def start_srv(): + bind_addr = yield + httpserver = make_http_server(bind_addr) + yield httpserver + yield httpserver + + srv_creator = iter(start_srv()) + next(srv_creator) + yield srv_creator + try: + while True: + httpserver = next(srv_creator) + if httpserver is not None: + httpserver.stop() + except StopIteration: + pass + + +@pytest.fixture +def unix_sock_file(): + """Check that bound UNIX socket address is stored in server.""" + tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp() + + yield tmp_sock_fname + + os.close(tmp_sock_fh) + os.unlink(tmp_sock_fname) + + +def test_prepare_makes_server_ready(): + """Check that prepare() makes the server ready, and stop() clears it.""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + assert not httpserver.ready + assert not httpserver.requests._threads + + httpserver.prepare() + + assert httpserver.ready + assert httpserver.requests._threads + for thr in httpserver.requests._threads: + assert thr.ready + + httpserver.stop() + + assert not httpserver.requests._threads + assert not httpserver.ready + + +def test_stop_interrupts_serve(): + """Check that stop() interrupts running of serve().""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + httpserver.prepare() + serve_thread = threading.Thread(target=httpserver.serve) + serve_thread.start() + + serve_thread.join(0.5) + assert serve_thread.is_alive() + + httpserver.stop() + + serve_thread.join(0.5) + assert not serve_thread.is_alive() + + +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + ) +) +def test_bind_addr_inet(http_server, ip_addr): + """Check that bound IP address is stored in server.""" + httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) + + assert httpserver.bind_addr[0] == ip_addr + assert httpserver.bind_addr[1] != EPHEMERAL_PORT + + +@non_windows_sock_test +def test_bind_addr_unix(http_server, unix_sock_file): + """Check that bound UNIX socket address is stored in server.""" + httpserver = http_server.send(unix_sock_file) + + assert httpserver.bind_addr == unix_sock_file + + +@pytest.mark.skip(reason="Abstract sockets don't work currently") +@non_windows_sock_test +def test_bind_addr_unix_abstract(http_server): + """Check that bound UNIX socket address is stored in server.""" + unix_abstract_sock = b'\x00cheroot/test/socket/here.sock' + httpserver = http_server.send(unix_abstract_sock) + + assert httpserver.bind_addr == unix_abstract_sock + + +PEERCRED_IDS_URI = '/peer_creds/ids' +PEERCRED_TEXTS_URI = '/peer_creds/texts' + + +class _TestGateway(Gateway): + def respond(self): + req = self.req + conn = req.conn + req_uri = bton(req.uri) + if req_uri == PEERCRED_IDS_URI: + peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid + return ['|'.join(map(str, peer_creds))] + elif req_uri == PEERCRED_TEXTS_URI: + return ['!'.join((conn.peer_user, conn.peer_group))] + return super(_TestGateway, self).respond() + + +@pytest.mark.skip( + reason='Test HTTP client is not able to work through UNIX socket currently' +) +@non_windows_sock_test +def test_peercreds_unix_sock(http_server, unix_sock_file): + """Check that peercred lookup and resolution work when enabled.""" + httpserver = http_server.send(unix_sock_file) + httpserver.gateway = _TestGateway + httpserver.peercreds_enabled = True + + testclient = get_server_client(httpserver) + + expected_peercreds = os.getpid(), os.getuid(), os.getgid() + expected_peercreds = '|'.join(map(str, expected_peercreds)) + assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds + assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI) + + httpserver.peercreds_resolve_enabled = True + import grp + expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name + expected_textcreds = '!'.join(map(str, expected_textcreds)) + assert testclient.get(PEERCRED_TEXTS_URI) == expected_textcreds diff --git a/libraries/cheroot/test/webtest.py b/libraries/cheroot/test/webtest.py new file mode 100644 index 00000000..43448f5b --- /dev/null +++ b/libraries/cheroot/test/webtest.py @@ -0,0 +1,581 @@ +"""Extensions to unittest for web frameworks. + +Use the WebCase.getPage method to request a page from your HTTP server. +Framework Integration +===================== +If you have control over your server process, you can handle errors +in the server-side of the HTTP conversation a bit better. You must run +both the client (your WebCase tests) and the server in the same process +(but in separate threads, obviously). +When an error occurs in the framework, call server_error. It will print +the traceback to stdout, and keep any assertions you have from running +(the assumption is that, if the server errors, the page output will not +be of further significance to your tests). +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pprint +import re +import socket +import sys +import time +import traceback +import os +import json +import unittest +import warnings + +from six.moves import range, http_client, map, urllib_parse +import six + +from more_itertools.more import always_iterable + + +def interface(host): + """Return an IP address for a client connection given the server host. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + if host == '0.0.0.0': + # INADDR_ANY, which should respond on localhost. + return '127.0.0.1' + if host == '::': + # IN6ADDR_ANY, which should respond on localhost. + return '::1' + return host + + +try: + # Jython support + if sys.platform[:4] == 'java': + def getchar(): + """Get a key press.""" + # Hopefully this is enough + return sys.stdin.read(1) + else: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + """Get a key press.""" + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + """Get a key press.""" + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +# from jaraco.properties +class NonDataProperty: + """Non-data property decorator.""" + + def __init__(self, fget): + """Initialize a non-data property.""" + assert fget is not None, 'fget cannot be none' + assert callable(fget), 'fget must be callable' + self.fget = fget + + def __get__(self, obj, objtype=None): + """Return a class property.""" + if obj is None: + return self + return self.fget(obj) + + +class WebCase(unittest.TestCase): + """Helper web test suite base.""" + + HOST = '127.0.0.1' + PORT = 8000 + HTTP_CONN = http_client.HTTPConnection + PROTOCOL = 'HTTP/1.1' + + scheme = 'http' + url = None + + status = None + headers = None + body = None + + encoding = 'utf-8' + + time = None + + @property + def _Conn(self): + """Return HTTPConnection or HTTPSConnection based on self.scheme. + + * from http.client. + """ + cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper()) + return getattr(http_client, cls_name) + + def get_conn(self, auto_open=False): + """Return a connection to our HTTP server.""" + conn = self._Conn(self.interface(), self.PORT) + # Automatically re-connect? + conn.auto_open = auto_open + conn.connect() + return conn + + def set_persistent(self, on=True, auto_open=False): + """Make our HTTP_CONN persistent (or not). + + If the 'on' argument is True (the default), then self.HTTP_CONN + will be set to an instance of HTTP(S)?Connection + to persist across requests. + As this class only allows for a single open connection, if + self already has an open connection, it will be closed. + """ + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + self.HTTP_CONN = ( + self.get_conn(auto_open=auto_open) + if on + else self._Conn + ) + + @property + def persistent(self): # noqa: D401; irrelevant for properties + """Presense of the persistent HTTP connection.""" + return hasattr(self.HTTP_CONN, '__class__') + + @persistent.setter + def persistent(self, on): + self.set_persistent(on) + + def interface(self): + """Return an IP address for a client connection. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + return interface(self.HOST) + + def getPage(self, url, headers=None, method='GET', body=None, + protocol=None, raise_subcls=None): + """Open the url with debugging support. Return status, headers, body. + + url should be the identifier passed to the server, typically a + server-absolute path and query string (sent between method and + protocol), and should only be an absolute URI if proxy support is + enabled in the server. + + If the application under test generates absolute URIs, be sure + to wrap them first with strip_netloc:: + + class MyAppWebCase(WebCase): + def getPage(url, *args, **kwargs): + super(MyAppWebCase, self).getPage( + cheroot.test.webtest.strip_netloc(url), + *args, **kwargs + ) + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + ServerError.on = False + + if isinstance(url, six.text_type): + url = url.encode('utf-8') + if isinstance(body, six.text_type): + body = body.encode('utf-8') + + self.url = url + self.time = None + start = time.time() + result = openURL(url, headers, method, body, self.HOST, self.PORT, + self.HTTP_CONN, protocol or self.PROTOCOL, + raise_subcls) + self.time = time.time() - start + self.status, self.headers, self.body = result + + # Build a list of request cookies from the previous response cookies. + self.cookies = [('Cookie', v) for k, v in self.headers + if k.lower() == 'set-cookie'] + + if ServerError.on: + raise ServerError() + return result + + @NonDataProperty + def interactive(self): + """Determine whether tests are run in interactive mode. + + Load interactivity setting from environment, where + the value can be numeric or a string like true or + False or 1 or 0. + """ + env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True') + is_interactive = bool(json.loads(env_str.lower())) + if is_interactive: + warnings.warn( + 'Interactive test failure interceptor support via ' + 'WEBTEST_INTERACTIVE environment variable is deprecated.', + DeprecationWarning + ) + return is_interactive + + console_height = 30 + + def _handlewebError(self, msg): + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = (' Show: ' + '[B]ody [H]eaders [S]tatus [U]RL; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ') + sys.stdout.write(p) + sys.stdout.flush() + while True: + i = getchar().upper() + if not isinstance(i, type('')): + i = i.decode('ascii') + if i not in 'BHSUIRX': + continue + print(i.upper()) # Also prints new line + if i == 'B': + for x, line in enumerate(self.body.splitlines()): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r') + if m == 'q': + break + print(line) + elif i == 'H': + pprint.pprint(self.headers) + elif i == 'S': + print(self.status) + elif i == 'U': + print(self.url) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise self.failureException(msg) + elif i == 'X': + sys.exit() + sys.stdout.write(p) + sys.stdout.flush() + + @property + def status_code(self): # noqa: D401; irrelevant for properties + """Integer HTTP status code.""" + return int(self.status[:3]) + + def status_matches(self, expected): + """Check whether actual status matches expected.""" + actual = ( + self.status_code + if isinstance(expected, int) else + self.status + ) + return expected == actual + + def assertStatus(self, status, msg=None): + """Fail if self.status != status. + + status may be integer code, exact string status, or + iterable of allowed possibilities. + """ + if any(map(self.status_matches, always_iterable(status))): + return + + tmpl = 'Status {self.status} does not match {status}' + msg = msg or tmpl.format(**locals()) + self._handlewebError(msg) + + def assertHeader(self, key, value=None, msg=None): + """Fail if (key, [value]) not in self.headers.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + if value is None or str(value) == v: + return v + + if msg is None: + if value is None: + msg = '%r not in headers' % key + else: + msg = '%r:%r not in headers' % (key, value) + self._handlewebError(msg) + + def assertHeaderIn(self, key, values, msg=None): + """Fail if header indicated by key doesn't have one of the values.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + matches = [value for value in values if str(value) == v] + if matches: + return matches + + if msg is None: + msg = '%(key)r not in %(values)r' % vars() + self._handlewebError(msg) + + def assertHeaderItemValue(self, key, value, msg=None): + """Fail if the header does not contain the specified value.""" + actual_value = self.assertHeader(key, msg=msg) + header_values = map(str.strip, actual_value.split(',')) + if value in header_values: + return value + + if msg is None: + msg = '%r not in %r' % (value, header_values) + self._handlewebError(msg) + + def assertNoHeader(self, key, msg=None): + """Fail if key in self.headers.""" + lowkey = key.lower() + matches = [k for k, v in self.headers if k.lower() == lowkey] + if matches: + if msg is None: + msg = '%r in headers' % key + self._handlewebError(msg) + + def assertNoHeaderItemValue(self, key, value, msg=None): + """Fail if the header contains the specified value.""" + lowkey = key.lower() + hdrs = self.headers + matches = [k for k, v in hdrs if k.lower() == lowkey and v == value] + if matches: + if msg is None: + msg = '%r:%r in %r' % (key, value, hdrs) + self._handlewebError(msg) + + def assertBody(self, value, msg=None): + """Fail if value != self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value != self.body: + if msg is None: + msg = 'expected body:\n%r\n\nactual body:\n%r' % ( + value, self.body) + self._handlewebError(msg) + + def assertInBody(self, value, msg=None): + """Fail if value not in self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value not in self.body: + if msg is None: + msg = '%r not in body: %s' % (value, self.body) + self._handlewebError(msg) + + def assertNotInBody(self, value, msg=None): + """Fail if value in self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value in self.body: + if msg is None: + msg = '%r found in body' % value + self._handlewebError(msg) + + def assertMatchesBody(self, pattern, msg=None, flags=0): + """Fail if value (a regex pattern) is not in self.body.""" + if isinstance(pattern, six.text_type): + pattern = pattern.encode(self.encoding) + if re.search(pattern, self.body, flags) is None: + if msg is None: + msg = 'No match for %r in body' % pattern + self._handlewebError(msg) + + +methods_with_bodies = ('POST', 'PUT', 'PATCH') + + +def cleanHeaders(headers, method, body, host, port): + """Return request headers, with required headers added (if missing).""" + if headers is None: + headers = [] + + # Add the required Host request header if not present. + # [This specifies the host:port of the server, not the client.] + found = False + for k, v in headers: + if k.lower() == 'host': + found = True + break + if not found: + if port == 80: + headers.append(('Host', host)) + else: + headers.append(('Host', '%s:%s' % (host, port))) + + if method in methods_with_bodies: + # Stick in default type and length headers if not present + found = False + for k, v in headers: + if k.lower() == 'content-type': + found = True + break + if not found: + headers.append( + ('Content-Type', 'application/x-www-form-urlencoded')) + headers.append(('Content-Length', str(len(body or '')))) + + return headers + + +def shb(response): + """Return status, headers, body the way we like from a response.""" + if six.PY3: + h = response.getheaders() + else: + h = [] + key, value = None, None + for line in response.msg.headers: + if line: + if line[0] in ' \t': + value += line.strip() + else: + if key and value: + h.append((key, value)) + key, value = line.split(':', 1) + key = key.strip() + value = value.strip() + if key and value: + h.append((key, value)) + + return '%s %s' % (response.status, response.reason), h, response.read() + + +def openURL(url, headers=None, method='GET', body=None, + host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection, + protocol='HTTP/1.1', raise_subcls=None): + """ + Open the given HTTP resource and return status, headers, and body. + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + headers = cleanHeaders(headers, method, body, host, port) + + # Trying 10 times is simply in case of socket errors. + # Normal case--it should run once. + for trial in range(10): + try: + # Allow http_conn to be a class or an instance + if hasattr(http_conn, 'host'): + conn = http_conn + else: + conn = http_conn(interface(host), port) + + conn._http_vsn_str = protocol + conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()])) + + if six.PY3 and isinstance(url, bytes): + url = url.decode() + conn.putrequest(method.upper(), url, skip_host=True, + skip_accept_encoding=True) + + for key, value in headers: + conn.putheader(key, value.encode('Latin-1')) + conn.endheaders() + + if body is not None: + conn.send(body) + + # Handle response + response = conn.getresponse() + + s, h, b = shb(response) + + if not hasattr(http_conn, 'host'): + # We made our own conn instance. Close it. + conn.close() + + return s, h, b + except socket.error as e: + if raise_subcls is not None and isinstance(e, raise_subcls): + raise + else: + time.sleep(0.5) + if trial == 9: + raise + + +def strip_netloc(url): + """Return absolute-URI path from URL. + + Strip the scheme and host from the URL, returning the + server-absolute portion. + + Useful for wrapping an absolute-URI for which only the + path is expected (such as in calls to getPage). + + >>> strip_netloc('https://google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('//google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('/foo/bar?bing#baz') + '/foo/bar?bing' + """ + parsed = urllib_parse.urlparse(url) + scheme, netloc, path, params, query, fragment = parsed + stripped = '', '', path, params, query, '' + return urllib_parse.urlunparse(stripped) + + +# Add any exceptions which your web framework handles +# normally (that you don't want server_error to trap). +ignored_exceptions = [] + +# You'll want set this to True when you can't guarantee +# that each response will immediately follow each request; +# for example, when handling requests via multiple threads. +ignore_all = False + + +class ServerError(Exception): + """Exception for signalling server error.""" + + on = False + + +def server_error(exc=None): + """Server debug hook. + + Return True if exception handled, False if ignored. + You probably want to wrap this, so you can still handle an error using + your framework when it's ignored. + """ + if exc is None: + exc = sys.exc_info() + + if ignore_all or exc[0] in ignored_exceptions: + return False + else: + ServerError.on = True + print('') + print(''.join(traceback.format_exception(*exc))) + return True diff --git a/libraries/cheroot/testing.py b/libraries/cheroot/testing.py new file mode 100644 index 00000000..f01d0aa1 --- /dev/null +++ b/libraries/cheroot/testing.py @@ -0,0 +1,144 @@ +"""Pytest fixtures and other helpers for doing testing by end-users.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from contextlib import closing +import errno +import socket +import threading +import time + +import pytest +from six.moves import http_client + +import cheroot.server +from cheroot.test import webtest +import cheroot.wsgi + +EPHEMERAL_PORT = 0 +NO_INTERFACE = None # Using this or '' will cause an exception +ANY_INTERFACE_IPV4 = '0.0.0.0' +ANY_INTERFACE_IPV6 = '::' + +config = { + cheroot.wsgi.Server: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'wsgi_app': None, + }, + cheroot.server.HTTPServer: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'gateway': cheroot.server.Gateway, + }, +} + + +def cheroot_server(server_factory): + """Set up and tear down a Cheroot server instance.""" + conf = config[server_factory].copy() + bind_port = conf.pop('bind_addr')[-1] + + for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4: + try: + actual_bind_addr = (interface, bind_port) + httpserver = server_factory( # create it + bind_addr=actual_bind_addr, + **conf + ) + except OSError: + pass + else: + break + + threading.Thread(target=httpserver.safe_start).start() # spawn it + while not httpserver.ready: # wait until fully initialized and bound + time.sleep(0.1) + + yield httpserver + + httpserver.stop() # destroy it + + +@pytest.fixture(scope='module') +def wsgi_server(): + """Set up and tear down a Cheroot WSGI server instance.""" + for srv in cheroot_server(cheroot.wsgi.Server): + yield srv + + +@pytest.fixture(scope='module') +def native_server(): + """Set up and tear down a Cheroot HTTP server instance.""" + for srv in cheroot_server(cheroot.server.HTTPServer): + yield srv + + +class _TestClient: + def __init__(self, server): + self._interface, self._host, self._port = _get_conn_data(server) + self._http_connection = self.get_connection() + self.server_instance = server + + def get_connection(self): + name = '{interface}:{port}'.format( + interface=self._interface, + port=self._port, + ) + return http_client.HTTPConnection(name) + + def request( + self, uri, method='GET', headers=None, http_conn=None, + protocol='HTTP/1.1', + ): + return webtest.openURL( + uri, method=method, + headers=headers, + host=self._host, port=self._port, + http_conn=http_conn or self._http_connection, + protocol=protocol, + ) + + def __getattr__(self, attr_name): + def _wrapper(uri, **kwargs): + http_method = attr_name.upper() + return self.request(uri, method=http_method, **kwargs) + + return _wrapper + + +def _probe_ipv6_sock(interface): + # Alternate way is to check IPs on interfaces using glibc, like: + # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py + try: + with closing(socket.socket(family=socket.AF_INET6)) as sock: + sock.bind((interface, 0)) + except (OSError, socket.error) as sock_err: + # In Python 3 socket.error is an alias for OSError + # In Python 2 socket.error is a subclass of IOError + if sock_err.errno != errno.EADDRNOTAVAIL: + raise + else: + return True + + return False + + +def _get_conn_data(server): + if isinstance(server.bind_addr, tuple): + host, port = server.bind_addr + else: + host, port = server.bind_addr, 0 + + interface = webtest.interface(host) + + if ':' in interface and not _probe_ipv6_sock(interface): + interface = '127.0.0.1' + if ':' in host: + host = interface + + return interface, host, port + + +def get_server_client(server): + """Create and return a test client for the given server.""" + return _TestClient(server) diff --git a/libraries/cheroot/workers/__init__.py b/libraries/cheroot/workers/__init__.py new file mode 100644 index 00000000..098b8f25 --- /dev/null +++ b/libraries/cheroot/workers/__init__.py @@ -0,0 +1 @@ +"""HTTP workers pool.""" diff --git a/libraries/cheroot/workers/threadpool.py b/libraries/cheroot/workers/threadpool.py new file mode 100644 index 00000000..ff8fbcee --- /dev/null +++ b/libraries/cheroot/workers/threadpool.py @@ -0,0 +1,271 @@ +"""A thread-based worker pool.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import threading +import time +import socket + +from six.moves import queue + + +__all__ = ('WorkerThread', 'ThreadPool') + + +class TrueyZero: + """Object which equals and does math like the integer 0 but evals True.""" + + def __add__(self, other): + return other + + def __radd__(self, other): + return other + + +trueyzero = TrueyZero() + +_SHUTDOWNREQUEST = None + + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + def __init__(self, server): + """Initialize WorkerThread instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + """ + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ( + (self.start_time is None) and + trueyzero or + self.conn.requests_seen + ), + 'Bytes Read': lambda s: self.bytes_read + ( + (self.start_time is None) and + trueyzero or + self.conn.rfile.bytes_read + ), + 'Bytes Written': lambda s: self.bytes_written + ( + (self.start_time is None) and + trueyzero or + self.conn.wfile.bytes_written + ), + 'Work Time': lambda s: self.work_time + ( + (self.start_time is None) and + trueyzero or + time.time() - self.start_time + ), + 'Read Throughput': lambda s: s['Bytes Read'](s) / ( + s['Work Time'](s) or 1e-6), + 'Write Throughput': lambda s: s['Bytes Written'](s) / ( + s['Work Time'](s) or 1e-6), + } + threading.Thread.__init__(self) + + def run(self): + """Process incoming HTTP connections. + + Retrieves incoming connections from thread pool. + """ + self.server.stats['Worker Threads'][self.getName()] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + if self.server.stats['Enabled']: + self.start_time = time.time() + try: + conn.communicate() + finally: + conn.close() + if self.server.stats['Enabled']: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit) as ex: + self.server.interrupt = ex + + +class ThreadPool: + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__( + self, server, min=10, max=-1, accepted_queue_size=-1, + accepted_queue_timeout=10): + """Initialize HTTP requests queue instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + min (int): minimum number of worker threads + max (int): maximum number of worker threads + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue(maxsize=accepted_queue_size) + self._queue_put_timeout = accepted_queue_timeout + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName('CP Server ' + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + @property + def idle(self): # noqa: D401; irrelevant for properties + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + + def put(self, obj): + """Put request into queue. + + Args: + obj (cheroot.server.HTTPConnection): HTTP connection + waiting to be processed + """ + self._queue.put(obj, block=True, timeout=self._queue_put_timeout) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + if self.max > 0: + budget = max(self.max - len(self._threads), 0) + else: + # self.max <= 0 indicates no maximum + budget = float('inf') + + n_new = min(amount, budget) + + workers = [self._spawn_worker() for i in range(n_new)] + while not all(worker.ready for worker in workers): + time.sleep(.1) + self._threads.extend(workers) + + def _spawn_worker(self): + worker = WorkerThread(self.server) + worker.setName('CP Server ' + worker.getName()) + worker.start() + return worker + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + # calculate the number of threads above the minimum + n_extra = max(len(self._threads) - self.min, 0) + + # don't remove more than amount + n_to_remove = min(amount, n_extra) + + # put shutdown requests on the queue equal to the number of threads + # to remove. As each request is processed by a worker, that worker + # will terminate and be culled from the list. + for n in range(n_to_remove): + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + """Terminate all worker threads. + + Args: + timeout (int): time to wait for threads to stop gracefully + """ + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout is not None and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See + # https://github.com/cherrypy/cherrypy/issues/691. + KeyboardInterrupt): + pass + + @property + def qsize(self): + """Return the queue size.""" + return self._queue.qsize() diff --git a/libraries/cheroot/wsgi.py b/libraries/cheroot/wsgi.py new file mode 100644 index 00000000..a04c9438 --- /dev/null +++ b/libraries/cheroot/wsgi.py @@ -0,0 +1,423 @@ +"""This class holds Cheroot WSGI server implementation. + +Simplest example on how to use this server:: + + from cheroot import wsgi + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return [b'Hello world!'] + + addr = '0.0.0.0', 8070 + server = wsgi.Server(addr, my_crazy_app) + server.start() + +The Cheroot WSGI server can serve as many WSGI applications +as you want in one instance by using a PathInfoDispatcher:: + + path_map = { + '/': my_crazy_app, + '/blog': my_blog_app, + } + d = wsgi.PathInfoDispatcher(path_map) + server = wsgi.Server(addr, d) +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import sys + +import six +from six.moves import filter + +from . import server +from .workers import threadpool +from ._compat import ntob, bton + + +class Server(server.HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__( + self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5, + accepted_queue_size=-1, accepted_queue_timeout=10, + peercreds_enabled=False, peercreds_resolve_enabled=False, + ): + """Initialize WSGI Server instance. + + Args: + bind_addr (tuple): network interface to listen to + wsgi_app (callable): WSGI application callable + numthreads (int): number of threads for WSGI thread pool + server_name (str): web server name to be advertised via + Server HTTP header + max (int): maximum number of worker threads + request_queue_size (int): the 'backlog' arg to + socket.listen(); max queued connections + timeout (int): the timeout in seconds for accepted connections + shutdown_timeout (int): the total time, in seconds, to + wait for worker threads to cleanly exit + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + super(Server, self).__init__( + bind_addr, + gateway=wsgi_gateways[self.wsgi_version], + server_name=server_name, + peercreds_enabled=peercreds_enabled, + peercreds_resolve_enabled=peercreds_resolve_enabled, + ) + self.wsgi_app = wsgi_app + self.request_queue_size = request_queue_size + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.requests = threadpool.ThreadPool( + self, min=numthreads or 1, max=max, + accepted_queue_size=accepted_queue_size, + accepted_queue_timeout=accepted_queue_timeout) + + @property + def numthreads(self): + """Set minimum number of threads.""" + return self.requests.min + + @numthreads.setter + def numthreads(self, value): + self.requests.min = value + + +class Gateway(server.Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + """Initialize WSGI Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + super(Gateway, self).__init__(req) + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + @classmethod + def gateway_map(cls): + """Create a mapping of gateways and their versions. + + Returns: + dict[tuple[int,int],class]: map of gateway version and + corresponding class + + """ + return dict( + (gw.version, gw) + for gw in cls.__subclasses__() + ) + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + raise NotImplementedError + + def respond(self): + """Process the current request. + + From :pep:`333`: + + The start_response callable must not actually transmit + the response headers. Instead, it must store them for the + server or gateway to transmit only after the first + iteration of the application return value that yields + a NON-EMPTY string, or upon the application's first + invocation of the write() callable. + """ + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in filter(None, response): + if not isinstance(chunk, six.binary_type): + raise ValueError('WSGI Applications must yield bytes') + self.write(chunk) + finally: + # Send headers if not already sent + self.req.ensure_headers_sent() + if hasattr(response, 'close'): + response.close() + + def start_response(self, status, headers, exc_info=None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError('WSGI start_response called a second ' + 'time with no exc_info.') + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + six.reraise(*exc_info) + finally: + exc_info = None + + self.req.status = self._encode_status(status) + + for k, v in headers: + if not isinstance(k, str): + raise TypeError( + 'WSGI response header key %r is not of type str.' % k) + if not isinstance(v, str): + raise TypeError( + 'WSGI response header value %r is not of type str.' % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + out_header = ntob(k), ntob(v) + self.req.outheaders.append(out_header) + + return self.write + + @staticmethod + def _encode_status(status): + """Cast status to bytes representation of current Python version. + + According to :pep:`3333`, when using Python 3, the response status + and headers must be bytes masquerading as unicode; that is, they + must be of type "str" but are restricted to code points in the + "latin-1" set. + """ + if six.PY2: + return status + if not isinstance(status, str): + raise TypeError('WSGI response status is not of type str.') + return status.encode('ISO-8859-1') + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError('WSGI write called before start_response.') + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response( + '500 Internal Server Error', + 'The requested resource returned more bytes than the ' + 'declared Content-Length.') + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + self.req.ensure_headers_sent() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + 'Response body exceeds the declared Content-Length.') + + +class Gateway_10(Gateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + version = 1, 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + req_conn = req.conn + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': bton(req.path), + 'QUERY_STRING': bton(req.qs), + 'REMOTE_ADDR': req_conn.remote_addr or '', + 'REMOTE_PORT': str(req_conn.remote_port or ''), + 'REQUEST_METHOD': bton(req.method), + 'REQUEST_URI': bton(req.uri), + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': bton(req.request_protocol), + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.input_terminated': bool(req.chunked_read), + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': bton(req.scheme), + 'wsgi.version': self.version, + } + + if isinstance(req.server.bind_addr, six.string_types): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env['SERVER_PORT'] = '' + try: + env['X_REMOTE_PID'] = str(req_conn.peer_pid) + env['X_REMOTE_UID'] = str(req_conn.peer_uid) + env['X_REMOTE_GID'] = str(req_conn.peer_gid) + + env['X_REMOTE_USER'] = str(req_conn.peer_user) + env['X_REMOTE_GROUP'] = str(req_conn.peer_group) + + env['REMOTE_USER'] = env['X_REMOTE_USER'] + except RuntimeError: + """Unable to retrieve peer creds data. + + Unsupported by current kernel or socket error happened, or + unsupported socket type, or disabled. + """ + else: + env['SERVER_PORT'] = str(req.server.bind_addr[1]) + + # Request headers + env.update( + ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v)) + for k, v in req.inheaders.items() + ) + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop('HTTP_CONTENT_TYPE', None) + if ct is not None: + env['CONTENT_TYPE'] = ct + cl = env.pop('HTTP_CONTENT_LENGTH', None) + if cl is not None: + env['CONTENT_LENGTH'] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class Gateway_u0(Gateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys + and values in both Python 2 and Python 3. + """ + + version = 'u', 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + env_10 = super(Gateway_u0, self).get_environ() + env = dict(map(self._decode_key, env_10.items())) + + # Request-URI + enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8')) + try: + env['PATH_INFO'] = req.path.decode(enc) + env['QUERY_STRING'] = req.qs.decode(enc) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env['wsgi.url_encoding'] = 'ISO-8859-1' + env['PATH_INFO'] = env_10['PATH_INFO'] + env['QUERY_STRING'] = env_10['QUERY_STRING'] + + env.update(map(self._decode_value, env.items())) + + return env + + @staticmethod + def _decode_key(item): + k, v = item + if six.PY2: + k = k.decode('ISO-8859-1') + return k, v + + @staticmethod + def _decode_value(item): + k, v = item + skip_keys = 'REQUEST_URI', 'wsgi.input' + if six.PY3 or not isinstance(v, bytes) or k in skip_keys: + return k, v + return k, v.decode('ISO-8859-1') + + +wsgi_gateways = Gateway.gateway_map() + + +class PathInfoDispatcher: + """A WSGI dispatcher for dispatch based on the PATH_INFO.""" + + def __init__(self, apps): + """Initialize path info WSGI app dispatcher. + + Args: + apps (dict[str,object]|list[tuple[str,object]]): URI prefix + and WSGI app pairs + """ + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + def by_path_len(app): + return len(app[0]) + apps.sort(key=by_path_len, reverse=True) + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip('/'), a) for p, a in apps] + + def __call__(self, environ, start_response): + """Process incoming WSGI request. + + Ref: :pep:`3333` + + Args: + environ (Mapping): a dict containing WSGI environment variables + start_response (callable): function, which sets response + status and headers + + Returns: + list[bytes]: iterable containing bytes to be returned in + HTTP response body + + """ + path = environ['PATH_INFO'] or '/' + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + '/') or path == p: + environ = environ.copy() + environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p + environ['PATH_INFO'] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + + +# compatibility aliases +globals().update( + WSGIServer=Server, + WSGIGateway=Gateway, + WSGIGateway_u0=Gateway_u0, + WSGIGateway_10=Gateway_10, + WSGIPathInfoDispatcher=PathInfoDispatcher, +) diff --git a/libraries/cherrypy/__init__.py b/libraries/cherrypy/__init__.py new file mode 100644 index 00000000..c5925980 --- /dev/null +++ b/libraries/cherrypy/__init__.py @@ -0,0 +1,362 @@ +"""CherryPy is a pythonic, object-oriented HTTP framework. + +CherryPy consists of not one, but four separate API layers. + +The APPLICATION LAYER is the simplest. CherryPy applications are written as +a tree of classes and methods, where each branch in the tree corresponds to +a branch in the URL path. Each method is a 'page handler', which receives +GET and POST params as keyword arguments, and returns or yields the (HTML) +body of the response. The special method name 'index' is used for paths +that end in a slash, and the special method name 'default' is used to +handle multiple paths via a single handler. This layer also includes: + + * the 'exposed' attribute (and cherrypy.expose) + * cherrypy.quickstart() + * _cp_config attributes + * cherrypy.tools (including cherrypy.session) + * cherrypy.url() + +The ENVIRONMENT LAYER is used by developers at all levels. It provides +information about the current request and response, plus the application +and server environment, via a (default) set of top-level objects: + + * cherrypy.request + * cherrypy.response + * cherrypy.engine + * cherrypy.server + * cherrypy.tree + * cherrypy.config + * cherrypy.thread_data + * cherrypy.log + * cherrypy.HTTPError, NotFound, and HTTPRedirect + * cherrypy.lib + +The EXTENSION LAYER allows advanced users to construct and share their own +plugins. It consists of: + + * Hook API + * Tool API + * Toolbox API + * Dispatch API + * Config Namespace API + +Finally, there is the CORE LAYER, which uses the core API's to construct +the default components which are available at higher layers. You can think +of the default components as the 'reference implementation' for CherryPy. +Megaframeworks (and advanced users) may replace the default components +with customized or extended components. The core API's are: + + * Application API + * Engine API + * Request API + * Server API + * WSGI API + +These API's are described in the `CherryPy specification +<https://github.com/cherrypy/cherrypy/wiki/CherryPySpec>`_. +""" + +from threading import local as _local + +from ._cperror import ( + HTTPError, HTTPRedirect, InternalRedirect, + NotFound, CherryPyException, +) + +from . import _cpdispatch as dispatch + +from ._cptools import default_toolbox as tools, Tool +from ._helper import expose, popargs, url + +from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig + +import cherrypy.lib.httputil as _httputil + +from ._cptree import Application +from . import _cpwsgi as wsgi + +from . import process +try: + from .process import win32 + engine = win32.Win32Bus() + engine.console_control_handler = win32.ConsoleCtrlHandler(engine) + del win32 +except ImportError: + engine = process.bus + +from . import _cpchecker + +__all__ = ( + 'HTTPError', 'HTTPRedirect', 'InternalRedirect', + 'NotFound', 'CherryPyException', + 'dispatch', 'tools', 'Tool', 'Application', + 'wsgi', 'process', 'tree', 'engine', + 'quickstart', 'serving', 'request', 'response', 'thread_data', + 'log', 'expose', 'popargs', 'url', 'config', +) + + +__import__('cherrypy._cptools') +__import__('cherrypy._cprequest') + + +tree = _cptree.Tree() + + +__version__ = '17.4.0' + + +engine.listeners['before_request'] = set() +engine.listeners['after_request'] = set() + + +engine.autoreload = process.plugins.Autoreloader(engine) +engine.autoreload.subscribe() + +engine.thread_manager = process.plugins.ThreadManager(engine) +engine.thread_manager.subscribe() + +engine.signal_handler = process.plugins.SignalHandler(engine) + + +class _HandleSignalsPlugin(object): + """Handle signals from other processes. + + Based on the configured platform handlers above. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Add the handlers based on the platform.""" + if hasattr(self.bus, 'signal_handler'): + self.bus.signal_handler.subscribe() + if hasattr(self.bus, 'console_control_handler'): + self.bus.console_control_handler.subscribe() + + +engine.signals = _HandleSignalsPlugin(engine) + + +server = _cpserver.Server() +server.subscribe() + + +def quickstart(root=None, script_name='', config=None): + """Mount the given root, start the builtin server (and engine), then block. + + root: an instance of a "controller class" (a collection of page handler + methods) which represents the root of the application. + script_name: a string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the URL + at which to mount the given root. For example, if root.index() will + handle requests to "http://www.example.com:8080/dept/app1/", then + the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the root + of the URI, it MUST be an empty string (not "/"). + config: a file or dict containing application config. If this contains + a [global] section, those entries will be used in the global + (site-wide) config. + """ + if config: + _global_conf_alias.update(config) + + tree.mount(root, script_name, config) + + engine.signals.subscribe() + engine.start() + engine.block() + + +class _Serving(_local): + """An interface for registering request and response objects. + + Rather than have a separate "thread local" object for the request and + the response, this class works as a single threadlocal container for + both objects (and any others which developers wish to define). In this + way, we can easily dump those objects when we stop/start a new HTTP + conversation, yet still refer to them as module-level globals in a + thread-safe way. + """ + + request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), + _httputil.Host('127.0.0.1', 1111)) + """ + The request object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + response = _cprequest.Response() + """ + The response object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + def load(self, request, response): + self.request = request + self.response = response + + def clear(self): + """Remove all attributes of self.""" + self.__dict__.clear() + + +serving = _Serving() + + +class _ThreadLocalProxy(object): + + __slots__ = ['__attrname__', '__dict__'] + + def __init__(self, attrname): + self.__attrname__ = attrname + + def __getattr__(self, name): + child = getattr(serving, self.__attrname__) + return getattr(child, name) + + def __setattr__(self, name, value): + if name in ('__attrname__', ): + object.__setattr__(self, name, value) + else: + child = getattr(serving, self.__attrname__) + setattr(child, name, value) + + def __delattr__(self, name): + child = getattr(serving, self.__attrname__) + delattr(child, name) + + @property + def __dict__(self): + child = getattr(serving, self.__attrname__) + d = child.__class__.__dict__.copy() + d.update(child.__dict__) + return d + + def __getitem__(self, key): + child = getattr(serving, self.__attrname__) + return child[key] + + def __setitem__(self, key, value): + child = getattr(serving, self.__attrname__) + child[key] = value + + def __delitem__(self, key): + child = getattr(serving, self.__attrname__) + del child[key] + + def __contains__(self, key): + child = getattr(serving, self.__attrname__) + return key in child + + def __len__(self): + child = getattr(serving, self.__attrname__) + return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) + # Python 3 + __bool__ = __nonzero__ + + +# Create request and response object (the same objects will be used +# throughout the entire life of the webserver, but will redirect +# to the "serving" object) +request = _ThreadLocalProxy('request') +response = _ThreadLocalProxy('response') + +# Create thread_data object as a thread-specific all-purpose storage + + +class _ThreadData(_local): + """A container for thread-specific data.""" + + +thread_data = _ThreadData() + + +# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. +# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. +# The only other way would be to change what is returned from type(request) +# and that's not possible in pure Python (you'd have to fake ob_type). +def _cherrypy_pydoc_resolve(thing, forceload=0): + """Given an object or a path to an object, get the object and its name.""" + if isinstance(thing, _ThreadLocalProxy): + thing = getattr(serving, thing.__attrname__) + return _pydoc._builtin_resolve(thing, forceload) + + +try: + import pydoc as _pydoc + _pydoc._builtin_resolve = _pydoc.resolve + _pydoc.resolve = _cherrypy_pydoc_resolve +except ImportError: + pass + + +class _GlobalLogManager(_cplogging.LogManager): + """A site-wide LogManager; routes to app.log or global log as appropriate. + + This :class:`LogManager<cherrypy._cplogging.LogManager>` implements + cherrypy.log() and cherrypy.log.access(). If either + function is called during a request, the message will be sent to the + logger for the current Application. If they are called outside of a + request, the message will be sent to the site-wide logger. + """ + + def __call__(self, *args, **kwargs): + """Log the given message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + # Do NOT use try/except here. See + # https://github.com/cherrypy/cherrypy/issues/945 + if hasattr(request, 'app') and hasattr(request.app, 'log'): + log = request.app.log + else: + log = self + return log.error(*args, **kwargs) + + def access(self): + """Log an access message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + try: + return request.app.log.access() + except AttributeError: + return _cplogging.LogManager.access(self) + + +log = _GlobalLogManager() +# Set a default screen handler on the global log. +log.screen = True +log.error_file = '' +# Using an access file makes CP about 10% slower. Leave off by default. +log.access_file = '' + + +@engine.subscribe('log') +def _buslog(msg, level): + log.error(msg, 'ENGINE', severity=level) + + +# Use _global_conf_alias so quickstart can use 'config' as an arg +# without shadowing cherrypy.config. +config = _global_conf_alias = _cpconfig.Config() +config.defaults = { + 'tools.log_tracebacks.on': True, + 'tools.log_headers.on': True, + 'tools.trailing_slash.on': True, + 'tools.encode.on': True +} +config.namespaces['log'] = lambda k, v: setattr(log, k, v) +config.namespaces['checker'] = lambda k, v: setattr(checker, k, v) +# Must reset to get our defaults applied. +config.reset() + +checker = _cpchecker.Checker() +engine.subscribe('start', checker) diff --git a/libraries/cherrypy/__main__.py b/libraries/cherrypy/__main__.py new file mode 100644 index 00000000..6674f7cb --- /dev/null +++ b/libraries/cherrypy/__main__.py @@ -0,0 +1,5 @@ +"""CherryPy'd cherryd daemon runner.""" +from cherrypy.daemon import run + + +__name__ == '__main__' and run() diff --git a/libraries/cherrypy/_cpchecker.py b/libraries/cherrypy/_cpchecker.py new file mode 100644 index 00000000..39b7c972 --- /dev/null +++ b/libraries/cherrypy/_cpchecker.py @@ -0,0 +1,325 @@ +"""Checker for CherryPy sites and mounted apps.""" +import os +import warnings + +import six +from six.moves import builtins + +import cherrypy + + +class Checker(object): + """A checker for CherryPy sites and their mounted applications. + + When this object is called at engine startup, it executes each + of its own methods whose names start with ``check_``. If you wish + to disable selected checks, simply add a line in your global + config which sets the appropriate method to False:: + + [global] + checker.check_skipped_app_config = False + + You may also dynamically add or replace ``check_*`` methods in this way. + """ + + on = True + """If True (the default), run all checks; if False, turn off all checks.""" + + def __init__(self): + """Initialize Checker instance.""" + self._populate_known_types() + + def __call__(self): + """Run all check_* methods.""" + if self.on: + oldformatwarning = warnings.formatwarning + warnings.formatwarning = self.formatwarning + try: + for name in dir(self): + if name.startswith('check_'): + method = getattr(self, name) + if method and hasattr(method, '__call__'): + method() + finally: + warnings.formatwarning = oldformatwarning + + def formatwarning(self, message, category, filename, lineno, line=None): + """Format a warning.""" + return 'CherryPy Checker:\n%s\n\n' % message + + # This value should be set inside _cpconfig. + global_config_contained_paths = False + + def check_app_config_entries_dont_start_with_script_name(self): + """Check for App config with sections that repeat script_name.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + if sn == '': + continue + sn_atoms = sn.strip('/').split('/') + for key in app.config.keys(): + key_atoms = key.strip('/').split('/') + if key_atoms[:len(sn_atoms)] == sn_atoms: + warnings.warn( + 'The application mounted at %r has config ' + 'entries that start with its script name: %r' % (sn, + key)) + + def check_site_config_entries_in_app_config(self): + """Check for mounted Applications that have site-scoped config.""" + for sn, app in six.iteritems(cherrypy.tree.apps): + if not isinstance(app, cherrypy.Application): + continue + + msg = [] + for section, entries in six.iteritems(app.config): + if section.startswith('/'): + for key, value in six.iteritems(entries): + for n in ('engine.', 'server.', 'tree.', 'checker.'): + if key.startswith(n): + msg.append('[%s] %s = %s' % + (section, key, value)) + if msg: + msg.insert(0, + 'The application mounted at %r contains the ' + 'following config entries, which are only allowed ' + 'in site-wide config. Move them to a [global] ' + 'section and pass them to cherrypy.config.update() ' + 'instead of tree.mount().' % sn) + warnings.warn(os.linesep.join(msg)) + + def check_skipped_app_config(self): + """Check for mounted Applications that have no config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + msg = 'The Application mounted at %r has an empty config.' % sn + if self.global_config_contained_paths: + msg += (' It looks like the config you passed to ' + 'cherrypy.config.update() contains application-' + 'specific sections. You must explicitly pass ' + 'application config via ' + 'cherrypy.tree.mount(..., config=app_config)') + warnings.warn(msg) + return + + def check_app_config_brackets(self): + """Check for App config with extraneous brackets in section names.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + for key in app.config.keys(): + if key.startswith('[') or key.endswith(']'): + warnings.warn( + 'The application mounted at %r has config ' + 'section names with extraneous brackets: %r. ' + 'Config *files* need brackets; config *dicts* ' + '(e.g. passed to tree.mount) do not.' % (sn, key)) + + def check_static_paths(self): + """Check Application config for incorrect static paths.""" + # Use the dummy Request object in the main thread. + request = cherrypy.request + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + request.app = app + for section in app.config: + # get_resource will populate request.config + request.get_resource(section + '/dummy.html') + conf = request.config.get + + if conf('tools.staticdir.on', False): + msg = '' + root = conf('tools.staticdir.root') + dir = conf('tools.staticdir.dir') + if dir is None: + msg = 'tools.staticdir.dir is not set.' + else: + fulldir = '' + if os.path.isabs(dir): + fulldir = dir + if root: + msg = ('dir is an absolute path, even ' + 'though a root is provided.') + testdir = os.path.join(root, dir[1:]) + if os.path.exists(testdir): + msg += ( + '\nIf you meant to serve the ' + 'filesystem folder at %r, remove the ' + 'leading slash from dir.' % (testdir,)) + else: + if not root: + msg = ( + 'dir is a relative path and ' + 'no root provided.') + else: + fulldir = os.path.join(root, dir) + if not os.path.isabs(fulldir): + msg = ('%r is not an absolute path.' % ( + fulldir,)) + + if fulldir and not os.path.exists(fulldir): + if msg: + msg += '\n' + msg += ('%r (root + dir) is not an existing ' + 'filesystem path.' % fulldir) + + if msg: + warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r' + % (msg, section, root, dir)) + + # -------------------------- Compatibility -------------------------- # + obsolete = { + 'server.default_content_type': 'tools.response_headers.headers', + 'log_access_file': 'log.access_file', + 'log_config_options': None, + 'log_file': 'log.error_file', + 'log_file_not_found': None, + 'log_request_headers': 'tools.log_headers.on', + 'log_to_screen': 'log.screen', + 'show_tracebacks': 'request.show_tracebacks', + 'throw_errors': 'request.throw_errors', + 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' + 'cherrypy.Application(Root())))'), + } + + deprecated = {} + + def _compat(self, config): + """Process config and warn on each obsolete or deprecated entry.""" + for section, conf in config.items(): + if isinstance(conf, dict): + for k in conf: + if k in self.obsolete: + warnings.warn('%r is obsolete. Use %r instead.\n' + 'section: [%s]' % + (k, self.obsolete[k], section)) + elif k in self.deprecated: + warnings.warn('%r is deprecated. Use %r instead.\n' + 'section: [%s]' % + (k, self.deprecated[k], section)) + else: + if section in self.obsolete: + warnings.warn('%r is obsolete. Use %r instead.' + % (section, self.obsolete[section])) + elif section in self.deprecated: + warnings.warn('%r is deprecated. Use %r instead.' + % (section, self.deprecated[section])) + + def check_compatibility(self): + """Process config and warn on each obsolete or deprecated entry.""" + self._compat(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._compat(app.config) + + # ------------------------ Known Namespaces ------------------------ # + extra_config_namespaces = [] + + def _known_ns(self, app): + ns = ['wsgi'] + ns.extend(app.toolboxes) + ns.extend(app.namespaces) + ns.extend(app.request_class.namespaces) + ns.extend(cherrypy.config.namespaces) + ns += self.extra_config_namespaces + + for section, conf in app.config.items(): + is_path_section = section.startswith('/') + if is_path_section and isinstance(conf, dict): + for k in conf: + atoms = k.split('.') + if len(atoms) > 1: + if atoms[0] not in ns: + # Spit out a special warning if a known + # namespace is preceded by "cherrypy." + if atoms[0] == 'cherrypy' and atoms[1] in ns: + msg = ( + 'The config entry %r is invalid; ' + 'try %r instead.\nsection: [%s]' + % (k, '.'.join(atoms[1:]), section)) + else: + msg = ( + 'The config entry %r is invalid, ' + 'because the %r config namespace ' + 'is unknown.\n' + 'section: [%s]' % (k, atoms[0], section)) + warnings.warn(msg) + elif atoms[0] == 'tools': + if atoms[1] not in dir(cherrypy.tools): + msg = ( + 'The config entry %r may be invalid, ' + 'because the %r tool was not found.\n' + 'section: [%s]' % (k, atoms[1], section)) + warnings.warn(msg) + + def check_config_namespaces(self): + """Process config and warn on each unknown config namespace.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_ns(app) + + # -------------------------- Config Types -------------------------- # + known_config_types = {} + + def _populate_known_types(self): + b = [x for x in vars(builtins).values() + if type(x) is type(str)] + + def traverse(obj, namespace): + for name in dir(obj): + # Hack for 3.2's warning about body_params + if name == 'body_params': + continue + vtype = type(getattr(obj, name, None)) + if vtype in b: + self.known_config_types[namespace + '.' + name] = vtype + + traverse(cherrypy.request, 'request') + traverse(cherrypy.response, 'response') + traverse(cherrypy.server, 'server') + traverse(cherrypy.engine, 'engine') + traverse(cherrypy.log, 'log') + + def _known_types(self, config): + msg = ('The config entry %r in section %r is of type %r, ' + 'which does not match the expected type %r.') + + for section, conf in config.items(): + if not isinstance(conf, dict): + conf = {section: conf} + for k, v in conf.items(): + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + + def check_config_types(self): + """Assert that config values are of the same type as default values.""" + self._known_types(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_types(app.config) + + # -------------------- Specific config warnings -------------------- # + def check_localhost(self): + """Warn if any socket_host is 'localhost'. See #711.""" + for k, v in cherrypy.config.items(): + if k == 'server.socket_host' and v == 'localhost': + warnings.warn("The use of 'localhost' as a socket host can " + 'cause problems on newer systems, since ' + "'localhost' can map to either an IPv4 or an " + "IPv6 address. You should use '127.0.0.1' " + "or '[::1]' instead.") diff --git a/libraries/cherrypy/_cpcompat.py b/libraries/cherrypy/_cpcompat.py new file mode 100644 index 00000000..f454505c --- /dev/null +++ b/libraries/cherrypy/_cpcompat.py @@ -0,0 +1,162 @@ +"""Compatibility code for using CherryPy with various versions of Python. + +To retain compatibility with older Python versions, this module provides a +useful abstraction over the differences between Python versions, sometimes by +preferring a newer idiom, sometimes an older one, and sometimes a custom one. + +In particular, Python 2 uses str and '' for byte strings, while Python 3 +uses str and '' for unicode strings. We will call each of these the 'native +string' type for each version. Because of this major difference, this module +provides +two functions: 'ntob', which translates native strings (of type 'str') into +byte strings regardless of Python version, and 'ntou', which translates native +strings to unicode strings. + +Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'. +They were created with Python 2.3-2.5 compatibility in mind. +Instead, use unicode literals (from __future__) and bytes literals +and their .encode/.decode methods as needed. +""" + +import re +import sys +import threading + +import six +from six.moves import urllib + + +if six.PY3: + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) + + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n + + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n +else: + # Python 2 + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given + encoding. + """ + assert_native(n) + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses + # this to signal that it wants to pass a string with embedded \uXXXX + # escapes, but without having to prefix it with u'' for Python 2, + # but no prefix for Python 3. + if encoding == 'escape': + return six.text_type( # unicode for Python 2 + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: six.unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 + # is almost always what was intended. + return n.decode(encoding) + + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 2, the native string type is bytes. + if isinstance(n, six.text_type): # unicode for Python 2 + return n.encode(encoding) + return n + + +def assert_native(n): + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) + + +# Some platforms don't expose HTTPSConnection, so handle it separately +HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None) + + +def _unquote_plus_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.unquote_plus(string).decode(encoding, errors) + + +def _unquote_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.unquote(string).decode(encoding, errors) + + +def _quote_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.quote(string.encode(encoding, errors)) + + +unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat +unquote = urllib.parse.unquote if six.PY3 else _unquote_compat +quote = urllib.parse.quote if six.PY3 else _quote_compat + +try: + # Prefer simplejson + import simplejson as json +except ImportError: + import json + + +json_decode = json.JSONDecoder().decode +_json_encode = json.JSONEncoder().iterencode + + +if six.PY3: + # Encode to bytes on Python 3 + def json_encode(value): + for chunk in _json_encode(value): + yield chunk.encode('utf-8') +else: + json_encode = _json_encode + + +text_or_bytes = six.text_type, bytes + + +if sys.version_info >= (3, 3): + Timer = threading.Timer + Event = threading.Event +else: + # Python 3.2 and earlier + Timer = threading._Timer + Event = threading._Event + +# html module come in 3.2 version +try: + from html import escape +except ImportError: + from cgi import escape + + +# html module needed the argument quote=False because in cgi the default +# is False. With quote=True the results differ. + +def escape_html(s, escape_quote=False): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. + + When escape_quote=True, escape (') and (") chars. + """ + return escape(s, quote=escape_quote) diff --git a/libraries/cherrypy/_cpconfig.py b/libraries/cherrypy/_cpconfig.py new file mode 100644 index 00000000..79d9d911 --- /dev/null +++ b/libraries/cherrypy/_cpconfig.py @@ -0,0 +1,300 @@ +""" +Configuration system for CherryPy. + +Configuration in CherryPy is implemented via dictionaries. Keys are strings +which name the mapped value, which may be of any type. + + +Architecture +------------ + +CherryPy Requests are part of an Application, which runs in a global context, +and configuration data may apply to any of those three scopes: + +Global + Configuration entries which apply everywhere are stored in + cherrypy.config. + +Application + Entries which apply to each mounted application are stored + on the Application object itself, as 'app.config'. This is a two-level + dict where each key is a path, or "relative URL" (for example, "/" or + "/path/to/my/page"), and each value is a config dict. Usually, this + data is provided in the call to tree.mount(root(), config=conf), + although you may also use app.merge(conf). + +Request + Each Request object possesses a single 'Request.config' dict. + Early in the request process, this dict is populated by merging global + config entries, Application entries (whose path equals or is a parent + of Request.path_info), and any config acquired while looking up the + page handler (see next). + + +Declaration +----------- + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, CherryPy +uses Python's builtin ConfigParser; you declare Application config by +writing each path as a section header:: + + [/path/to/my/page] + request.stream = True + +To declare global configuration entries, place them in a [global] section. + +You may also declare config entries directly on the classes and methods +(page handlers) that make up your CherryPy application via the ``_cp_config`` +attribute, set with the ``cherrypy.config`` decorator. For example:: + + @cherrypy.config(**{'tools.gzip.on': True}) + class Demo: + + @cherrypy.expose + @cherrypy.config(**{'request.show_tracebacks': False}) + def index(self): + return "Hello world" + +.. note:: + + This behavior is only guaranteed for the default dispatcher. + Other dispatchers may have different restrictions on where + you can attach config attributes. + + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. +Current namespaces: + +engine + Controls the 'application engine', including autoreload. + These can only be declared in the global config. + +tree + Grafts cherrypy.Application objects onto cherrypy.tree. + These can only be declared in the global config. + +hooks + Declares additional request-processing functions. + +log + Configures the logging for each application. + These can only be declared in the global or / config. + +request + Adds attributes to each Request. + +response + Adds attributes to each Response. + +server + Controls the default HTTP server via cherrypy.server. + These can only be declared in the global config. + +tools + Runs and configures additional request-processing packages. + +wsgi + Adds WSGI middleware to an Application's "pipeline". + These can only be declared in the app's root config ("/"). + +checker + Controls the 'checker', which looks for common errors in + app state (including config) when the engine starts. + Global config only. + +The only key that does not exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +cherrypy._cpconfig.environments[environment]. It only applies to the global +config, and only when you use cherrypy.config.update. + +You can define your own namespaces to be called at the Global, Application, +or Request level, by adding a named handler to cherrypy.config.namespaces, +app.namespaces, or app.request_class.namespaces. The name can +be any string, and the handler must be either a callable or a (Python 2.5 +style) context manager. +""" + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import reprconf + + +def _if_filename_register_autoreload(ob): + """Register for autoreload if ob is a string (presumed filename).""" + is_filename = isinstance(ob, text_or_bytes) + is_filename and cherrypy.engine.autoreload.files.add(ob) + + +def merge(base, other): + """Merge one app config (from a dict, file, or filename) into another. + + If the given config is a filename, it will be appended to + the list of files to monitor for "autoreload" changes. + """ + _if_filename_register_autoreload(other) + + # Load other into base + for section, value_map in reprconf.Parser.load(other).items(): + if not isinstance(value_map, dict): + raise ValueError( + 'Application config must include section headers, but the ' + "config you tried to merge doesn't have any sections. " + 'Wrap your config in another dict with paths as section ' + "headers, for example: {'/': config}.") + base.setdefault(section, {}).update(value_map) + + +class Config(reprconf.Config): + """The 'global' configuration data for the entire CherryPy process.""" + + def update(self, config): + """Update self from a dict, file or filename.""" + _if_filename_register_autoreload(config) + super(Config, self).update(config) + + def _apply(self, config): + """Update self from a dict.""" + if isinstance(config.get('global'), dict): + if len(config) > 1: + cherrypy.checker.global_config_contained_paths = True + config = config['global'] + if 'tools.staticdir.dir' in config: + config['tools.staticdir.section'] = 'global' + super(Config, self)._apply(config) + + @staticmethod + def __call__(**kwargs): + """Decorate for page handlers to set _cp_config.""" + def tool_decorator(f): + _Vars(f).setdefault('_cp_config', {}).update(kwargs) + return f + return tool_decorator + + +class _Vars(object): + """Adapter allowing setting a default attribute on a function or class.""" + + def __init__(self, target): + self.target = target + + def setdefault(self, key, default): + if not hasattr(self.target, key): + setattr(self.target, key, default) + return getattr(self.target, key) + + +# Sphinx begin config.environments +Config.environments = environments = { + 'staging': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + }, + 'production': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + }, + 'embedded': { + # For use with CherryPy embedded in another deployment stack. + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }, + 'test_suite': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': True, + 'request.show_mismatched_params': True, + 'log.screen': False, + }, +} +# Sphinx end config.environments + + +def _server_namespace_handler(k, v): + """Config handler for the "server" namespace.""" + atoms = k.split('.', 1) + if len(atoms) > 1: + # Special-case config keys of the form 'server.servername.socket_port' + # to configure additional HTTP servers. + if not hasattr(cherrypy, 'servers'): + cherrypy.servers = {} + + servername, k = atoms + if servername not in cherrypy.servers: + from cherrypy import _cpserver + cherrypy.servers[servername] = _cpserver.Server() + # On by default, but 'on = False' can unsubscribe it (see below). + cherrypy.servers[servername].subscribe() + + if k == 'on': + if v: + cherrypy.servers[servername].subscribe() + else: + cherrypy.servers[servername].unsubscribe() + else: + setattr(cherrypy.servers[servername], k, v) + else: + setattr(cherrypy.server, k, v) + + +Config.namespaces['server'] = _server_namespace_handler + + +def _engine_namespace_handler(k, v): + """Config handler for the "engine" namespace.""" + engine = cherrypy.engine + + if k in {'SIGHUP', 'SIGTERM'}: + engine.subscribe(k, v) + return + + if '.' in k: + plugin, attrname = k.split('.', 1) + try: + plugin = getattr(engine, plugin) + except Exception as error: + setattr(engine, k, v) + else: + op = 'subscribe' if v else 'unsubscribe' + sub_unsub = getattr(plugin, op, None) + if attrname == 'on' and callable(sub_unsub): + sub_unsub() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) + + +Config.namespaces['engine'] = _engine_namespace_handler + + +def _tree_namespace_handler(k, v): + """Namespace handler for the 'tree' config namespace.""" + if isinstance(v, dict): + for script_name, app in v.items(): + cherrypy.tree.graft(app, script_name) + msg = 'Mounted: %s on %s' % (app, script_name or '/') + cherrypy.engine.log(msg) + else: + cherrypy.tree.graft(v, v.script_name) + cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/')) + + +Config.namespaces['tree'] = _tree_namespace_handler diff --git a/libraries/cherrypy/_cpdispatch.py b/libraries/cherrypy/_cpdispatch.py new file mode 100644 index 00000000..83eb79cb --- /dev/null +++ b/libraries/cherrypy/_cpdispatch.py @@ -0,0 +1,686 @@ +"""CherryPy dispatchers. + +A 'dispatcher' is the object which looks up the 'page handler' callable +and collects config for the current request based on the path_info, other +request attributes, and the application architecture. The core calls the +dispatcher as early as possible, passing it a 'path_info' argument. + +The default dispatcher discovers the page handler by matching path_info +to a hierarchical arrangement of objects, starting at request.app.root. +""" + +import string +import sys +import types +try: + classtype = (type, types.ClassType) +except AttributeError: + classtype = type + +import cherrypy + + +class PageHandler(object): + + """Callable which sets response.body.""" + + def __init__(self, callable, *args, **kwargs): + self.callable = callable + self.args = args + self.kwargs = kwargs + + @property + def args(self): + """The ordered args should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.args + + @args.setter + def args(self, args): + cherrypy.serving.request.args = args + return cherrypy.serving.request.args + + @property + def kwargs(self): + """The named kwargs should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + return cherrypy.serving.request.kwargs + + def __call__(self): + try: + return self.callable(*self.args, **self.kwargs) + except TypeError: + x = sys.exc_info()[1] + try: + test_callable_spec(self.callable, self.args, self.kwargs) + except cherrypy.HTTPError: + raise sys.exc_info()[1] + except Exception: + raise x + raise + + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the + handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + show_mismatched_params = getattr( + cherrypy.serving.request, 'show_mismatched_params', False) + try: + (args, varargs, varkw, defaults) = getargspec(callable) + except TypeError: + if isinstance(callable, object) and hasattr(callable, '__call__'): + (args, varargs, varkw, + defaults) = getargspec(callable.__call__) + else: + # If it wasn't one of our own types, re-raise + # the original error + raise + + if args and ( + # For callable objects, which have a __call__(self) method + hasattr(callable, '__call__') or + # For normal methods + inspect.ismethod(callable) + ): + # Strip 'self' + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + # figure out which args have defaults. + args_with_defaults = args[-len(defaults or []):] + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args_with_defaults[i]] == 0: + arg_usage[args_with_defaults[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.items(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + message = None + if show_mismatched_params: + message = 'Missing parameters: %s' % ','.join(missing_args) + raise cherrypy.HTTPError(404, message=message) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.serving.request.body.params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + message = None + if show_mismatched_params: + message = 'Multiple values for parameters: '\ + '%s' % ','.join(multiple_args) + raise cherrypy.HTTPError(error, message=message) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + message = None + if show_mismatched_params: + message = 'Unexpected query string '\ + 'parameters: %s' % ', '.join(extra_qs_params) + raise cherrypy.HTTPError(404, message=message) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + message = None + if show_mismatched_params: + message = 'Unexpected body parameters: '\ + '%s' % ', '.join(extra_body_params) + raise cherrypy.HTTPError(400, message=message) + + +try: + import inspect +except ImportError: + 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] + + +class LateParamPageHandler(PageHandler): + + """When passing cherrypy.request.params to the page handler, we do not + want to capture that dict too early; we want to give tools like the + decoding tool a chance to modify the params dict in-between the lookup + of the handler and the actual calling of the handler. This subclass + takes that into account, and allows request.params to be 'bound late' + (it's more complicated than that, but that's the effect). + """ + + @property + def kwargs(self): + """Page handler kwargs (with cherrypy.request.params copied in).""" + kwargs = cherrypy.serving.request.params.copy() + if self._kwargs: + kwargs.update(self._kwargs) + return kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + self._kwargs = kwargs + + +if sys.version_info < (3, 0): + punctuation_to_underscores = string.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, str) or len(t) != 256: + raise ValueError( + 'The translate argument must be a str of len 256.') +else: + punctuation_to_underscores = str.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, dict): + raise ValueError('The translate argument must be a dict.') + + +class Dispatcher(object): + + """CherryPy Dispatcher which walks a tree of objects to find a handler. + + The tree is rooted at cherrypy.request.app.root, and each hierarchical + component in the path_info argument is matched to a corresponding nested + attribute of the root object. Matching handlers must have an 'exposed' + attribute which evaluates to True. The special method name "index" + matches a URI which ends in a slash ("/"). The special method name + "default" may match a portion of the path_info (but only when no longer + substring of the path_info matches some other object). + + This is the default, built-in dispatcher for CherryPy. + """ + + dispatch_method_name = '_cp_dispatch' + """ + The name of the dispatch method that nodes may optionally implement + to provide their own dynamic dispatch algorithm. + """ + + def __init__(self, dispatch_method_name=None, + translate=punctuation_to_underscores): + validate_translator(translate) + self.translate = translate + if dispatch_method_name: + self.dispatch_method_name = dispatch_method_name + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + func, vpath = self.find_handler(path_info) + + if func: + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.NotFound() + + def find_handler(self, path): + """Return the appropriate page handler, plus any virtual path. + + This will return two objects. The first will be a callable, + which can be used to generate page output. Any parameters from + the query string or request body will be sent to that callable + as keyword arguments. + + The callable is found by traversing the application's tree, + starting from cherrypy.request.app.root, and matching path + components to successive objects in the tree. For example, the + URL "/path/to/handler" might return root.path.to.handler. + + The second object returned will be a list of names which are + 'virtual path' components: parts of the URL which are dynamic, + and were not used when looking up the handler. + These virtual path components are passed to the handler as + positional arguments. + """ + request = cherrypy.serving.request + app = request.app + root = app.root + dispatch_name = self.dispatch_method_name + + # Get config for the root object/path. + fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] + fullpath_len = len(fullpath) + segleft = fullpath_len + nodeconf = {} + if hasattr(root, '_cp_config'): + nodeconf.update(root._cp_config) + if '/' in app.config: + nodeconf.update(app.config['/']) + object_trail = [['root', root, nodeconf, segleft]] + + node = root + iternames = fullpath[:] + while iternames: + name = iternames[0] + # map to legal Python identifiers (e.g. replace '.' with '_') + objname = name.translate(self.translate) + + nodeconf = {} + subnode = getattr(node, objname, None) + pre_len = len(iternames) + if subnode is None: + dispatch = getattr(node, dispatch_name, None) + if dispatch and hasattr(dispatch, '__call__') and not \ + getattr(dispatch, 'exposed', False) and \ + pre_len > 1: + # Don't expose the hidden 'index' token to _cp_dispatch + # We skip this if pre_len == 1 since it makes no sense + # to call a dispatcher when we have no tokens left. + index_name = iternames.pop() + subnode = dispatch(vpath=iternames) + iternames.append(index_name) + else: + # We didn't find a path, but keep processing in case there + # is a default() handler. + iternames.pop(0) + else: + # We found the path, remove the vpath entry + iternames.pop(0) + segleft = len(iternames) + if segleft > pre_len: + # No path segment was removed. Raise an error. + raise cherrypy.CherryPyException( + 'A vpath segment was added. Custom dispatchers may only ' + 'remove elements. While trying to process ' + '{0} in {1}'.format(name, fullpath) + ) + elif segleft == pre_len: + # Assume that the handler used the current path segment, but + # did not pop it. This allows things like + # return getattr(self, vpath[0], None) + iternames.pop(0) + segleft -= 1 + node = subnode + + if node is not None: + # Get _cp_config attached to this node. + if hasattr(node, '_cp_config'): + nodeconf.update(node._cp_config) + + # Mix in values from app.config for this path. + existing_len = fullpath_len - pre_len + if existing_len != 0: + curpath = '/' + '/'.join(fullpath[0:existing_len]) + else: + curpath = '' + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] + for seg in new_segs: + curpath += '/' + seg + if curpath in app.config: + nodeconf.update(app.config[curpath]) + + object_trail.append([name, node, nodeconf, segleft]) + + def set_conf(): + """Collapse all object_trail config into cherrypy.request.config. + """ + base = cherrypy.config.copy() + # Note that we merge the config from each node + # even if that node was None. + for name, obj, conf, segleft in object_trail: + base.update(conf) + if 'tools.staticdir.dir' in conf: + base['tools.staticdir.section'] = '/' + \ + '/'.join(fullpath[0:fullpath_len - segleft]) + return base + + # Try successive objects (reverse order) + num_candidates = len(object_trail) - 1 + for i in range(num_candidates, -1, -1): + + name, candidate, nodeconf, segleft = object_trail[i] + if candidate is None: + continue + + # Try a "default" method on the current leaf. + if hasattr(candidate, 'default'): + defhandler = candidate.default + if getattr(defhandler, 'exposed', False): + # Insert any extra _cp_config from the default handler. + conf = getattr(defhandler, '_cp_config', {}) + object_trail.insert( + i + 1, ['default', defhandler, conf, segleft]) + request.config = set_conf() + # See https://github.com/cherrypy/cherrypy/issues/613 + request.is_index = path.endswith('/') + return defhandler, fullpath[fullpath_len - segleft:-1] + + # Uncomment the next line to restrict positional params to + # "default". + # if i < num_candidates - 2: continue + + # Try the current leaf. + if getattr(candidate, 'exposed', False): + request.config = set_conf() + if i == num_candidates: + # We found the extra ".index". Mark request so tools + # can redirect if path_info has no trailing slash. + request.is_index = True + else: + # We're not at an 'index' handler. Mark request so tools + # can redirect if path_info has NO trailing slash. + # Note that this also includes handlers which take + # positional parameters (virtual paths). + request.is_index = False + return candidate, fullpath[fullpath_len - segleft:-1] + + # We didn't find anything + request.config = set_conf() + return None, [] + + +class MethodDispatcher(Dispatcher): + + """Additional dispatch based on cherrypy.request.method.upper(). + + Methods named GET, POST, etc will be called on an exposed class. + The method names must be all caps; the appropriate Allow header + will be output showing all capitalized method names as allowable + HTTP verbs. + + Note that the containing class must be exposed, not the methods. + """ + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + resource, vpath = self.find_handler(path_info) + + if resource: + # Set Allow header + avail = [m for m in dir(resource) if m.isupper()] + if 'GET' in avail and 'HEAD' not in avail: + avail.append('HEAD') + avail.sort() + cherrypy.serving.response.headers['Allow'] = ', '.join(avail) + + # Find the subhandler + meth = request.method.upper() + func = getattr(resource, meth, None) + if func is None and meth == 'HEAD': + func = getattr(resource, 'GET', None) + if func: + # Grab any _cp_config on the subhandler. + if hasattr(func, '_cp_config'): + request.config.update(func._cp_config) + + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.HTTPError(405) + else: + request.handler = cherrypy.NotFound() + + +class RoutesDispatcher(object): + + """A Routes based dispatcher for CherryPy.""" + + def __init__(self, full_result=False, **mapper_options): + """ + Routes dispatcher + + Set full_result to True if you wish the controller + and the action to be passed on to the page handler + parameters. By default they won't be. + """ + import routes + self.full_result = full_result + self.controllers = {} + self.mapper = routes.Mapper(**mapper_options) + self.mapper.controller_scan = self.controllers.keys + + def connect(self, name, route, controller, **kwargs): + self.controllers[name] = controller + self.mapper.connect(name, route, controller=name, **kwargs) + + def redirect(self, url): + raise cherrypy.HTTPRedirect(url) + + def __call__(self, path_info): + """Set handler and config for the current request.""" + func = self.find_handler(path_info) + if func: + cherrypy.serving.request.handler = LateParamPageHandler(func) + else: + cherrypy.serving.request.handler = cherrypy.NotFound() + + def find_handler(self, path_info): + """Find the right page handler, and set request.config.""" + import routes + + request = cherrypy.serving.request + + config = routes.request_config() + config.mapper = self.mapper + if hasattr(request, 'wsgi_environ'): + config.environ = request.wsgi_environ + config.host = request.headers.get('Host', None) + config.protocol = request.scheme + config.redirect = self.redirect + + result = self.mapper.match(path_info) + + config.mapper_dict = result + params = {} + if result: + params = result.copy() + if not self.full_result: + params.pop('controller', None) + params.pop('action', None) + request.params.update(params) + + # Get config for the root object/path. + request.config = base = cherrypy.config.copy() + curpath = '' + + def merge(nodeconf): + if 'tools.staticdir.dir' in nodeconf: + nodeconf['tools.staticdir.section'] = curpath or '/' + base.update(nodeconf) + + app = request.app + root = app.root + if hasattr(root, '_cp_config'): + merge(root._cp_config) + if '/' in app.config: + merge(app.config['/']) + + # Mix in values from app.config. + atoms = [x for x in path_info.split('/') if x] + if atoms: + last = atoms.pop() + else: + last = None + for atom in atoms: + curpath = '/'.join((curpath, atom)) + if curpath in app.config: + merge(app.config[curpath]) + + handler = None + if result: + controller = result.get('controller') + controller = self.controllers.get(controller, controller) + if controller: + if isinstance(controller, classtype): + controller = controller() + # Get config from the controller. + if hasattr(controller, '_cp_config'): + merge(controller._cp_config) + + action = result.get('action') + if action is not None: + handler = getattr(controller, action, None) + # Get config from the handler + if hasattr(handler, '_cp_config'): + merge(handler._cp_config) + else: + handler = controller + + # Do the last path atom here so it can + # override the controller's _cp_config. + if last: + curpath = '/'.join((curpath, last)) + if curpath in app.config: + merge(app.config[curpath]) + + return handler + + +def XMLRPCDispatcher(next_dispatcher=Dispatcher()): + from cherrypy.lib import xmlrpcutil + + def xmlrpc_dispatch(path_info): + path_info = xmlrpcutil.patched_path(path_info) + return next_dispatcher(path_info) + return xmlrpc_dispatch + + +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, + **domains): + """ + Select a different handler based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different parts of a single + website structure. For example:: + + http://www.domain.example -> root + http://www.domain2.example -> root/domain2/ + http://www.domain2.example:443 -> root/secure + + can be accomplished via the following config:: + + [/] + request.dispatch = cherrypy.dispatch.VirtualHost( + **{'www.domain2.example': '/domain2', + 'www.domain2.example:443': '/secure', + }) + + next_dispatcher + The next dispatcher object in the dispatch chain. + The VirtualHost dispatcher adds a prefix to the URL and calls + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). + + use_x_forwarded_host + If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying. + + ``**domains`` + A dict of {host header value: virtual prefix} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding "virtual prefix" + value will be prepended to the URL path before calling the + next dispatcher. Note that you often need separate entries + for "example.com" and "www.example.com". In addition, "Host" + headers may contain the port number. + """ + from cherrypy.lib import httputil + + def vhost_dispatch(path_info): + request = cherrypy.serving.request + header = request.headers.get + + domain = header('Host', '') + if use_x_forwarded_host: + domain = header('X-Forwarded-Host', domain) + + prefix = domains.get(domain, '') + if prefix: + path_info = httputil.urljoin(prefix, path_info) + + result = next_dispatcher(path_info) + + # Touch up staticdir config. See + # https://github.com/cherrypy/cherrypy/issues/614. + section = request.config.get('tools.staticdir.section') + if section: + section = section[len(prefix):] + request.config['tools.staticdir.section'] = section + + return result + return vhost_dispatch diff --git a/libraries/cherrypy/_cperror.py b/libraries/cherrypy/_cperror.py new file mode 100644 index 00000000..e2a8fad8 --- /dev/null +++ b/libraries/cherrypy/_cperror.py @@ -0,0 +1,619 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise +themselves; this means you can set an +:class:`HTTPError<cherrypy._cperror.HTTPError>` +or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the +:attr:`request.handler<cherrypy._cprequest.Request.handler>`. + +.. _redirectingpost: + +Redirecting POST +================ + +When you GET a resource and are redirected by the server to another Location, +there's generally no problem since GET is both a "safe method" (there should +be no side-effects) and an "idempotent method" (multiple calls are no different +than a single call). + +POST, however, is neither safe nor idempotent--if you +charge a credit card, you don't want to be charged twice by a redirect! + +For this reason, *none* of the 3xx responses permit a user-agent (browser) to +resubmit a POST on redirection without first confirming the action with the +user: + +===== ================================= =========== +300 Multiple Choices Confirm with the user +301 Moved Permanently Confirm with the user +302 Found (Object moved temporarily) Confirm with the user +303 See Other GET the new URI; no confirmation +304 Not modified for conditional GET only; + POST should not raise this error +305 Use Proxy Confirm with the user +307 Temporary Redirect Confirm with the user +===== ================================= =========== + +However, browsers have historically implemented these restrictions poorly; +in particular, many browsers do not force the user to confirm 301, 302 +or 307 when redirecting POST. For this reason, CherryPy defaults to 303, +which most user-agents appear to have implemented correctly. Therefore, if +you raise HTTPRedirect for a POST request, the user-agent will most likely +attempt to GET the new URI (without asking for confirmation from the user). +We realize this is confusing for developers, but it's the safest thing we +could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` +or any other 3xx status if you know what you're doing, but given the +environment, we couldn't let any of those be the default. + +Custom Error Handling +===================== + +.. image:: /refman/cperrors.gif + +Anticipated HTTP responses +-------------------------- + +The 'error_page' config namespace can be used to provide custom HTML output for +expected responses (like 404 Not Found). Supply a filename from which the +output will be read. The contents will be interpolated with the values +%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python +`string formatting +<http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_. + +:: + + _cp_config = { + 'error_page.404': os.path.join(localDir, "static/index.html") + } + + +Beginning in version 3.1, you may also provide a function or other callable as +an error_page entry. It will be passed the same status, message, traceback and +version arguments that are interpolated into templates:: + + def error_page_402(status, message, traceback, version): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + cherrypy.config.update({'error_page.402': error_page_402}) + +Also in 3.1, in addition to the numbered error codes, you may also supply +"error_page.default" to handle all codes which do not have their own error_page +entry. + + + +Unanticipated errors +-------------------- + +CherryPy also has a generic error handling mechanism: whenever an unanticipated +error occurs in your code, it will call +:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to +set the response status, headers, and body. By default, this is the same +output as +:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide +some other behavior, you generally replace "request.error_response". + +Here is some sample code that shows how to display a custom error message and +send an e-mail containing the error:: + + from cherrypy import _cperror + + def handle_error(): + cherrypy.response.status = 500 + cherrypy.response.body = [ + "<html><body>Sorry, an error occurred</body></html>" + ] + sendMail('error@domain.com', + 'Error in your web app', + _cperror.format_exc()) + + @cherrypy.config(**{'request.error_response': handle_error}) + class Root: + pass + +Note that you have to explicitly set +:attr:`response.body <cherrypy._cprequest.Response.body>` +and not simply return an error message as a result. +""" + +import io +import contextlib +from sys import exc_info as _exc_info +from traceback import format_exception as _format_exception +from xml.sax import saxutils + +import six +from six.moves import urllib + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cpcompat import escape_html +from cherrypy._cpcompat import ntob +from cherrypy._cpcompat import tonative +from cherrypy._helper import classproperty +from cherrypy.lib import httputil as _httputil + + +class CherryPyException(Exception): + + """A base class for CherryPy exceptions.""" + pass + + +class InternalRedirect(CherryPyException): + + """Exception raised to switch to the handler for a different URL. + + This exception will redirect processing to another path within the site + (without informing the client). Provide the new path as an argument when + raising the exception. Provide any params in the querystring for the new + URL. + """ + + def __init__(self, path, query_string=''): + self.request = cherrypy.serving.request + + self.query_string = query_string + if '?' in path: + # Separate any params included in the path + path, self.query_string = path.split('?', 1) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a URL relative to root (e.g. "/dummy") + # 2. a URL relative to the current path + # Note that any query string will be discarded. + path = urllib.parse.urljoin(self.request.path_info, path) + + # Set a 'path' member attribute so that code which traps this + # error can have access to it. + self.path = path + + CherryPyException.__init__(self, path, self.query_string) + + +class HTTPRedirect(CherryPyException): + + """Exception raised when the request should be redirected. + + This exception will force a HTTP redirect to the URL or URL's you give it. + The new URL must be passed as the first argument to the Exception, + e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. + If a URL is absolute, it will be used as-is. If it is relative, it is + assumed to be relative to the current cherrypy.request.path_info. + + If one of the provided URL is a unicode object, it will be encoded + using the default encoding or the one passed in parameter. + + There are multiple types of redirect, from which you can select via the + ``status`` argument. If you do not provide a ``status`` arg, it defaults to + 303 (or 302 if responding with HTTP/1.0). + + Examples:: + + raise cherrypy.HTTPRedirect("") + raise cherrypy.HTTPRedirect("/abs/path", 307) + raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) + + See :ref:`redirectingpost` for additional caveats. + """ + + urls = None + """The list of URL's to emit.""" + + encoding = 'utf-8' + """The encoding when passed urls are not native strings""" + + def __init__(self, urls, status=None, encoding=None): + self.urls = abs_urls = [ + # Note that urljoin will "do the right thing" whether url is: + # 1. a complete URL with host (e.g. "http://www.example.com/test") + # 2. a URL relative to root (e.g. "/dummy") + # 3. a URL relative to the current path + # Note that any query string in cherrypy.request is discarded. + urllib.parse.urljoin( + cherrypy.url(), + tonative(url, encoding or self.encoding), + ) + for url in always_iterable(urls) + ] + + status = ( + int(status) + if status is not None + else self.default_status + ) + if not 300 <= status <= 399: + raise ValueError('status must be between 300 and 399.') + + CherryPyException.__init__(self, abs_urls, status) + + @classproperty + def default_status(cls): + """ + The default redirect status for the request. + + RFC 2616 indicates a 301 response code fits our goal; however, + browser support for 301 is quite messy. Use 302/303 instead. See + http://www.alanflavell.org.uk/www/post-redirect.html + """ + return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302 + + @property + def status(self): + """The integer HTTP status code to emit.""" + _, status = self.args[:2] + return status + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPRedirect object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + response.status = status = self.status + + if status in (300, 301, 302, 303, 307): + response.headers['Content-Type'] = 'text/html;charset=utf-8' + # "The ... URI SHOULD be given by the Location field + # in the response." + response.headers['Location'] = self.urls[0] + + # "Unless the request method was HEAD, the entity of the response + # SHOULD contain a short hypertext note with a hyperlink to the + # new URI(s)." + msg = { + 300: 'This resource can be found at ', + 301: 'This resource has permanently moved to ', + 302: 'This resource resides temporarily at ', + 303: 'This resource can be found at ', + 307: 'This resource has moved temporarily to ', + }[status] + msg += '<a href=%s>%s</a>.' + msgs = [ + msg % (saxutils.quoteattr(u), escape_html(u)) + for u in self.urls + ] + response.body = ntob('<br />\n'.join(msgs), 'utf-8') + # Previous code may have set C-L, so we have to reset it + # (allow finalize to set it). + response.headers.pop('Content-Length', None) + elif status == 304: + # Not Modified. + # "The response MUST include the following header fields: + # Date, unless its omission is required by section 14.18.1" + # The "Date" header should have been set in Response.__init__ + + # "...the response SHOULD NOT include other entity-headers." + for key in ('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Location', 'Content-MD5', + 'Content-Range', 'Content-Type', 'Expires', + 'Last-Modified'): + if key in response.headers: + del response.headers[key] + + # "The 304 response MUST NOT contain a message-body." + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + elif status == 305: + # Use Proxy. + # self.urls[0] should be the URI of the proxy. + response.headers['Location'] = ntob(self.urls[0], 'utf-8') + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + else: + raise ValueError('The %s status code is unknown.' % status) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +def clean_headers(status): + """Remove any headers which should not apply to an error response.""" + response = cherrypy.serving.response + + # Remove headers which applied to the original content, + # but do not apply to the error page. + respheaders = response.headers + for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After', + 'Vary', 'Content-Encoding', 'Content-Length', 'Expires', + 'Content-Location', 'Content-MD5', 'Last-Modified']: + if key in respheaders: + del respheaders[key] + + if status != 416: + # A server sending a response with status code 416 (Requested + # range not satisfiable) SHOULD include a Content-Range field + # with a byte-range-resp-spec of "*". The instance-length + # specifies the current length of the selected resource. + # A response with status code 206 (Partial Content) MUST NOT + # include a Content-Range field with a byte-range- resp-spec of "*". + if 'Content-Range' in respheaders: + del respheaders['Content-Range'] + + +class HTTPError(CherryPyException): + + """Exception used to return an HTTP error code (4xx-5xx) to the client. + + This exception can be used to automatically send a response using a + http status code, with an appropriate error page. It takes an optional + ``status`` argument (which must be between 400 and 599); it defaults to 500 + ("Internal Server Error"). It also takes an optional ``message`` argument, + which will be returned in the response body. See + `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_ + for a complete list of available error codes and when to use them. + + Examples:: + + raise cherrypy.HTTPError(403) + raise cherrypy.HTTPError( + "403 Forbidden", "You are not allowed to access this resource.") + """ + + status = None + """The HTTP status code. May be of type int or str (with a Reason-Phrase). + """ + + code = None + """The integer HTTP status code.""" + + reason = None + """The HTTP Reason-Phrase string.""" + + def __init__(self, status=500, message=None): + self.status = status + try: + self.code, self.reason, defaultmsg = _httputil.valid_status(status) + except ValueError: + raise self.__class__(500, _exc_info()[1].args[0]) + + if self.code < 400 or self.code > 599: + raise ValueError('status must be between 400 and 599.') + + # See http://www.python.org/dev/peps/pep-0352/ + # self.message = message + self._message = message or defaultmsg + CherryPyException.__init__(self, status, message) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPError object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + + clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + tb = None + if cherrypy.serving.request.show_tracebacks: + tb = format_exc() + + response.headers.pop('Content-Length', None) + + content = self.get_error_page(self.status, traceback=tb, + message=self._message) + response.body = content + + _be_ie_unfriendly(self.code) + + def get_error_page(self, *args, **kwargs): + return get_error_page(*args, **kwargs) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + @classmethod + @contextlib.contextmanager + def handle(cls, exception, status=500, message=''): + """Translate exception into an HTTPError.""" + try: + yield + except exception as exc: + raise cls(status, message or str(exc)) + + +class NotFound(HTTPError): + + """Exception raised when a URL could not be mapped to any handler (404). + + This is equivalent to raising + :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`. + """ + + def __init__(self, path=None): + if path is None: + request = cherrypy.serving.request + path = request.script_name + request.path_info + self.args = (path,) + HTTPError.__init__(self, 404, "The path '%s' was not found." % path) + + +_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC +"-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta> + <title>%(status)s</title> + <style type="text/css"> + #powered_by { + margin-top: 20px; + border-top: 2px solid black; + font-style: italic; + } + + #traceback { + color: red; + } + </style> +</head> + <body> + <h2>%(status)s</h2> + <p>%(message)s</p> + <pre id="traceback">%(traceback)s</pre> + <div id="powered_by"> + <span> + Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a> + </span> + </div> + </body> +</html> +''' + + +def get_error_page(status, **kwargs): + """Return an HTML page, containing a pretty error response. + + status should be an int or a str. + kwargs will be interpolated into the page template. + """ + try: + code, reason, message = _httputil.valid_status(status) + except ValueError: + raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) + + # We can't use setdefault here, because some + # callers send None for kwarg values. + if kwargs.get('status') is None: + kwargs['status'] = '%s %s' % (code, reason) + if kwargs.get('message') is None: + kwargs['message'] = message + if kwargs.get('traceback') is None: + kwargs['traceback'] = '' + if kwargs.get('version') is None: + kwargs['version'] = cherrypy.__version__ + + for k, v in six.iteritems(kwargs): + if v is None: + kwargs[k] = '' + else: + kwargs[k] = escape_html(kwargs[k]) + + # Use a custom template or callable for the error page? + pages = cherrypy.serving.request.error_page + error_page = pages.get(code) or pages.get('default') + + # Default template, can be overridden below. + template = _HTTPErrorTemplate + if error_page: + try: + if hasattr(error_page, '__call__'): + # The caller function may be setting headers manually, + # so we delegate to it completely. We may be returning + # an iterator as well as a string here. + # + # We *must* make sure any content is not unicode. + result = error_page(**kwargs) + if cherrypy.lib.is_iterator(result): + from cherrypy.lib.encoding import UTF8StreamEncoder + return UTF8StreamEncoder(result) + elif isinstance(result, six.text_type): + return result.encode('utf-8') + else: + if not isinstance(result, bytes): + raise ValueError( + 'error page function did not ' + 'return a bytestring, six.text_type or an ' + 'iterator - returned object of type %s.' + % (type(result).__name__)) + return result + else: + # Load the template from this path. + template = io.open(error_page, newline='').read() + except Exception: + e = _format_exception(*_exc_info())[-1] + m = kwargs['message'] + if m: + m += '<br />' + m += 'In addition, the custom error page failed:\n<br />%s' % e + kwargs['message'] = m + + response = cherrypy.serving.response + response.headers['Content-Type'] = 'text/html;charset=utf-8' + result = template % kwargs + return result.encode('utf-8') + + +_ie_friendly_error_sizes = { + 400: 512, 403: 256, 404: 512, 405: 256, + 406: 512, 408: 512, 409: 512, 410: 256, + 500: 512, 501: 512, 505: 512, +} + + +def _be_ie_unfriendly(status): + response = cherrypy.serving.response + + # For some statuses, Internet Explorer 5+ shows "friendly error + # messages" instead of our response.body if the body is smaller + # than a given size. Fix this by returning a body over that size + # (by adding whitespace). + # See http://support.microsoft.com/kb/q218155/ + s = _ie_friendly_error_sizes.get(status, 0) + if s: + s += 1 + # Since we are issuing an HTTP error status, we assume that + # the entity is short, and we should just collapse it. + content = response.collapse_body() + content_length = len(content) + if content_length and content_length < s: + # IN ADDITION: the response must be written to IE + # in one chunk or it will still get replaced! Bah. + content = content + (b' ' * (s - content_length)) + response.body = content + response.headers['Content-Length'] = str(len(content)) + + +def format_exc(exc=None): + """Return exc (or sys.exc_info if None), formatted.""" + try: + if exc is None: + exc = _exc_info() + if exc == (None, None, None): + return '' + import traceback + return ''.join(traceback.format_exception(*exc)) + finally: + del exc + + +def bare_error(extrabody=None): + """Produce status, headers, body for a critical error. + + Returns a triple without calling any other questionable functions, + so it should be as error-free as possible. Call it from an HTTP server + if you get errors outside of the request. + + If extrabody is None, a friendly but rather unhelpful error message + is set in the body. If extrabody is a string, it will be appended + as-is to the body. + """ + + # The whole point of this function is to be a last line-of-defense + # in handling errors. That is, it must not raise any errors itself; + # it cannot be allowed to fail. Therefore, don't add to it! + # In particular, don't call any other CP functions. + + body = b'Unrecoverable error in the server.' + if extrabody is not None: + if not isinstance(extrabody, bytes): + extrabody = extrabody.encode('utf-8') + body += b'\n' + extrabody + + return (b'500 Internal Server Error', + [(b'Content-Type', b'text/plain'), + (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))], + [body]) diff --git a/libraries/cherrypy/_cplogging.py b/libraries/cherrypy/_cplogging.py new file mode 100644 index 00000000..53b9addb --- /dev/null +++ b/libraries/cherrypy/_cplogging.py @@ -0,0 +1,482 @@ +""" +Simple config +============= + +Although CherryPy uses the :mod:`Python logging module <logging>`, it does so +behind the scenes so that simple logging is simple, but complicated logging +is still possible. "Simple" logging means that you can log to the screen +(i.e. console/stdout) or to a file, and that you can easily have separate +error and access log files. + +Here are the simplified logging settings. You use these by adding lines to +your config file or dict. You should set these at either the global level or +per application (see next), but generally not both. + + * ``log.screen``: Set this to True to have both "error" and "access" messages + printed to stdout. + * ``log.access_file``: Set this to an absolute filename where you want + "access" messages written. + * ``log.error_file``: Set this to an absolute filename where you want "error" + messages written. + +Many events are automatically logged; to log your own application events, call +:func:`cherrypy.log`. + +Architecture +============ + +Separate scopes +--------------- + +CherryPy provides log managers at both the global and application layers. +This means you can have one set of logging rules for your entire site, +and another set of rules specific to each application. The global log +manager is found at :func:`cherrypy.log`, and the log manager for each +application is found at :attr:`app.log<cherrypy._cptree.Application.log>`. +If you're inside a request, the latter is reachable from +``cherrypy.request.app.log``; if you're outside a request, you'll have to +obtain a reference to the ``app``: either the return value of +:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used +:func:`quickstart()<cherrypy.quickstart>` instead, via +``cherrypy.tree.apps['/']``. + +By default, the global logs are named "cherrypy.error" and "cherrypy.access", +and the application logs are named "cherrypy.error.2378745" and +"cherrypy.access.2378745" (the number is the id of the Application object). +This means that the application logs "bubble up" to the site logs, so if your +application has no log handlers, the site-level handlers will still log the +messages. + +Errors vs. Access +----------------- + +Each log manager handles both "access" messages (one per HTTP request) and +"error" messages (everything else). Note that the "error" log is not just for +errors! The format of access messages is highly formalized, but the error log +isn't--it receives messages from a variety of sources (including full error +tracebacks, if enabled). + +If you are logging the access log and error log to the same source, then there +is a possibility that a specially crafted error message may replicate an access +log message as described in CWE-117. In this case it is the application +developer's responsibility to manually escape data before +using CherryPy's log() +functionality, or they may create an application that is vulnerable to CWE-117. +This would be achieved by using a custom handler escape any special characters, +and attached as described below. + +Custom Handlers +=============== + +The simple settings above work by manipulating Python's standard :mod:`logging` +module. So when you need something more complex, the full power of the standard +module is yours to exploit. You can borrow or create custom handlers, formats, +filters, and much more. Here's an example that skips the standard FileHandler +and uses a RotatingFileHandler instead: + +:: + + #python + log = app.log + + # Remove the default FileHandlers if present. + log.error_file = "" + log.access_file = "" + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + fname = getattr(log, "rot_error_file", "error.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + fname = getattr(log, "rot_access_file", "access.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.access_log.addHandler(h) + + +The ``rot_*`` attributes are pulled straight from the application log object. +Since "log.*" config entries simply set attributes on the log object, you can +add custom attributes to your heart's content. Note that these handlers are +used ''instead'' of the default, simple handlers outlined above (so don't set +the "log.error_file" config entry, for example). +""" + +import datetime +import logging +import os +import sys + +import six + +import cherrypy +from cherrypy import _cperror + + +# Silence the no-handlers "warning" (stderr write!) in stdlib logging +logging.Logger.manager.emittedNoHandlerWarning = 1 +logfmt = logging.Formatter('%(message)s') + + +class NullHandler(logging.Handler): + + """A no-op logging handler to silence the logging.lastResort handler.""" + + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + +class LogManager(object): + + """An object to assist both simple and advanced logging. + + ``cherrypy.log`` is an instance of this class. + """ + + appid = None + """The id() of the Application object which owns this log manager. If this + is a global log manager, appid is None.""" + + error_log = None + """The actual :class:`logging.Logger` instance for error messages.""" + + access_log = None + """The actual :class:`logging.Logger` instance for access messages.""" + + access_log_format = ( + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' + if six.PY3 else + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + ) + + logger_root = None + """The "top-level" logger name. + + This string will be used as the first segment in the Logger names. + The default is "cherrypy", for example, in which case the Logger names + will be of the form:: + + cherrypy.error.<appid> + cherrypy.access.<appid> + """ + + def __init__(self, appid=None, logger_root='cherrypy'): + self.logger_root = logger_root + self.appid = appid + if appid is None: + self.error_log = logging.getLogger('%s.error' % logger_root) + self.access_log = logging.getLogger('%s.access' % logger_root) + else: + self.error_log = logging.getLogger( + '%s.error.%s' % (logger_root, appid)) + self.access_log = logging.getLogger( + '%s.access.%s' % (logger_root, appid)) + self.error_log.setLevel(logging.INFO) + self.access_log.setLevel(logging.INFO) + + # Silence the no-handlers "warning" (stderr write!) in stdlib logging + self.error_log.addHandler(NullHandler()) + self.access_log.addHandler(NullHandler()) + + cherrypy.engine.subscribe('graceful', self.reopen_files) + + def reopen_files(self): + """Close and reopen all file handlers.""" + for log in (self.error_log, self.access_log): + for h in log.handlers: + if isinstance(h, logging.FileHandler): + h.acquire() + h.stream.close() + h.stream = open(h.baseFilename, h.mode) + h.release() + + def error(self, msg='', context='', severity=logging.INFO, + traceback=False): + """Write the given ``msg`` to the error log. + + This is not just for errors! Applications may call this at any time + to log application-specific information. + + If ``traceback`` is True, the traceback of the current exception + (if any) will be appended to ``msg``. + """ + exc_info = None + if traceback: + exc_info = _cperror._exc_info() + + self.error_log.log( + severity, + ' '.join((self.time(), context, msg)), + exc_info=exc_info, + ) + + def __call__(self, *args, **kwargs): + """An alias for ``error``.""" + return self.error(*args, **kwargs) + + def access(self): + """Write to the access log (in Apache/NCSA Combined Log format). + + See the + `apache documentation + <http://httpd.apache.org/docs/current/logs.html#combined>`_ + for format details. + + CherryPy calls this automatically for you. Note there are no arguments; + it collects the data itself from + :class:`cherrypy.request<cherrypy._cprequest.Request>`. + + Like Apache started doing in 2.0.46, non-printable and other special + characters in %r (and we expand that to all parts) are escaped using + \\xhh sequences, where hh stands for the hexadecimal representation + of the raw byte. Exceptions from this rule are " and \\, which are + escaped by prepending a backslash, and all whitespace characters, + which are written in their C-style notation (\\n, \\t, etc). + """ + request = cherrypy.serving.request + remote = request.remote + response = cherrypy.serving.response + outheaders = response.headers + inheaders = request.headers + if response.output_status is None: + status = '-' + else: + status = response.output_status.split(b' ', 1)[0] + if six.PY3: + status = status.decode('ISO-8859-1') + + atoms = {'h': remote.name or remote.ip, + 'l': '-', + 'u': getattr(request, 'login', None) or '-', + 't': self.time(), + 'r': request.request_line, + 's': status, + 'b': dict.get(outheaders, 'Content-Length', '') or '-', + 'f': dict.get(inheaders, 'Referer', ''), + 'a': dict.get(inheaders, 'User-Agent', ''), + 'o': dict.get(inheaders, 'Host', '-'), + 'i': request.unique_id, + 'z': LazyRfc3339UtcTime(), + } + if six.PY3: + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v + + try: + self.access_log.log( + logging.INFO, self.access_log_format.format(**atoms)) + except Exception: + self(traceback=True) + else: + for k, v in atoms.items(): + if isinstance(v, six.text_type): + v = v.encode('utf8') + elif not isinstance(v, str): + v = str(v) + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[1:-1] + # Escape double-quote. + atoms[k] = v.replace('"', '\\"') + + try: + self.access_log.log( + logging.INFO, self.access_log_format % atoms) + except Exception: + self(traceback=True) + + def time(self): + """Return now() in Apache Common Log Format (no timezone).""" + now = datetime.datetime.now() + monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + month = monthnames[now.month - 1].capitalize() + return ('[%02d/%s/%04d:%02d:%02d:%02d]' % + (now.day, month, now.year, now.hour, now.minute, now.second)) + + def _get_builtin_handler(self, log, key): + for h in log.handlers: + if getattr(h, '_cpbuiltin', None) == key: + return h + + # ------------------------- Screen handlers ------------------------- # + def _set_screen_handler(self, log, enable, stream=None): + h = self._get_builtin_handler(log, 'screen') + if enable: + if not h: + if stream is None: + stream = sys.stderr + h = logging.StreamHandler(stream) + h.setFormatter(logfmt) + h._cpbuiltin = 'screen' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def screen(self): + """Turn stderr/stdout logging on or off. + + If you set this to True, it'll add the appropriate StreamHandler for + you. If you set it to False, it will remove the handler. + """ + h = self._get_builtin_handler + has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') + return bool(has_h) + + @screen.setter + def screen(self, newvalue): + self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) + self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) + + # -------------------------- File handlers -------------------------- # + + def _add_builtin_file_handler(self, log, fname): + h = logging.FileHandler(fname) + h.setFormatter(logfmt) + h._cpbuiltin = 'file' + log.addHandler(h) + + def _set_file_handler(self, log, filename): + h = self._get_builtin_handler(log, 'file') + if filename: + if h: + if h.baseFilename != os.path.abspath(filename): + h.close() + log.handlers.remove(h) + self._add_builtin_file_handler(log, filename) + else: + self._add_builtin_file_handler(log, filename) + else: + if h: + h.close() + log.handlers.remove(h) + + @property + def error_file(self): + """The filename for self.error_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.error_log, 'file') + if h: + return h.baseFilename + return '' + + @error_file.setter + def error_file(self, newvalue): + self._set_file_handler(self.error_log, newvalue) + + @property + def access_file(self): + """The filename for self.access_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.access_log, 'file') + if h: + return h.baseFilename + return '' + + @access_file.setter + def access_file(self, newvalue): + self._set_file_handler(self.access_log, newvalue) + + # ------------------------- WSGI handlers ------------------------- # + + def _set_wsgi_handler(self, log, enable): + h = self._get_builtin_handler(log, 'wsgi') + if enable: + if not h: + h = WSGIErrorHandler() + h.setFormatter(logfmt) + h._cpbuiltin = 'wsgi' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def wsgi(self): + """Write errors to wsgi.errors. + + If you set this to True, it'll add the appropriate + :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you + (which writes errors to ``wsgi.errors``). + If you set it to False, it will remove the handler. + """ + return bool(self._get_builtin_handler(self.error_log, 'wsgi')) + + @wsgi.setter + def wsgi(self, newvalue): + self._set_wsgi_handler(self.error_log, newvalue) + + +class WSGIErrorHandler(logging.Handler): + + "A handler class which writes logging records to environ['wsgi.errors']." + + def flush(self): + """Flushes the stream.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + stream.flush() + + def emit(self, record): + """Emit a record.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + try: + msg = self.format(record) + fs = '%s\n' + import types + # if no unicode support... + if not hasattr(types, 'UnicodeType'): + stream.write(fs % msg) + else: + try: + stream.write(fs % msg) + except UnicodeError: + stream.write(fs % msg.encode('UTF-8')) + self.flush() + except Exception: + self.handleError(record) + + +class LazyRfc3339UtcTime(object): + def __str__(self): + """Return now() in RFC3339 UTC Format.""" + now = datetime.datetime.now() + return now.isoformat('T') + 'Z' diff --git a/libraries/cherrypy/_cpmodpy.py b/libraries/cherrypy/_cpmodpy.py new file mode 100644 index 00000000..ac91e625 --- /dev/null +++ b/libraries/cherrypy/_cpmodpy.py @@ -0,0 +1,356 @@ +"""Native adapter for serving CherryPy via mod_python + +Basic usage: + +########################################## +# Application in a module called myapp.py +########################################## + +import cherrypy + +class Root: + @cherrypy.expose + def index(self): + return 'Hi there, Ho there, Hey there' + + +# We will use this method from the mod_python configuration +# as the entry point to our application +def setup_server(): + cherrypy.tree.mount(Root()) + cherrypy.config.update({'environment': 'production', + 'log.screen': False, + 'show_tracebacks': False}) + +########################################## +# mod_python settings for apache2 +# This should reside in your httpd.conf +# or a file that will be loaded at +# apache startup +########################################## + +# Start +DocumentRoot "/" +Listen 8080 +LoadModule python_module /usr/lib/apache2/modules/mod_python.so + +<Location "/"> + PythonPath "sys.path+['/path/to/my/application']" + SetHandler python-program + PythonHandler cherrypy._cpmodpy::handler + PythonOption cherrypy.setup myapp::setup_server + PythonDebug On +</Location> +# End + +The actual path to your mod_python.so is dependent on your +environment. In this case we suppose a global mod_python +installation on a Linux distribution such as Ubuntu. + +We do set the PythonPath configuration setting so that +your application can be found by from the user running +the apache2 instance. Of course if your application +resides in the global site-package this won't be needed. + +Then restart apache2 and access http://127.0.0.1:8080 +""" + +import io +import logging +import os +import re +import sys + +import six + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +# ------------------------------ Request-handling + + +def setup(req): + from mod_python import apache + + # Run any setup functions defined by a "PythonOption cherrypy.setup" + # directive. + options = req.get_options() + if 'cherrypy.setup' in options: + for function in options['cherrypy.setup'].split(): + atoms = function.split('::', 1) + if len(atoms) == 1: + mod = __import__(atoms[0], globals(), locals()) + else: + modname, fname = atoms + mod = __import__(modname, globals(), locals(), [fname]) + func = getattr(mod, fname) + func() + + cherrypy.config.update({'log.screen': False, + 'tools.ignore_headers.on': True, + 'tools.ignore_headers.headers': ['Range'], + }) + + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.unsubscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.unsubscribe() + engine.autoreload.unsubscribe() + cherrypy.server.unsubscribe() + + @engine.subscribe('log') + def _log(msg, level): + newlevel = apache.APLOG_ERR + if logging.DEBUG >= level: + newlevel = apache.APLOG_DEBUG + elif logging.INFO >= level: + newlevel = apache.APLOG_INFO + elif logging.WARNING >= level: + newlevel = apache.APLOG_WARNING + # On Windows, req.server is required or the msg will vanish. See + # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html + # Also, "When server is not specified...LogLevel does not apply..." + apache.log_error(msg, newlevel, req.server) + + engine.start() + + def cherrypy_cleanup(data): + engine.exit() + try: + # apache.register_cleanup wasn't available until 3.1.4. + apache.register_cleanup(cherrypy_cleanup) + except AttributeError: + req.server.register_cleanup(req, cherrypy_cleanup) + + +class _ReadOnlyRequest: + expose = ('read', 'readline', 'readlines') + + def __init__(self, req): + for method in self.expose: + self.__dict__[method] = getattr(req, method) + + +recursive = False + +_isSetUp = False + + +def handler(req): + from mod_python import apache + try: + global _isSetUp + if not _isSetUp: + setup(req) + _isSetUp = True + + # Obtain a Request object from CherryPy + local = req.connection.local_addr + local = httputil.Host( + local[0], local[1], req.connection.local_host or '') + remote = req.connection.remote_addr + remote = httputil.Host( + remote[0], remote[1], req.connection.remote_host or '') + + scheme = req.parsed_uri[0] or 'http' + req.get_basic_auth_pw() + + try: + # apache.mpm_query only became available in mod_python 3.1 + q = apache.mpm_query + threaded = q(apache.AP_MPMQ_IS_THREADED) + forked = q(apache.AP_MPMQ_IS_FORKED) + except AttributeError: + bad_value = ("You must provide a PythonOption '%s', " + "either 'on' or 'off', when running a version " + 'of mod_python < 3.1') + + options = req.get_options() + + threaded = options.get('multithread', '').lower() + if threaded == 'on': + threaded = True + elif threaded == 'off': + threaded = False + else: + raise ValueError(bad_value % 'multithread') + + forked = options.get('multiprocess', '').lower() + if forked == 'on': + forked = True + elif forked == 'off': + forked = False + else: + raise ValueError(bad_value % 'multiprocess') + + sn = cherrypy.tree.script_name(req.uri or '/') + if sn is None: + send_response(req, '404 Not Found', [], '') + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.uri + qs = req.args or '' + reqproto = req.protocol + headers = list(six.iteritems(req.headers_in)) + rfile = _ReadOnlyRequest(req) + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving(local, remote, scheme, + 'HTTP/1.1') + request.login = req.user + request.multithread = bool(threaded) + request.multiprocess = bool(forked) + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, reqproto, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same URL ' + 'twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + send_response( + req, response.output_status, response.header_list, + response.body, response.stream) + finally: + app.release_serving() + except Exception: + tb = format_exc() + cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) + s, h, b = bare_error() + send_response(req, s, h, b) + return apache.OK + + +def send_response(req, status, headers, body, stream=False): + # Set response status + req.status = int(status[:3]) + + # Set response headers + req.content_type = 'text/plain' + for header, value in headers: + if header.lower() == 'content-type': + req.content_type = value + continue + req.headers_out.add(header, value) + + if stream: + # Flush now so the status and headers are sent immediately. + req.flush() + + # Set response body + for seg in always_iterable(body): + req.write(seg) + + +# --------------- Startup tools for CherryPy + mod_python --------------- # +try: + import subprocess + + def popen(fullcmd): + p = subprocess.Popen(fullcmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=True) + return p.stdout +except ImportError: + def popen(fullcmd): + pipein, pipeout = os.popen4(fullcmd) + return pipeout + + +def read_process(cmd, args=''): + fullcmd = '%s %s' % (cmd, args) + pipeout = popen(fullcmd) + try: + firstline = pipeout.readline() + cmd_not_found = re.search( + b'(not recognized|No such file|not found)', + firstline, + re.IGNORECASE + ) + if cmd_not_found: + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +class ModPythonServer(object): + + template = """ +# Apache2 server configuration file for running CherryPy with mod_python. + +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +<Location %(loc)s> + SetHandler python-program + PythonHandler %(handler)s + PythonDebug On +%(opts)s +</Location> +""" + + def __init__(self, loc='/', port=80, opts=None, apache_path='apache', + handler='cherrypy._cpmodpy::handler'): + self.loc = loc + self.port = port + self.opts = opts + self.apache_path = apache_path + self.handler = handler + + def start(self): + opts = ''.join([' PythonOption %s %s\n' % (k, v) + for k, v in self.opts]) + conf_data = self.template % {'port': self.port, + 'loc': self.loc, + 'opts': opts, + 'handler': self.handler, + } + + mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') + f = open(mpconf, 'wb') + try: + f.write(conf_data) + finally: + f.close() + + response = read_process(self.apache_path, '-k start -f %s' % mpconf) + self.ready = True + return response + + def stop(self): + os.popen('apache -k stop') + self.ready = False diff --git a/libraries/cherrypy/_cpnative_server.py b/libraries/cherrypy/_cpnative_server.py new file mode 100644 index 00000000..55653c35 --- /dev/null +++ b/libraries/cherrypy/_cpnative_server.py @@ -0,0 +1,160 @@ +"""Native adapter for serving CherryPy via its builtin server.""" + +import logging +import sys +import io + +import cheroot.server + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +class NativeGateway(cheroot.server.Gateway): + """Native gateway implementation allowing to bypass WSGI.""" + + recursive = False + + def respond(self): + """Obtain response from CherryPy machinery and then send it.""" + req = self.req + try: + # Obtain a Request object from CherryPy + local = req.server.bind_addr + local = httputil.Host(local[0], local[1], '') + remote = req.conn.remote_addr, req.conn.remote_port + remote = httputil.Host(remote[0], remote[1], '') + + scheme = req.scheme + sn = cherrypy.tree.script_name(req.uri or '/') + if sn is None: + self.send_response('404 Not Found', [], ['']) + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.path + qs = req.qs or '' + headers = req.inheaders.items() + rfile = req.rfile + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving( + local, remote, scheme, 'HTTP/1.1') + request.multithread = True + request.multiprocess = False + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the + # response + try: + request.run(method, path, qs, + req.request_protocol, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not self.recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same ' + 'URL twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + self.send_response( + response.output_status, response.header_list, + response.body) + finally: + app.release_serving() + except Exception: + tb = format_exc() + # print tb + cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) + s, h, b = bare_error() + self.send_response(s, h, b) + + def send_response(self, status, headers, body): + """Send response to HTTP request.""" + req = self.req + + # Set response status + req.status = status or b'500 Server Error' + + # Set response headers + for header, value in headers: + req.outheaders.append((header, value)) + if (req.ready and not req.sent_headers): + req.sent_headers = True + req.send_headers() + + # Set response body + for seg in body: + req.write(seg) + + +class CPHTTPServer(cheroot.server.HTTPServer): + """Wrapper for cheroot.server.HTTPServer. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. + Therefore, we wrap it here, so we can apply some attributes + from config -> cherrypy.server -> HTTPServer. + """ + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPHTTPServer.""" + self.server_adapter = server_adapter + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + cheroot.server.HTTPServer.__init__( + self, server_adapter.bind_addr, NativeGateway, + minthreads=server_adapter.thread_pool, + maxthreads=server_adapter.thread_pool_max, + server_name=server_name) + + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0) + self.request_queue_size = self.server_adapter.socket_queue_size + self.timeout = self.server_adapter.socket_timeout + self.shutdown_timeout = self.server_adapter.shutdown_timeout + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) diff --git a/libraries/cherrypy/_cpreqbody.py b/libraries/cherrypy/_cpreqbody.py new file mode 100644 index 00000000..893fe5f5 --- /dev/null +++ b/libraries/cherrypy/_cpreqbody.py @@ -0,0 +1,1000 @@ +"""Request body processing for CherryPy. + +.. versionadded:: 3.2 + +Application authors have complete control over the parsing of HTTP request +entities. In short, +:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>` +is now always set to an instance of +:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`, +and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`. + +When an HTTP request includes an entity body, it is often desirable to +provide that information to applications in a form other than the raw bytes. +Different content types demand different approaches. Examples: + + * For a GIF file, we want the raw bytes in a stream. + * An HTML form is better parsed into its component fields, and each text field + decoded from bytes to unicode. + * A JSON body should be deserialized into a Python dict or list. + +When the request contains a Content-Type header, the media type is used as a +key to look up a value in the +:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict. +If the full media +type is not found, then the major type is tried; for example, if no processor +is found for the 'image/jpeg' type, then we look for a processor for the +'image' types altogether. If neither the full type nor the major type has a +matching processor, then a default processor is used +(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most +types, this means no processing is done, and the body is left unread as a +raw byte stream. Processors are configurable in an 'on_start_resource' hook. + +Some processors, especially those for the 'text' types, attempt to decode bytes +to unicode. If the Content-Type request header includes a 'charset' parameter, +this is used to decode the entity. Otherwise, one or more default charsets may +be attempted, although this decision is up to each processor. If a processor +successfully decodes an Entity or Part, it should set the +:attr:`charset<cherrypy._cpreqbody.Entity.charset>` attribute +on the Entity or Part to the name of the successful charset, so that +applications can easily re-encode or transcode the value if they wish. + +If the Content-Type of the request entity is of major type 'multipart', then +the above parsing process, and possibly a decoding process, is performed for +each part. + +For both the full entity and multipart parts, a Content-Disposition header may +be used to fill :attr:`name<cherrypy._cpreqbody.Entity.name>` and +:attr:`filename<cherrypy._cpreqbody.Entity.filename>` attributes on the +request.body or the Part. + +.. _custombodyprocessors: + +Custom Processors +================= + +You can add your own processors for any specific or major MIME type. Simply add +it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a +hook/tool that runs at ``on_start_resource`` or ``before_request_body``. +Here's the built-in JSON tool for an example:: + + def json_in(force=True, debug=False): + request = cherrypy.serving.request + def json_processor(entity): + '''Read application/json data into request.json.''' + if not entity.headers.get("Content-Length", ""): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + request.json = json_decode(body) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + if force: + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an application/json content type') + request.body.processors['application/json'] = json_processor + +We begin by defining a new ``json_processor`` function to stick in the +``processors`` dictionary. All processor functions take a single argument, +the ``Entity`` instance they are to process. It will be called whenever a +request is received (for those URI's where the tool is turned on) which +has a ``Content-Type`` of "application/json". + +First, it checks for a valid ``Content-Length`` (raising 411 if not valid), +then reads the remaining bytes on the socket. The ``fp`` object knows its +own length, so it won't hang waiting for data that never arrives. It will +return when all data has been read. Then, we decode those bytes using +Python's built-in ``json`` module, and stick the decoded result onto +``request.json`` . If it cannot be decoded, we raise 400. + +If the "force" argument is True (the default), the ``Tool`` clears the +``processors`` dict so that request entities of other ``Content-Types`` +aren't parsed at all. Since there's no entry for those invalid MIME +types, the ``default_proc`` method of ``cherrypy.request.body`` is +called. But this does nothing by default (usually to provide the page +handler an opportunity to handle it.) +But in our case, we want to raise 415, so we replace +``request.body.default_proc`` +with the error (``HTTPError`` instances, when called, raise themselves). + +If we were defining a custom processor, we can do so without making a ``Tool``. +Just add the config entry:: + + request.body.processors = {'application/json': json_processor} + +Note that you can only replace the ``processors`` dict wholesale this way, +not update the existing one. +""" + +try: + from io import DEFAULT_BUFFER_SIZE +except ImportError: + DEFAULT_BUFFER_SIZE = 8192 +import re +import sys +import tempfile +try: + from urllib import unquote_plus +except ImportError: + def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(b'+', b' ') + atoms = bs.split(b'%') + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return b''.join(atoms) + +import six +import cheroot.server + +import cherrypy +from cherrypy._cpcompat import ntou, unquote +from cherrypy.lib import httputil + + +# ------------------------------- Processors -------------------------------- # + +def process_urlencoded(entity): + """Read application/x-www-form-urlencoded data into entity.params.""" + qs = entity.fp.read() + for charset in entity.attempt_charsets: + try: + params = {} + for aparam in qs.split(b'&'): + for pair in aparam.split(b';'): + if not pair: + continue + + atoms = pair.split(b'=', 1) + if len(atoms) == 1: + atoms.append(b'') + + key = unquote_plus(atoms[0]).decode(charset) + value = unquote_plus(atoms[1]).decode(charset) + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + except UnicodeDecodeError: + pass + else: + entity.charset = charset + break + else: + raise cherrypy.HTTPError( + 400, 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(entity.attempt_charsets)) + + # Now that all values have been successfully parsed and decoded, + # apply them to the entity.params dict. + for key, value in params.items(): + if key in entity.params: + if not isinstance(entity.params[key], list): + entity.params[key] = [entity.params[key]] + entity.params[key].append(value) + else: + entity.params[key] = value + + +def process_multipart(entity): + """Read all multipart parts into entity.parts.""" + ib = '' + if 'boundary' in entity.content_type.params: + # http://tools.ietf.org/html/rfc2046#section-5.1.1 + # "The grammar for parameters on the Content-type field is such that it + # is often necessary to enclose the boundary parameter values in quotes + # on the Content-type line" + ib = entity.content_type.params['boundary'].strip('"') + + if not re.match('^[ -~]{0,200}[!-~]$', ib): + raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) + + ib = ('--' + ib).encode('ascii') + + # Find the first marker + while True: + b = entity.readline() + if not b: + return + + b = b.strip() + if b == ib: + break + + # Read all parts + while True: + part = entity.part_class.from_fp(entity.fp, ib) + entity.parts.append(part) + part.process() + if part.fp.done: + break + + +def process_multipart_form_data(entity): + """Read all multipart/form-data parts into entity.parts or entity.params. + """ + process_multipart(entity) + + kept_parts = [] + for part in entity.parts: + if part.name is None: + kept_parts.append(part) + else: + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if part.name in entity.params: + if not isinstance(entity.params[part.name], list): + entity.params[part.name] = [entity.params[part.name]] + entity.params[part.name].append(value) + else: + entity.params[part.name] = value + + entity.parts = kept_parts + + +def _old_process_multipart(entity): + """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" + process_multipart(entity) + + params = entity.params + + for part in entity.parts: + if part.name is None: + key = ntou('parts') + else: + key = part.name + + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + + +# -------------------------------- Entities --------------------------------- # +class Entity(object): + + """An HTTP request body, or MIME multipart body. + + This class collects information about the HTTP request entity. When a + given entity is of MIME type "multipart", each part is parsed into its own + Entity instance, and the set of parts stored in + :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`. + + Between the ``before_request_body`` and ``before_handler`` tools, CherryPy + tries to process the request body (if any) by calling + :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`. + This uses the ``content_type`` of the Entity to look up a suitable + processor in + :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, + a dict. + If a matching processor cannot be found for the complete Content-Type, + it tries again using the major type. For example, if a request with an + entity of type "image/jpeg" arrives, but no processor can be found for + that complete type, then one is sought for the major type "image". If a + processor is still not found, then the + :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method + of the Entity is called (which does nothing by default; you can + override this too). + + CherryPy includes processors for the "application/x-www-form-urlencoded" + type, the "multipart/form-data" type, and the "multipart" major type. + CherryPy 3.2 processes these types almost exactly as older versions. + Parts are passed as arguments to the page handler using their + ``Content-Disposition.name`` if given, otherwise in a generic "parts" + argument. Each such part is either a string, or the + :class:`Part<cherrypy._cpreqbody.Part>` itself if it's a file. (In this + case it will have ``file`` and ``filename`` attributes, or possibly a + ``value`` attribute). Each Part is itself a subclass of + Entity, and has its own ``process`` method and ``processors`` dict. + + There is a separate processor for the "multipart" major type which is more + flexible, and simply stores all multipart parts in + :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can + enable it with:: + + cherrypy.request.body.processors['multipart'] = \ + _cpreqbody.process_multipart + + in an ``on_start_resource`` tool. + """ + + # http://tools.ietf.org/html/rfc2046#section-4.1.2: + # "The default character set, which must be assumed in the + # absence of a charset parameter, is US-ASCII." + # However, many browsers send data in utf-8 with no charset. + attempt_charsets = ['utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + charset = None + """The successful decoding; see "attempt_charsets" above.""" + + content_type = None + """The value of the Content-Type request header. + + If the Entity is part of a multipart payload, this will be the Content-Type + given in the MIME headers for this part. + """ + + default_content_type = 'application/x-www-form-urlencoded' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part<cherrypy._cpreqbody.Part>`). + """ + + filename = None + """The ``Content-Disposition.filename`` header, if available.""" + + fp = None + """The readable socket file object.""" + + headers = None + """A dict of request/multipart header names and values. + + This is a copy of the ``request.headers`` for the ``request.body``; + for multipart parts, it is the set of headers for that part. + """ + + length = None + """The value of the ``Content-Length`` header, if provided.""" + + name = None + """The "name" parameter of the ``Content-Disposition`` header, if any.""" + + params = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True).""" + + processors = {'application/x-www-form-urlencoded': process_urlencoded, + 'multipart/form-data': process_multipart_form_data, + 'multipart': process_multipart, + } + """A dict of Content-Type names to processor methods.""" + + parts = None + """A list of Part instances if ``Content-Type`` is of major type + "multipart".""" + + part_class = None + """The class used for multipart parts. + + You can replace this with custom subclasses to alter the processing of + multipart parts. + """ + + def __init__(self, fp, headers, params=None, parts=None): + # Make an instance-specific copy of the class processors + # so Tools, etc. can replace them per-request. + self.processors = self.processors.copy() + + self.fp = fp + self.headers = headers + + if params is None: + params = {} + self.params = params + + if parts is None: + parts = [] + self.parts = parts + + # Content-Type + self.content_type = headers.elements('Content-Type') + if self.content_type: + self.content_type = self.content_type[0] + else: + self.content_type = httputil.HeaderElement.from_str( + self.default_content_type) + + # Copy the class 'attempt_charsets', prepending any Content-Type + # charset + dec = self.content_type.params.get('charset', None) + if dec: + self.attempt_charsets = [dec] + [c for c in self.attempt_charsets + if c != dec] + else: + self.attempt_charsets = self.attempt_charsets[:] + + # Length + self.length = None + clen = headers.get('Content-Length', None) + # If Transfer-Encoding is 'chunked', ignore any Content-Length. + if ( + clen is not None and + 'chunked' not in headers.get('Transfer-Encoding', '') + ): + try: + self.length = int(clen) + except ValueError: + pass + + # Content-Disposition + self.name = None + self.filename = None + disp = headers.elements('Content-Disposition') + if disp: + disp = disp[0] + if 'name' in disp.params: + self.name = disp.params['name'] + if self.name.startswith('"') and self.name.endswith('"'): + self.name = self.name[1:-1] + if 'filename' in disp.params: + self.filename = disp.params['filename'] + if ( + self.filename.startswith('"') and + self.filename.endswith('"') + ): + self.filename = self.filename[1:-1] + if 'filename*' in disp.params: + # @see https://tools.ietf.org/html/rfc5987 + encoding, lang, filename = disp.params['filename*'].split("'") + self.filename = unquote(str(filename), encoding) + + def read(self, size=None, fp_out=None): + return self.fp.read(size, fp_out) + + def readline(self, size=None): + return self.fp.readline(size) + + def readlines(self, sizehint=None): + return self.fp.readlines(sizehint) + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def next(self): + return self.__next__() + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read(fp_out=fp_out) + return fp_out + + def make_file(self): + """Return a file-like object into which the request body will be read. + + By default, this will return a TemporaryFile. Override as needed. + See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" + return tempfile.TemporaryFile() + + def fullvalue(self): + """Return this entity as a string, whether stored in a file or not.""" + if self.file: + # It was stored in a tempfile. Read it. + self.file.seek(0) + value = self.file.read() + self.file.seek(0) + else: + value = self.value + value = self.decode_entity(value) + return value + + def decode_entity(self, value): + """Return a given byte encoded value as a string""" + for charset in self.attempt_charsets: + try: + value = value.decode(charset) + except UnicodeDecodeError: + pass + else: + self.charset = charset + return value + else: + raise cherrypy.HTTPError( + 400, + 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(self.attempt_charsets) + ) + + def process(self): + """Execute the best-match processor for the given media type.""" + proc = None + ct = self.content_type.value + try: + proc = self.processors[ct] + except KeyError: + toptype = ct.split('/', 1)[0] + try: + proc = self.processors[toptype] + except KeyError: + pass + if proc is None: + self.default_proc() + else: + proc(self) + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + # Leave the fp alone for someone else to read. This works fine + # for request.body, but the Part subclasses need to override this + # so they can move on to the next part. + pass + + +class Part(Entity): + + """A MIME part entity, part of a multipart entity.""" + + # "The default character set, which must be assumed in the absence of a + # charset parameter, is US-ASCII." + attempt_charsets = ['us-ascii', 'utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + boundary = None + """The MIME multipart boundary.""" + + default_content_type = 'text/plain' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however (this class), + the MIME spec declares that a part with no Content-Type defaults to + "text/plain". + """ + + # This is the default in stdlib cgi. We may want to increase it. + maxrambytes = 1000 + """The threshold of bytes after which point the ``Part`` will store + its data in a file (generated by + :func:`make_file<cherrypy._cprequest.Entity.make_file>`) + instead of a string. Defaults to 1000, just like the :mod:`cgi` + module in Python's standard library. + """ + + def __init__(self, fp, headers, boundary): + Entity.__init__(self, fp, headers) + self.boundary = boundary + self.file = None + self.value = None + + @classmethod + def from_fp(cls, fp, boundary): + headers = cls.read_headers(fp) + return cls(fp, headers, boundary) + + @classmethod + def read_headers(cls, fp): + headers = httputil.HeaderMap() + while True: + line = fp.readline() + if not line: + # No more data--illegal end of headers + raise EOFError('Illegal end of headers.') + + if line == b'\r\n': + # Normal end of headers + break + if not line.endswith(b'\r\n'): + raise ValueError('MIME requires CRLF terminators: %r' % line) + + if line[0] in b' \t': + # It's a continuation line. + v = line.strip().decode('ISO-8859-1') + else: + k, v = line.split(b':', 1) + k = k.strip().decode('ISO-8859-1') + v = v.strip().decode('ISO-8859-1') + + existing = headers.get(k) + if existing: + v = ', '.join((existing, v)) + headers[k] = v + + return headers + + def read_lines_to_boundary(self, fp_out=None): + """Read bytes from self.fp and return or write them to a file. + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and that fp is returned. + """ + endmarker = self.boundary + b'--' + delim = b'' + prev_lf = True + lines = [] + seen = 0 + while True: + line = self.fp.readline(1 << 16) + if not line: + raise EOFError('Illegal end of multipart body.') + if line.startswith(b'--') and prev_lf: + strippedline = line.strip() + if strippedline == self.boundary: + break + if strippedline == endmarker: + self.fp.finish() + break + + line = delim + line + + if line.endswith(b'\r\n'): + delim = b'\r\n' + line = line[:-2] + prev_lf = True + elif line.endswith(b'\n'): + delim = b'\n' + line = line[:-1] + prev_lf = True + else: + delim = b'' + prev_lf = False + + if fp_out is None: + lines.append(line) + seen += len(line) + if seen > self.maxrambytes: + fp_out = self.make_file() + for line in lines: + fp_out.write(line) + else: + fp_out.write(line) + + if fp_out is None: + result = b''.join(lines) + return result + else: + fp_out.seek(0) + return fp_out + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + if self.filename: + # Always read into a file if a .filename was given. + self.file = self.read_into_file() + else: + result = self.read_lines_to_boundary() + if isinstance(result, bytes): + self.value = result + else: + self.file = result + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read_lines_to_boundary(fp_out=fp_out) + return fp_out + + +Entity.part_class = Part + +inf = float('inf') + + +class SizedReader: + + def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, + has_trailers=False): + # Wrap our fp in a buffer so peek() works + self.fp = fp + self.length = length + self.maxbytes = maxbytes + self.buffer = b'' + self.bufsize = bufsize + self.bytes_read = 0 + self.done = False + self.has_trailers = has_trailers + + def read(self, size=None, fp_out=None): + """Read bytes from the request body and return or write them to a file. + + A number of bytes less than or equal to the 'size' argument are read + off the socket. The actual number of bytes read are tracked in + self.bytes_read. The number may be smaller than 'size' when 1) the + client sends fewer bytes, 2) the 'Content-Length' request header + specifies fewer bytes than requested, or 3) the number of bytes read + exceeds self.maxbytes (in which case, 413 is raised). + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and None is returned. + """ + + if self.length is None: + if size is None: + remaining = inf + else: + remaining = size + else: + remaining = self.length - self.bytes_read + if size and size < remaining: + remaining = size + if remaining == 0: + self.finish() + if fp_out is None: + return b'' + else: + return None + + chunks = [] + + # Read bytes from the buffer. + if self.buffer: + if remaining is inf: + data = self.buffer + self.buffer = b'' + else: + data = self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + # Read bytes from the socket. + while remaining > 0: + chunksize = min(remaining, self.bufsize) + try: + data = self.fp.read(chunksize) + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + if not data: + self.finish() + break + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + if fp_out is None: + return b''.join(chunks) + + def readline(self, size=None): + """Read a line from the request body and return it.""" + chunks = [] + while size is None or size > 0: + chunksize = self.bufsize + if size is not None and size < self.bufsize: + chunksize = size + data = self.read(chunksize) + if not data: + break + pos = data.find(b'\n') + 1 + if pos: + chunks.append(data[:pos]) + remainder = data[pos:] + self.buffer += remainder + self.bytes_read -= len(remainder) + break + else: + chunks.append(data) + return b''.join(chunks) + + def readlines(self, sizehint=None): + """Read lines from the request body and return them.""" + if self.length is not None: + if sizehint is None: + sizehint = self.length - self.bytes_read + else: + sizehint = min(sizehint, self.length - self.bytes_read) + + lines = [] + seen = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + seen += len(line) + if seen >= sizehint: + break + return lines + + def finish(self): + self.done = True + if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): + self.trailers = {} + + try: + for line in self.fp.read_trailer_lines(): + if line[0] in b' \t': + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(b':', 1) + except ValueError: + raise ValueError('Illegal header line.') + k = k.strip().title() + v = v.strip() + + if k in cheroot.server.comma_separated_headers: + existing = self.trailers.get(k) + if existing: + v = b', '.join((existing, v)) + self.trailers[k] = v + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + + +class RequestBody(Entity): + + """The entity of the HTTP request.""" + + bufsize = 8 * 1024 + """The buffer size used when reading the socket.""" + + # Don't parse the request body at all if the client didn't provide + # a Content-Type header. See + # https://github.com/cherrypy/cherrypy/issues/790 + default_content_type = '' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part<cherrypy._cpreqbody.Part>`). + """ + + maxbytes = None + """Raise ``MaxSizeExceeded`` if more bytes than this are read from + the socket. + """ + + def __init__(self, fp, headers, params=None, request_params=None): + Entity.__init__(self, fp, headers, params) + + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 + # When no explicit charset parameter is provided by the + # sender, media subtypes of the "text" type are defined + # to have a default charset value of "ISO-8859-1" when + # received via HTTP. + if self.content_type.value.startswith('text/'): + for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): + if c in self.attempt_charsets: + break + else: + self.attempt_charsets.append('ISO-8859-1') + + # Temporary fix while deprecating passing .parts as .params. + self.processors['multipart'] = _old_process_multipart + + if request_params is None: + request_params = {} + self.request_params = request_params + + def process(self): + """Process the request entity based on its Content-Type.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # It is possible to send a POST request with no body, for example; + # however, app developers are responsible in that case to set + # cherrypy.request.process_body to False so this method isn't called. + h = cherrypy.serving.request.headers + if 'Content-Length' not in h and 'Transfer-Encoding' not in h: + raise cherrypy.HTTPError(411) + + self.fp = SizedReader(self.fp, self.length, + self.maxbytes, bufsize=self.bufsize, + has_trailers='Trailer' in h) + super(RequestBody, self).process() + + # Body params should also be a part of the request_params + # add them in here. + request_params = self.request_params + for key, value in self.params.items(): + # Python 2 only: keyword arguments must be byte strings (type + # 'str'). + if sys.version_info < (3, 0): + if isinstance(key, six.text_type): + key = key.encode('ISO-8859-1') + + if key in request_params: + if not isinstance(request_params[key], list): + request_params[key] = [request_params[key]] + request_params[key].append(value) + else: + request_params[key] = value diff --git a/libraries/cherrypy/_cprequest.py b/libraries/cherrypy/_cprequest.py new file mode 100644 index 00000000..3cc0c811 --- /dev/null +++ b/libraries/cherrypy/_cprequest.py @@ -0,0 +1,930 @@ +import sys +import time + +import uuid + +import six +from six.moves.http_cookies import SimpleCookie, CookieError + +from more_itertools import consume + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy import _cpreqbody +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil, reprconf, encoding + + +class Hook(object): + + """A callback and its metadata: failsafe, priority, and kwargs.""" + + callback = None + """ + The bare callable that this Hook object is wrapping, which will + be called when the Hook is called.""" + + failsafe = False + """ + If True, the callback is guaranteed to run even if other callbacks + from the same call point raise exceptions.""" + + priority = 50 + """ + Defines the order of execution for a list of Hooks. Priority numbers + should be limited to the closed interval [0, 100], but values outside + this range are acceptable, as are fractional values.""" + + kwargs = {} + """ + A set of keyword arguments that will be passed to the + callable on each call.""" + + def __init__(self, callback, failsafe=None, priority=None, **kwargs): + self.callback = callback + + if failsafe is None: + failsafe = getattr(callback, 'failsafe', False) + self.failsafe = failsafe + + if priority is None: + priority = getattr(callback, 'priority', 50) + self.priority = priority + + self.kwargs = kwargs + + def __lt__(self, other): + """ + Hooks sort by priority, ascending, such that + hooks of lower priority are run first. + """ + return self.priority < other.priority + + def __call__(self): + """Run self.callback(**self.kwargs).""" + return self.callback(**self.kwargs) + + def __repr__(self): + cls = self.__class__ + return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)' + % (cls.__module__, cls.__name__, self.callback, + self.failsafe, self.priority, + ', '.join(['%s=%r' % (k, v) + for k, v in self.kwargs.items()]))) + + +class HookMap(dict): + + """A map of call points to lists of callbacks (Hook objects).""" + + def __new__(cls, points=None): + d = dict.__new__(cls) + for p in points or []: + d[p] = [] + return d + + def __init__(self, *a, **kw): + pass + + def attach(self, point, callback, failsafe=None, priority=None, **kwargs): + """Append a new Hook made from the supplied arguments.""" + self[point].append(Hook(callback, failsafe, priority, **kwargs)) + + def run(self, point): + """Execute all registered Hooks (callbacks) for the given point.""" + exc = None + hooks = self[point] + hooks.sort() + for hook in hooks: + # Some hooks are guaranteed to run even if others at + # the same hookpoint fail. We will still log the failure, + # but proceed on to the next hook. The only way + # to stop all processing from one of these hooks is + # to raise SystemExit and stop the whole server. + if exc is None or hook.failsafe: + try: + hook() + except (KeyboardInterrupt, SystemExit): + raise + except (cherrypy.HTTPError, cherrypy.HTTPRedirect, + cherrypy.InternalRedirect): + exc = sys.exc_info()[1] + except Exception: + exc = sys.exc_info()[1] + cherrypy.log(traceback=True, severity=40) + if exc: + raise exc + + def __copy__(self): + newmap = self.__class__() + # We can't just use 'update' because we want copies of the + # mutable values (each is a list) as well. + for k, v in self.items(): + newmap[k] = v[:] + return newmap + copy = __copy__ + + def __repr__(self): + cls = self.__class__ + return '%s.%s(points=%r)' % ( + cls.__module__, + cls.__name__, + list(self) + ) + + +# Config namespace handlers + +def hooks_namespace(k, v): + """Attach bare hooks declared in config.""" + # Use split again to allow multiple hooks for a single + # hookpoint per path (e.g. "hooks.before_handler.1"). + # Little-known fact you only get from reading source ;) + hookpoint = k.split('.', 1)[0] + if isinstance(v, six.string_types): + v = cherrypy.lib.reprconf.attributes(v) + if not isinstance(v, Hook): + v = Hook(v) + cherrypy.serving.request.hooks[hookpoint].append(v) + + +def request_namespace(k, v): + """Attach request attributes declared in config.""" + # Provides config entries to set request.body attrs (like + # attempt_charsets). + if k[:5] == 'body.': + setattr(cherrypy.serving.request.body, k[5:], v) + else: + setattr(cherrypy.serving.request, 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 + if k[:8] == 'headers.': + cherrypy.serving.response.headers[k.split('.', 1)[1]] = v + else: + setattr(cherrypy.serving.response, k, v) + + +def error_page_namespace(k, v): + """Attach error pages declared in config.""" + if k != 'default': + k = int(k) + cherrypy.serving.request.error_page[k] = v + + +hookpoints = ['on_start_resource', 'before_request_body', + 'before_handler', 'before_finalize', + 'on_end_resource', 'on_end_request', + 'before_error_response', 'after_error_response'] + + +class Request(object): + + """An HTTP request. + + This object represents the metadata of an HTTP request message; + that is, it contains attributes which describe the environment + in which the request URL, headers, and body were sent (if you + want tools to interpret the headers and body, those are elsewhere, + mostly in Tools). This 'metadata' consists of socket data, + transport characteristics, and the Request-Line. This object + also contains data regarding the configuration in effect for + the given URL, and the execution plan for generating a response. + """ + + prev = None + """ + The previous Request object (if any). This should be None + unless we are processing an InternalRedirect.""" + + # Conversation/connection attributes + local = httputil.Host('127.0.0.1', 80) + 'An httputil.Host(ip, port, hostname) object for the server socket.' + + remote = httputil.Host('127.0.0.1', 1111) + 'An httputil.Host(ip, port, hostname) object for the client socket.' + + scheme = 'http' + """ + The protocol used between client and server. In most cases, + this will be either 'http' or 'https'.""" + + server_protocol = 'HTTP/1.1' + """ + The HTTP version for which the HTTP server is at least + conditionally compliant.""" + + base = '' + """The (scheme://host) portion of the requested URL. + In some cases (e.g. when proxying via mod_rewrite), this may contain + path segments which cherrypy.url uses when constructing url's, but + which otherwise are ignored by CherryPy. Regardless, this value + MUST NOT end in a slash.""" + + # Request-Line attributes + request_line = '' + """ + The complete Request-Line received from the client. This is a + single string consisting of the request method, URI, and protocol + version (joined by spaces). Any final CRLF is removed.""" + + method = 'GET' + """ + Indicates the HTTP method to be performed on the resource identified + by the Request-URI. Common methods include GET, HEAD, POST, PUT, and + DELETE. CherryPy allows any extension method; however, various HTTP + servers and gateways may restrict the set of allowable methods. + CherryPy applications SHOULD restrict the set (on a per-URI basis).""" + + query_string = '' + """ + 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, + 'a=3&b=4'.""" + + query_string_encoding = 'utf8' + """ + The encoding expected for query string arguments after % HEX HEX decoding). + If a query string is provided that cannot be decoded with this encoding, + 404 is raised (since technically it's a different URI). If you want + arbitrary encodings to not error, set this to 'Latin-1'; you can then + encode back to bytes and re-decode to whatever encoding you like later. + """ + + protocol = (1, 1) + """The HTTP protocol version corresponding to the set + of features which should be allowed in the response. If BOTH + the client's request message AND the server's level of HTTP + compliance is HTTP/1.1, this attribute will be the tuple (1, 1). + If either is 1.0, this attribute will be the tuple (1, 0). + Lower HTTP protocol versions are not explicitly supported.""" + + params = {} + """ + A dict which combines query string (GET) and request entity (POST) + variables. This is populated in two stages: GET params are added + before the 'on_start_resource' hook, and POST params are added + between the 'before_request_body' and 'before_handler' hooks.""" + + # Message attributes + header_list = [] + """ + A list of the HTTP request headers as (name, value) tuples. + In general, you should use request.headers (a dict) instead.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the request headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). See also: + httputil.HeaderMap, httputil.HeaderElement.""" + + cookie = SimpleCookie() + """See help(Cookie).""" + + rfile = None + """ + If the request included an entity (body), it will be available + as a stream in this attribute. However, the rfile will normally + be read for you between the 'before_request_body' hook and the + 'before_handler' hook, and the resulting string is placed into + either request.params or the request.body attribute. + + You may disable the automatic consumption of the rfile by setting + request.process_request_body to False, either in config for the desired + path, or in an 'on_start_resource' or 'before_request_body' hook. + + WARNING: In almost every case, you should not attempt to read from the + rfile stream after CherryPy's automatic mechanism has read it. If you + turn off the automatic parsing of rfile, you should read exactly the + number of bytes specified in request.headers['Content-Length']. + Ignoring either of these warnings may result in a hung request thread + or in corruption of the next (pipelined) request. + """ + + process_request_body = True + """ + If True, the rfile (if any) is automatically read and parsed, + and the result placed into request.params or request.body.""" + + methods_with_bodies = ('POST', 'PUT', 'PATCH') + """ + A sequence of HTTP methods for which CherryPy will automatically + attempt to read a body from the rfile. If you are going to change + this property, modify it on the configuration (recommended) + or on the "hook point" `on_start_resource`. + """ + + body = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' + or multipart, this will be None. Otherwise, this will be an instance + of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you + can .read()); this value is set between the 'before_request_body' and + 'before_handler' hooks (assuming that process_request_body is True).""" + + # Dispatch attributes + dispatch = cherrypy.dispatch.Dispatcher() + """ + The object which looks up the 'page handler' callable and collects + config for the current request based on the path_info, other + request attributes, and the application architecture. The core + calls the dispatcher as early as possible, passing it a 'path_info' + argument. + + The default dispatcher discovers the page handler by matching path_info + to a hierarchical arrangement of objects, starting at request.app.root. + See help(cherrypy.dispatch) for more information.""" + + script_name = '' + """ + The 'mount point' of the application which is handling this request. + + This attribute MUST NOT end in a slash. If the script_name refers to + the root of the URI, it MUST be an empty string (not "/"). + """ + + path_info = '/' + """ + The 'relative path' portion of the Request-URI. This is relative + to the script_name ('mount point') of the application which is + handling this request.""" + + login = None + """ + When authentication is used during the request processing this is + set to 'False' if it failed and to the 'username' value if it succeeded. + The default 'None' implies that no authentication happened.""" + + # Note that cherrypy.url uses "if request.app:" to determine whether + # the call is during a real HTTP request or not. So leave this None. + app = None + """The cherrypy.Application object which is handling this request.""" + + handler = None + """ + The function, method, or other callable which CherryPy will call to + produce the response. The discovery of the handler and the arguments + it will receive are determined by the request.dispatch object. + By default, the handler is discovered by walking a tree of objects + starting at request.app.root, and is then passed all HTTP params + (from the query string and POST body) as keyword arguments.""" + + toolmaps = {} + """ + A nested dict of all Toolboxes and Tools in effect for this request, + of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" + + config = None + """ + A flat dict of all configuration entries which apply to the + current request. These entries are collected from global config, + application config (based on request.path_info), and from handler + config (exactly how is governed by the request.dispatch object in + effect for this request; by default, handler config can be attached + anywhere in the tree between request.app.root and the final handler, + and inherits downward).""" + + is_index = None + """ + This will be True if the current request is mapped to an 'index' + resource handler (also, a 'default' handler if path_info ends with + a slash). The value may be used to automatically redirect the + user-agent to a 'more canonical' URL which either adds or removes + the trailing slash. See cherrypy.tools.trailing_slash.""" + + hooks = HookMap(hookpoints) + """ + A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. + Each key is a str naming the hook point, and each value is a list + of hooks which will be called at that hook point during this request. + The list of hooks is generally populated as early as possible (mostly + from Tools specified in config), but may be extended at any time. + See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" + + error_response = cherrypy.HTTPError(500).set_response + """ + The no-arg callable which will handle unexpected, untrapped errors + during request processing. This is not used for expected exceptions + (like NotFound, HTTPError, or HTTPRedirect) which are raised in + response to expected conditions (those should be customized either + via request.error_page or by overriding HTTPError.set_response). + By default, error_response uses HTTPError(500) to return a generic + error response to the user-agent.""" + + error_page = {} + """ + A dict of {error code: response filename or callable} pairs. + + The error code must be an int representing a given HTTP error code, + or the string 'default', which will be used if no matching entry + is found for a given numeric code. + + If a filename is provided, the file should contain a Python string- + formatting template, and can expect by default to receive format + values with the mapping keys %(status)s, %(message)s, %(traceback)s, + and %(version)s. The set of format mappings can be extended by + overriding HTTPError.set_response. + + If a callable is provided, it will be called by default with keyword + arguments 'status', 'message', 'traceback', and 'version', as for a + string-formatting template. The callable must return a string or + iterable of strings which will be set to response.body. It may also + override headers or perform any other processing. + + If no entry is given for an error code, and no 'default' entry exists, + a default template will be used. + """ + + show_tracebacks = True + """ + If True, unexpected errors encountered during request processing will + include a traceback in the response body.""" + + show_mismatched_params = True + """ + If True, mismatched parameters encountered during PageHandler invocation + processing will be included in the response body.""" + + throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) + """The sequence of exceptions which Request.run does not trap.""" + + throw_errors = False + """ + If True, Request.run will not trap any errors (except HTTPRedirect and + HTTPError, which are more properly called 'exceptions', not errors).""" + + closed = False + """True once the close method has been called, False otherwise.""" + + stage = None + """ + A string containing the stage reached in the request-handling process. + This is useful when debugging a live server with hung requests.""" + + unique_id = None + """A lazy object generating and memorizing UUID4 on ``str()`` render.""" + + namespaces = reprconf.NamespaceSet( + **{'hooks': hooks_namespace, + 'request': request_namespace, + 'response': response_namespace, + 'error_page': error_page_namespace, + 'tools': cherrypy.tools, + }) + + def __init__(self, local_host, remote_host, scheme='http', + server_protocol='HTTP/1.1'): + """Populate a new Request object. + + local_host should be an httputil.Host object with the server info. + remote_host should be an httputil.Host object with the client info. + scheme should be a string, either "http" or "https". + """ + self.local = local_host + self.remote = remote_host + self.scheme = scheme + self.server_protocol = server_protocol + + self.closed = False + + # Put a *copy* of the class error_page into self. + self.error_page = self.error_page.copy() + + # Put a *copy* of the class namespaces into self. + self.namespaces = self.namespaces.copy() + + self.stage = None + + self.unique_id = LazyUUID4() + + def close(self): + """Run cleanup code. (Core)""" + if not self.closed: + self.closed = True + self.stage = 'on_end_request' + self.hooks.run('on_end_request') + self.stage = 'close' + + def run(self, method, path, query_string, req_protocol, headers, rfile): + r"""Process the Request. (Core) + + method, path, query_string, and req_protocol should be pulled directly + from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). + + path + This should be %XX-unquoted, but query_string should not be. + + When using Python 2, they both MUST be byte strings, + not unicode strings. + + When using Python 3, they both MUST be unicode strings, + not byte strings, and preferably not bytes \x00-\xFF + disguised as unicode. + + headers + A list of (name, value) tuples. + + rfile + A file-like object containing the HTTP request entity. + + When run() is done, the returned object should have 3 attributes: + + * status, e.g. "200 OK" + * header_list, a list of (name, value) tuples + * body, an iterable yielding strings + + Consumer code (HTTP servers) should then access these response + attributes to build the outbound stream. + + """ + response = cherrypy.serving.response + self.stage = 'run' + try: + self.error_response = cherrypy.HTTPError(500).set_response + + self.method = method + path = path or '/' + self.query_string = query_string or '' + self.params = {} + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(self.server_protocol[5]), int(self.server_protocol[7]) + self.protocol = min(rp, sp) + response.headers.protocol = self.protocol + + # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). + url = path + if query_string: + url += '?' + query_string + self.request_line = '%s %s %s' % (method, url, req_protocol) + + self.header_list = list(headers) + self.headers = httputil.HeaderMap() + + self.rfile = rfile + self.body = None + + self.cookie = SimpleCookie() + self.handler = None + + # path_info should be the path from the + # app root (script_name) to the handler. + self.script_name = self.app.script_name + self.path_info = pi = path[len(self.script_name):] + + self.stage = 'respond' + self.respond(pi) + + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + else: + # Failure in setup, error handler or finalize. Bypass them. + # Can't use handle_error because we may not have hooks yet. + cherrypy.log(traceback=True, severity=40) + if self.show_tracebacks: + body = format_exc() + else: + body = '' + r = bare_error(body) + response.output_status, response.header_list, response.body = r + + if self.method == 'HEAD': + # HEAD requests MUST NOT return a message-body in the response. + response.body = [] + + try: + cherrypy.log.access() + except Exception: + cherrypy.log.error(traceback=True) + + return response + + def respond(self, path_info): + """Generate a response for the resource at self.path_info. (Core)""" + try: + try: + try: + self._do_respond(path_info) + except (cherrypy.HTTPRedirect, cherrypy.HTTPError): + inst = sys.exc_info()[1] + inst.set_response() + self.stage = 'before_finalize (HTTPError)' + self.hooks.run('before_finalize') + cherrypy.serving.response.finalize() + finally: + self.stage = 'on_end_resource' + self.hooks.run('on_end_resource') + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + self.handle_error() + + def _do_respond(self, path_info): + response = cherrypy.serving.response + + if self.app is None: + raise cherrypy.NotFound() + + self.hooks = self.__class__.hooks.copy() + self.toolmaps = {} + + # Get the 'Host' header, so we can HTTPRedirect properly. + self.stage = 'process_headers' + self.process_headers() + + self.stage = 'get_resource' + self.get_resource(path_info) + + self.body = _cpreqbody.RequestBody( + self.rfile, self.headers, request_params=self.params) + + self.namespaces(self.config) + + self.stage = 'on_start_resource' + self.hooks.run('on_start_resource') + + # Parse the querystring + self.stage = 'process_query_string' + self.process_query_string() + + # Process the body + if self.process_request_body: + if self.method not in self.methods_with_bodies: + self.process_request_body = False + self.stage = 'before_request_body' + self.hooks.run('before_request_body') + if self.process_request_body: + self.body.process() + + # Run the handler + self.stage = 'before_handler' + self.hooks.run('before_handler') + if self.handler: + self.stage = 'handler' + response.body = self.handler() + + # Finalize + self.stage = 'before_finalize' + self.hooks.run('before_finalize') + response.finalize() + + def process_query_string(self): + """Parse the query string into Python structures. (Core)""" + try: + p = httputil.parse_query_string( + self.query_string, encoding=self.query_string_encoding) + except UnicodeDecodeError: + raise cherrypy.HTTPError( + 404, 'The given query string could not be processed. Query ' + 'strings for this resource must be encoded with %r.' % + self.query_string_encoding) + + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if six.PY2: + for key, value in p.items(): + if isinstance(key, six.text_type): + del p[key] + p[key.encode(self.query_string_encoding)] = value + self.params.update(p) + + def process_headers(self): + """Parse HTTP header data into Python structures. (Core)""" + # Process the headers into self.headers + headers = self.headers + for name, value in self.header_list: + # Call title() now (and use dict.__method__(headers)) + # so title doesn't have to be called twice. + name = name.title() + value = value.strip() + + headers[name] = httputil.decode_TEXT_maybe(value) + + # Some clients, notably Konquoror, supply multiple + # cookies on different lines with the same key. To + # handle this case, store all cookies in self.cookie. + if name == 'Cookie': + try: + self.cookie.load(value) + except CookieError as exc: + raise cherrypy.HTTPError(400, str(exc)) + + if not dict.__contains__(headers, 'Host'): + # All Internet-based HTTP/1.1 servers MUST respond with a 400 + # (Bad Request) status code to any HTTP/1.1 request message + # which lacks a Host header field. + if self.protocol >= (1, 1): + msg = "HTTP/1.1 requires a 'Host' request header." + raise cherrypy.HTTPError(400, msg) + host = dict.get(headers, 'Host') + if not host: + host = self.local.name or self.local.ip + self.base = '%s://%s' % (self.scheme, host) + + def get_resource(self, path): + """Call a dispatcher (which sets self.handler and .config). (Core)""" + # First, see if there is a custom dispatch at this URI. Custom + # dispatchers can only be specified in app.config, not in _cp_config + # (since custom dispatchers may not even have an app.root). + dispatch = self.app.find_config( + path, 'request.dispatch', self.dispatch) + + # dispatch() should set self.handler and self.config + dispatch(path) + + def handle_error(self): + """Handle the last unanticipated exception. (Core)""" + try: + self.hooks.run('before_error_response') + if self.error_response: + self.error_response() + self.hooks.run('after_error_response') + cherrypy.serving.response.finalize() + except cherrypy.HTTPRedirect: + inst = sys.exc_info()[1] + inst.set_response() + cherrypy.serving.response.finalize() + + +class ResponseBody(object): + + """The body of the HTTP response (the response entity).""" + + unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' + 'if you wish to return unicode.') + + def __get__(self, obj, objclass=None): + if obj is None: + # When calling on the class instead of an instance... + return self + else: + return obj._body + + def __set__(self, obj, value): + # Convert the given value to an iterable object. + if isinstance(value, six.text_type): + raise ValueError(self.unicode_err) + elif isinstance(value, list): + # every item in a list must be bytes... + if any(isinstance(item, six.text_type) for item in value): + raise ValueError(self.unicode_err) + + obj._body = encoding.prepare_iter(value) + + +class Response(object): + + """An HTTP Response, including status, headers, and body.""" + + status = '' + """The HTTP Status-Code and Reason-Phrase.""" + + header_list = [] + """ + A list of the HTTP response headers as (name, value) tuples. + In general, you should use response.headers (a dict) instead. This + attribute is generated from response.headers and is not valid until + after the finalize phase.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the response headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). + + .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` + """ + + cookie = SimpleCookie() + """See help(Cookie).""" + + body = ResponseBody() + """The body (entity) of the HTTP response.""" + + time = None + """The value of time.time() when created. Use in HTTP dates.""" + + stream = False + """If False, buffer the response body.""" + + def __init__(self): + self.status = None + self.header_list = None + self._body = [] + self.time = time.time() + + self.headers = httputil.HeaderMap() + # Since we know all our keys are titled strings, we can + # bypass HeaderMap.update and get a big speed boost. + dict.update(self.headers, { + 'Content-Type': 'text/html', + 'Server': 'CherryPy/' + cherrypy.__version__, + 'Date': httputil.HTTPDate(self.time), + }) + self.cookie = SimpleCookie() + + def collapse_body(self): + """Collapse self.body to a single string; replace it and return it.""" + new_body = b''.join(self.body) + self.body = new_body + return new_body + + def _flush_body(self): + """ + Discard self.body but consume any generator such that + any finalization can occur, such as is required by + caching.tee_output(). + """ + consume(iter(self.body)) + + def finalize(self): + """Transform headers (and cookies) into self.header_list. (Core)""" + try: + code, reason, _ = httputil.valid_status(self.status) + except ValueError: + raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) + + headers = self.headers + + self.status = '%s %s' % (code, reason) + self.output_status = ntob(str(code), 'ascii') + \ + b' ' + headers.encode(reason) + + if self.stream: + # The upshot: wsgiserver will chunk the response if + # you pop Content-Length (or set it explicitly to None). + # Note that lib.static sets C-L to the file's st_size. + if dict.get(headers, 'Content-Length') is None: + dict.pop(headers, 'Content-Length', None) + elif code < 200 or code in (204, 205, 304): + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." + dict.pop(headers, 'Content-Length', None) + self._flush_body() + self.body = b'' + else: + # Responses which are not streamed should have a Content-Length, + # but allow user code to set Content-Length if desired. + if dict.get(headers, 'Content-Length') is None: + content = self.collapse_body() + dict.__setitem__(headers, 'Content-Length', len(content)) + + # Transform our header dict into a list of tuples. + self.header_list = h = headers.output() + + cookie = self.cookie.output() + if cookie: + for line in cookie.split('\r\n'): + name, value = line.split(': ', 1) + if isinstance(name, six.text_type): + name = name.encode('ISO-8859-1') + if isinstance(value, six.text_type): + value = headers.encode(value) + h.append((name, value)) + + +class LazyUUID4(object): + def __str__(self): + """Return UUID4 and keep it for future calls.""" + return str(self.uuid4) + + @property + def uuid4(self): + """Provide unique id on per-request basis using UUID4. + + It's evaluated lazily on render. + """ + try: + self._uuid4 + except AttributeError: + # evaluate on first access + self._uuid4 = uuid.uuid4() + + return self._uuid4 diff --git a/libraries/cherrypy/_cpserver.py b/libraries/cherrypy/_cpserver.py new file mode 100644 index 00000000..0f60e2c8 --- /dev/null +++ b/libraries/cherrypy/_cpserver.py @@ -0,0 +1,252 @@ +"""Manage HTTP servers with CherryPy.""" + +import six + +import cherrypy +from cherrypy.lib.reprconf import attributes +from cherrypy._cpcompat import text_or_bytes +from cherrypy.process.servers import ServerAdapter + + +__all__ = ('Server', ) + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + + @property + def socket_host(self): # noqa: D401; irrelevant for properties + """The hostname or IP address on which to listen for connections. + + Host values may be any IPv4 or IPv6 address, or any valid hostname. + The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if + your hosts file prefers IPv6). The string '0.0.0.0' is a special + IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' + is the similar IN6ADDR_ANY for IPv6. The empty string or None are + not allowed. + """ + return self._socket_host + + @socket_host.setter + def socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + 'interfaces (INADDR_ANY).') + self._socket_host = value + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + accepted_queue_size = -1 + """The maximum number of requests which will be queued up before + the server refuses to accept it (default -1, meaning no limit).""" + + accepted_queue_timeout = 10 + """The timeout in seconds for attempting to add a request to the + queue when the queue is full (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit. + """ + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. + If exceeded, the HTTP server should return "413 Request Entity Too Large". + """ + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + cheroot.wsgi.Server) which cherrypy.server will control. + Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + ssl_ciphers = None + """The ciphers list of SSL.""" + + if six.PY3: + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with + the builtin WSGI server. Builtin options are: 'builtin' (to + use the SSL library built into recent versions of Python). + You may also register your own classes in the + cheroot.server.ssl_adapters dict.""" + else: + ssl_module = 'pyopenssl' + """The name of a registered SSL adaptation module to use with the + builtin WSGI server. Builtin options are 'builtin' (to use the SSL + library built into recent versions of Python) and 'pyopenssl' (to + use the PyOpenSSL project, which you must install separately). You + may also register your own classes in the cheroot.server.ssl_adapters + dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. + """ + + peercreds = False + """If True, peer cred lookup for UNIX domain socket will put to WSGI env. + + This information will then be available through WSGI env vars: + * X_REMOTE_PID + * X_REMOTE_UID + * X_REMOTE_GID + """ + + peercreds_resolve = False + """If True, username/group will be looked up in the OS from peercreds. + + This information will then be available through WSGI env vars: + * REMOTE_USER + * X_REMOTE_USER + * X_REMOTE_GROUP + """ + + def __init__(self): + """Initialize Server instance.""" + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, text_or_bytes): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + super(Server, self).start() + start.priority = 75 + + @property + def bind_addr(self): + """Return bind address. + + A (host, port) tuple for TCP sockets or a str for Unix domain sockts. + """ + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + + @bind_addr.setter + def bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, text_or_bytes): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError('bind_addr must be a (host, port) tuple ' + '(for TCP sockets) or a string (for Unix ' + 'domain sockets), not %r' % value) + + def base(self): + """Return the base for this server. + + e.i. scheme://host[:port] or sock file + """ + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) diff --git a/libraries/cherrypy/_cptools.py b/libraries/cherrypy/_cptools.py new file mode 100644 index 00000000..57460285 --- /dev/null +++ b/libraries/cherrypy/_cptools.py @@ -0,0 +1,509 @@ +"""CherryPy tools. A "tool" is any helper, adapted to CP. + +Tools are usually designed to be used in a variety of ways (although some +may only offer one if they choose): + + Library calls + All tools are callables that can be used wherever needed. + The arguments are straightforward and should be detailed within the + docstring. + + Function decorators + All tools, when called, may be used as decorators which configure + individual CherryPy page handlers (methods on the CherryPy tree). + That is, "@tools.anytool()" should "turn on" the tool via the + decorated function's _cp_config attribute. + + CherryPy config + If a tool exposes a "_setup" callable, it will be called + once per Request (if the feature is "turned on" via config). + +Tools may be implemented as any object with a namespace. The builtins +are generally either modules or instances of the tools.Tool class. +""" + +import six + +import cherrypy +from cherrypy._helper import expose + +from cherrypy.lib import cptools, encoding, static, jsontools +from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc +from cherrypy.lib import caching as _caching +from cherrypy.lib import auth_basic, auth_digest + + +def _getargs(func): + """Return the names of all static arguments to the given function.""" + # Use this instead of importing inspect for less mem overhead. + import types + if six.PY3: + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ + else: + if isinstance(func, types.MethodType): + func = func.im_func + co = func.func_code + return co.co_varnames[:co.co_argcount] + + +_attr_error = ( + 'CherryPy Tools cannot be turned on directly. Instead, turn them ' + 'on via config, or use them as decorators on your page handlers.' +) + + +class Tool(object): + + """A registered function for use with CherryPy request-processing hooks. + + help(tool.callable) should give you more information about this Tool. + """ + + namespace = 'tools' + + def __init__(self, point, callable, name=None, priority=50): + self._point = point + self.callable = callable + self._name = name + self._priority = priority + self.__doc__ = self.callable.__doc__ + self._setargs() + + @property + def on(self): + raise AttributeError(_attr_error) + + @on.setter + def on(self, value): + raise AttributeError(_attr_error) + + def _setargs(self): + """Copy func parameter names to obj attributes.""" + try: + for arg in _getargs(self.callable): + setattr(self, arg, None) + except (TypeError, AttributeError): + if hasattr(self.callable, '__call__'): + for arg in _getargs(self.callable.__call__): + setattr(self, arg, None) + # IronPython 1.0 raises NotImplementedError because + # inspect.getargspec tries to access Python bytecode + # in co_code attribute. + except NotImplementedError: + pass + # IronPython 1B1 may raise IndexError in some cases, + # but if we trap it here it doesn't prevent CP from working. + except IndexError: + pass + + def _merged_args(self, d=None): + """Return a dict of configuration entries for this Tool.""" + if d: + conf = d.copy() + else: + conf = {} + + tm = cherrypy.serving.request.toolmaps[self.namespace] + if self._name in tm: + conf.update(tm[self._name]) + + if 'on' in conf: + del conf['on'] + + return conf + + def __call__(self, *args, **kwargs): + """Compile-time decorator (turn on the tool in config). + + For example:: + + @expose + @tools.proxy() + def whats_my_base(self): + return cherrypy.request.base + """ + if args: + raise TypeError('The %r Tool does not accept positional ' + 'arguments; you must use keyword arguments.' + % self._name) + + def tool_decorator(f): + if not hasattr(f, '_cp_config'): + f._cp_config = {} + subspace = self.namespace + '.' + self._name + '.' + f._cp_config[subspace + 'on'] = True + for k, v in kwargs.items(): + f._cp_config[subspace + k] = v + return f + return tool_decorator + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self.callable, + priority=p, **conf) + + +class HandlerTool(Tool): + + """Tool which is called 'before main', that may skip normal handlers. + + If the tool successfully handles the request (by setting response.body), + if should return True. This will cause CherryPy to skip any 'normal' page + handler. If the tool did not handle the request, it should return False + to tell CherryPy to continue on and call the normal page handler. If the + tool is declared AS a page handler (see the 'handler' method), returning + False will raise NotFound. + """ + + def __init__(self, callable, name=None): + Tool.__init__(self, 'before_handler', callable, name) + + def handler(self, *args, **kwargs): + """Use this tool as a CherryPy page handler. + + For example:: + + class Root: + nav = tools.staticdir.handler(section="/nav", dir="nav", + root=absDir) + """ + @expose + def handle_func(*a, **kw): + handled = self.callable(*args, **self._merged_args(kwargs)) + if not handled: + raise cherrypy.NotFound() + return cherrypy.serving.response.body + return handle_func + + def _wrapper(self, **kwargs): + if self.callable(**kwargs): + cherrypy.serving.request.handler = None + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self._wrapper, + priority=p, **conf) + + +class HandlerWrapperTool(Tool): + + """Tool which wraps request.handler in a provided wrapper function. + + The 'newhandler' arg must be a handler wrapper function that takes a + 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all + page handler + functions, it must return an iterable for use as cherrypy.response.body. + + For example, to allow your 'inner' page handlers to return dicts + which then get interpolated into a template:: + + def interpolator(next_handler, *args, **kwargs): + filename = cherrypy.request.config.get('template') + cherrypy.response.template = env.get_template(filename) + response_dict = next_handler(*args, **kwargs) + return cherrypy.response.template.render(**response_dict) + cherrypy.tools.jinja = HandlerWrapperTool(interpolator) + """ + + def __init__(self, newhandler, point='before_handler', name=None, + priority=50): + self.newhandler = newhandler + self._point = point + self._name = name + self._priority = priority + + def callable(self, *args, **kwargs): + innerfunc = cherrypy.serving.request.handler + + def wrap(*args, **kwargs): + return self.newhandler(innerfunc, *args, **kwargs) + cherrypy.serving.request.handler = wrap + + +class ErrorTool(Tool): + + """Tool which is used to replace the default request.error_response.""" + + def __init__(self, callable, name=None): + Tool.__init__(self, None, callable, name) + + def _wrapper(self): + self.callable(**self._merged_args()) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + cherrypy.serving.request.error_response = self._wrapper + + +# Builtin tools # + + +class SessionTool(Tool): + + """Session Tool for CherryPy. + + sessions.locking + When 'implicit' (the default), the session will be locked for you, + just before running the page handler. + + When 'early', the session will be locked before reading the request + body. This is off by default for safety reasons; for example, + a large upload would block the session, denying an AJAX + progress meter + (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_). + + When 'explicit' (or any other value), you need to call + cherrypy.session.acquire_lock() yourself before using + session data. + """ + + def __init__(self): + # _sessions.init must be bound after headers are read + Tool.__init__(self, 'before_request_body', _sessions.init) + + def _lock_session(self): + cherrypy.serving.session.acquire_lock() + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + hooks = cherrypy.serving.request.hooks + + conf = self._merged_args() + + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + + hooks.attach(self._point, self.callable, priority=p, **conf) + + locking = conf.pop('locking', 'implicit') + if locking == 'implicit': + hooks.attach('before_handler', self._lock_session) + elif locking == 'early': + # Lock before the request body (but after _sessions.init runs!) + hooks.attach('before_request_body', self._lock_session, + priority=60) + else: + # Don't lock + pass + + hooks.attach('before_finalize', _sessions.save) + hooks.attach('on_end_request', _sessions.close) + + def regenerate(self): + """Drop the current session and make a new one (with a new id).""" + sess = cherrypy.serving.session + sess.regenerate() + + # Grab cookie-relevant tool args + relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure' + conf = dict( + (k, v) + for k, v in self._merged_args().items() + if k in relevant + ) + _sessions.set_response_cookie(**conf) + + +class XMLRPCController(object): + + """A Controller (page handler collection) for XML-RPC. + + To use it, have your controllers subclass this base class (it will + turn on the tool for you). + + You can also supply the following optional config entries:: + + tools.xmlrpc.encoding: 'utf-8' + tools.xmlrpc.allow_none: 0 + + XML-RPC is a rather discontinuous layer over HTTP; dispatching to the + appropriate handler must first be performed according to the URL, and + then a second dispatch step must take place according to the RPC method + specified in the request body. It also allows a superfluous "/RPC2" + prefix in the URL, supplies its own handler args in the body, and + requires a 200 OK "Fault" response instead of 404 when the desired + method is not found. + + Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. + This Controller acts as the dispatch target for the first half (based + on the URL); it then reads the RPC method from the request body and + does its own second dispatch step based on that method. It also reads + body params, and returns a Fault on error. + + The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 + in your URL's, you can safely skip turning on the XMLRPCDispatcher. + Otherwise, you need to use declare it in config:: + + request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() + """ + + # Note we're hard-coding this into the 'tools' namespace. We could do + # a huge amount of work to make it relocatable, but the only reason why + # would be if someone actually disabled the default_toolbox. Meh. + _cp_config = {'tools.xmlrpc.on': True} + + @expose + def default(self, *vpath, **params): + rpcparams, rpcmethod = _xmlrpc.process_body() + + subhandler = self + for attr in str(rpcmethod).split('.'): + subhandler = getattr(subhandler, attr, None) + + if subhandler and getattr(subhandler, 'exposed', False): + body = subhandler(*(vpath + rpcparams), **params) + + else: + # https://github.com/cherrypy/cherrypy/issues/533 + # if a method is not found, an xmlrpclib.Fault should be returned + # raising an exception here will do that; see + # cherrypy.lib.xmlrpcutil.on_error + raise Exception('method "%s" is not supported' % attr) + + conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {}) + _xmlrpc.respond(body, + conf.get('encoding', 'utf-8'), + conf.get('allow_none', 0)) + return cherrypy.serving.response.body + + +class SessionAuthTool(HandlerTool): + pass + + +class CachingTool(Tool): + + """Caching Tool for CherryPy.""" + + def _wrapper(self, **kwargs): + request = cherrypy.serving.request + if _caching.get(**kwargs): + request.handler = None + else: + if request.cacheable: + # Note the devious technique here of adding hooks on the fly + request.hooks.attach('before_finalize', _caching.tee_output, + priority=100) + _wrapper.priority = 90 + + def _setup(self): + """Hook caching into cherrypy.request.""" + conf = self._merged_args() + + p = conf.pop('priority', None) + cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, + priority=p, **conf) + + +class Toolbox(object): + + """A collection of Tools. + + This object also functions as a config namespace handler for itself. + Custom toolboxes should be added to each Application's toolboxes dict. + """ + + def __init__(self, namespace): + self.namespace = namespace + + def __setattr__(self, name, value): + # If the Tool._name is None, supply it from the attribute name. + if isinstance(value, Tool): + if value._name is None: + value._name = name + value.namespace = self.namespace + object.__setattr__(self, name, value) + + def __enter__(self): + """Populate request.toolmaps from tools specified in config.""" + cherrypy.serving.request.toolmaps[self.namespace] = map = {} + + def populate(k, v): + toolname, arg = k.split('.', 1) + bucket = map.setdefault(toolname, {}) + bucket[arg] = v + return populate + + def __exit__(self, exc_type, exc_val, exc_tb): + """Run tool._setup() for each tool in our toolmap.""" + map = cherrypy.serving.request.toolmaps.get(self.namespace) + if map: + for name, settings in map.items(): + if settings.get('on', False): + tool = getattr(self, name) + tool._setup() + + def register(self, point, **kwargs): + """ + Return a decorator which registers the function + at the given hook point. + """ + def decorator(func): + attr_name = kwargs.get('name', func.__name__) + tool = Tool(point, func, **kwargs) + setattr(self, attr_name, tool) + return func + return decorator + + +default_toolbox = _d = Toolbox('tools') +_d.session_auth = SessionAuthTool(cptools.session_auth) +_d.allow = Tool('on_start_resource', cptools.allow) +_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) +_d.response_headers = Tool('on_start_resource', cptools.response_headers) +_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) +_d.log_headers = Tool('before_error_response', cptools.log_request_headers) +_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) +_d.err_redirect = ErrorTool(cptools.redirect) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) +_d.decode = Tool('before_request_body', encoding.decode) +# the order of encoding, gzip, caching is important +_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) +_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) +_d.staticdir = HandlerTool(static.staticdir) +_d.staticfile = HandlerTool(static.staticfile) +_d.sessions = SessionTool() +_d.xmlrpc = ErrorTool(_xmlrpc.on_error) +_d.caching = CachingTool('before_handler', _caching.get, 'caching') +_d.expires = Tool('before_finalize', _caching.expires) +_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) +_d.referer = Tool('before_request_body', cptools.referer) +_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) +_d.flatten = Tool('before_finalize', cptools.flatten) +_d.accept = Tool('on_start_resource', cptools.accept) +_d.redirect = Tool('on_start_resource', cptools.redirect) +_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) +_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) +_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) +_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) +_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) +_d.params = Tool('before_handler', cptools.convert_params, priority=15) + +del _d, cptools, encoding, static diff --git a/libraries/cherrypy/_cptree.py b/libraries/cherrypy/_cptree.py new file mode 100644 index 00000000..ceb54379 --- /dev/null +++ b/libraries/cherrypy/_cptree.py @@ -0,0 +1,313 @@ +"""CherryPy Application and Tree objects.""" + +import os + +import six + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools +from cherrypy.lib import httputil, reprconf + + +class Application(object): + """A CherryPy Application. + + Servers and gateways should not instantiate Request objects directly. + Instead, they should ask an Application object for a request object. + + An instance of this class may also be used as a WSGI callable + (WSGI application object) for itself. + """ + + root = None + """The top-most container of page handlers for this app. Handlers should + be arranged in a hierarchy of attributes, matching the expected URI + hierarchy; the default dispatcher then searches this hierarchy for a + matching handler. When using a dispatcher other than the default, + this value may be None.""" + + config = {} + """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict + of {key: value} pairs.""" + + namespaces = reprconf.NamespaceSet() + toolboxes = {'tools': cherrypy.tools} + + log = None + """A LogManager instance. See _cplogging.""" + + wsgiapp = None + """A CPWSGIApp instance. See _cpwsgi.""" + + request_class = _cprequest.Request + response_class = _cprequest.Response + + relative_urls = False + + def __init__(self, root, script_name='', config=None): + """Initialize Application with given root.""" + self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) + self.root = root + self.script_name = script_name + self.wsgiapp = _cpwsgi.CPWSGIApp(self) + + self.namespaces = self.namespaces.copy() + self.namespaces['log'] = lambda k, v: setattr(self.log, k, v) + self.namespaces['wsgi'] = self.wsgiapp.namespace_handler + + self.config = self.__class__.config.copy() + if config: + self.merge(config) + + def __repr__(self): + """Generate a representation of the Application instance.""" + return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__, + self.root, self.script_name) + + script_name_doc = """The URI "mount point" for this app. A mount point + is that portion of the URI which is constant for all URIs that are + serviced by this application; it does not include scheme, host, or proxy + ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + + @property + def script_name(self): # noqa: D401; irrelevant for properties + """The URI "mount point" for this app. + + A mount point is that portion of the URI which is constant for all URIs + that are serviced by this application; it does not include scheme, + host, or proxy ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + if self._script_name is not None: + return self._script_name + + # A `_script_name` with a value of None signals that the script name + # should be pulled from WSGI environ. + return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/') + + @script_name.setter + def script_name(self, value): + if value: + value = value.rstrip('/') + self._script_name = value + + def merge(self, config): + """Merge the given config into self.config.""" + _cpconfig.merge(self.config, config) + + # Handle namespaces specified in config. + self.namespaces(self.config.get('/', {})) + + def find_config(self, path, key, default=None): + """Return the most-specific value for key along path, or default.""" + trail = path or '/' + while trail: + nodeconf = self.config.get(trail, {}) + + if key in nodeconf: + return nodeconf[key] + + lastslash = trail.rfind('/') + if lastslash == -1: + break + elif lastslash == 0 and trail != '/': + trail = '/' + else: + trail = trail[:lastslash] + + return default + + def get_serving(self, local, remote, scheme, sproto): + """Create and return a Request and Response object.""" + req = self.request_class(local, remote, scheme, sproto) + req.app = self + + for name, toolbox in self.toolboxes.items(): + req.namespaces[name] = toolbox + + resp = self.response_class() + cherrypy.serving.load(req, resp) + cherrypy.engine.publish('acquire_thread') + cherrypy.engine.publish('before_request') + + return req, resp + + def release_serving(self): + """Release the current serving (request and response).""" + req = cherrypy.serving.request + + cherrypy.engine.publish('after_request') + + try: + req.close() + except Exception: + cherrypy.log(traceback=True, severity=40) + + cherrypy.serving.clear() + + def __call__(self, environ, start_response): + """Call a WSGI-callable.""" + return self.wsgiapp(environ, start_response) + + +class Tree(object): + """A registry of CherryPy applications, mounted at diverse points. + + An instance of this class may also be used as a WSGI callable + (WSGI application object), in which case it dispatches to all + mounted apps. + """ + + apps = {} + """ + A dict of the form {script name: application}, where "script name" + is a string declaring the URI mount point (no trailing slash), and + "application" is an instance of cherrypy.Application (or an arbitrary + WSGI callable if you happen to be using a WSGI server).""" + + def __init__(self): + """Initialize registry Tree.""" + self.apps = {} + + def mount(self, root, script_name='', config=None): + """Mount a new app from a root object, script_name, and config. + + root + An instance of a "controller class" (a collection of page + handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. + + script_name + A string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the + URL at which to mount the given root. For example, if root.index() + will handle requests to "http://www.example.com:8080/dept/app1/", + then the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the + root of the URI, it MUST be an empty string (not "/"). + + config + A file or dict containing application config. + """ + if script_name is None: + raise TypeError( + "The 'script_name' argument may not be None. Application " + 'objects may, however, possess a script_name of None (in ' + 'order to inpect the WSGI environ for SCRIPT_NAME upon each ' + 'request). You cannot mount such Applications on this Tree; ' + 'you must pass them to a WSGI server interface directly.') + + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + + if isinstance(root, Application): + app = root + if script_name != '' and script_name != app.script_name: + raise ValueError( + 'Cannot specify a different script name and pass an ' + 'Application instance to cherrypy.mount') + script_name = app.script_name + else: + app = Application(root, script_name) + + # If mounted at "", add favicon.ico + needs_favicon = ( + script_name == '' + and root is not None + and not hasattr(root, 'favicon_ico') + ) + if needs_favicon: + favicon = os.path.join( + os.getcwd(), + os.path.dirname(__file__), + 'favicon.ico', + ) + root.favicon_ico = tools.staticfile.handler(favicon) + + if config: + app.merge(config) + + self.apps[script_name] = app + + return app + + def graft(self, wsgi_callable, script_name=''): + """Mount a wsgi callable at the given script_name.""" + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + self.apps[script_name] = wsgi_callable + + def script_name(self, path=None): + """Return the script_name of the app at the given path, or None. + + If path is None, cherrypy.request is used. + """ + if path is None: + try: + request = cherrypy.serving.request + path = httputil.urljoin(request.script_name, + request.path_info) + except AttributeError: + return None + + while True: + if path in self.apps: + return path + + if path == '': + return None + + # Move one node up the tree and try again. + path = path[:path.rfind('/')] + + def __call__(self, environ, start_response): + """Pre-initialize WSGI env and call WSGI-callable.""" + # If you're calling this, then you're probably setting SCRIPT_NAME + # to '' (some WSGI servers always set SCRIPT_NAME to ''). + # Try to look up the app using the full path. + env1x = environ + if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) + path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), + env1x.get('PATH_INFO', '')) + sn = self.script_name(path or '/') + if sn is None: + start_response('404 Not Found', []) + return [] + + app = self.apps[sn] + + # Correct the SCRIPT_NAME and PATH_INFO environ entries. + environ = environ.copy() + if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 2/WSGI u.0: all strings MUST be of type unicode + enc = environ[ntou('wsgi.url_encoding')] + environ[ntou('SCRIPT_NAME')] = sn.decode(enc) + environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc) + else: + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip('/')):] + return app(environ, start_response) diff --git a/libraries/cherrypy/_cpwsgi.py b/libraries/cherrypy/_cpwsgi.py new file mode 100644 index 00000000..0b4942ff --- /dev/null +++ b/libraries/cherrypy/_cpwsgi.py @@ -0,0 +1,467 @@ +"""WSGI interface (see PEP 333 and 3333). + +Note that WSGI environ keys and values are 'native strings'; that is, +whatever the type of "" is. For Python 2, that's a byte string; for Python 3, +it's a unicode string. But PEP 3333 says: "even if Python's str type is +actually Unicode "under the hood", the content of native strings must +still be translatable to bytes via the Latin-1 encoding!" +""" + +import sys as _sys +import io + +import six + +import cherrypy as _cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cperror +from cherrypy.lib import httputil +from cherrypy.lib import is_closable_iterator + + +def downgrade_wsgi_ux_to_1x(environ): + """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. + """ + env1x = {} + + url_encoding = environ[ntou('wsgi.url_encoding')] + for k, v in list(environ.items()): + if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: + v = v.encode(url_encoding) + elif isinstance(v, six.text_type): + v = v.encode('ISO-8859-1') + env1x[k.encode('ISO-8859-1')] = v + + return env1x + + +class VirtualHost(object): + + """Select a different WSGI application based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different applications. For example:: + + root = Root() + RootApp = cherrypy.Application(root) + Domain2App = cherrypy.Application(root) + SecureApp = cherrypy.Application(Secure()) + + vhost = cherrypy._cpwsgi.VirtualHost( + RootApp, + domains={ + 'www.domain2.example': Domain2App, + 'www.domain2.example:443': SecureApp, + }, + ) + + cherrypy.tree.graft(vhost) + """ + default = None + """Required. The default WSGI application.""" + + use_x_forwarded_host = True + """If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying.""" + + domains = {} + """A dict of {host header value: application} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding WSGI application + will be called instead of the default. Note that you often need + separate entries for "example.com" and "www.example.com". + In addition, "Host" headers may contain the port number. + """ + + def __init__(self, default, domains=None, use_x_forwarded_host=True): + self.default = default + self.domains = domains or {} + self.use_x_forwarded_host = use_x_forwarded_host + + def __call__(self, environ, start_response): + domain = environ.get('HTTP_HOST', '') + if self.use_x_forwarded_host: + domain = environ.get('HTTP_X_FORWARDED_HOST', domain) + + nextapp = self.domains.get(domain) + if nextapp is None: + nextapp = self.default + return nextapp(environ, start_response) + + +class InternalRedirector(object): + + """WSGI middleware that handles raised cherrypy.InternalRedirect.""" + + def __init__(self, nextapp, recursive=False): + self.nextapp = nextapp + self.recursive = recursive + + def __call__(self, environ, start_response): + redirections = [] + while True: + environ = environ.copy() + try: + return self.nextapp(environ, start_response) + except _cherrypy.InternalRedirect: + ir = _sys.exc_info()[1] + sn = environ.get('SCRIPT_NAME', '') + path = environ.get('PATH_INFO', '') + qs = environ.get('QUERY_STRING', '') + + # Add the *previous* path_info + qs to redirections. + old_uri = sn + path + if qs: + old_uri += '?' + qs + redirections.append(old_uri) + + if not self.recursive: + # Check to see if the new URI has been redirected to + # already + new_uri = sn + ir.path + if ir.query_string: + new_uri += '?' + ir.query_string + if new_uri in redirections: + ir.request.close() + tmpl = ( + 'InternalRedirector visited the same URL twice: %r' + ) + raise RuntimeError(tmpl % new_uri) + + # Munge the environment and try again. + environ['REQUEST_METHOD'] = 'GET' + environ['PATH_INFO'] = ir.path + environ['QUERY_STRING'] = ir.query_string + environ['wsgi.input'] = io.BytesIO() + environ['CONTENT_LENGTH'] = '0' + environ['cherrypy.previous_request'] = ir.request + + +class ExceptionTrapper(object): + + """WSGI middleware that traps exceptions.""" + + def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): + self.nextapp = nextapp + self.throws = throws + + def __call__(self, environ, start_response): + return _TrappedResponse( + self.nextapp, + environ, + start_response, + self.throws + ) + + +class _TrappedResponse(object): + + response = iter([]) + + def __init__(self, nextapp, environ, start_response, throws): + self.nextapp = nextapp + self.environ = environ + self.start_response = start_response + self.throws = throws + self.started_response = False + self.response = self.trap( + self.nextapp, self.environ, self.start_response, + ) + self.iter_response = iter(self.response) + + def __iter__(self): + self.started_response = True + return self + + def __next__(self): + return self.trap(next, self.iter_response) + + # todo: https://pythonhosted.org/six/#six.Iterator + if six.PY2: + next = __next__ + + def close(self): + if hasattr(self.response, 'close'): + self.response.close() + + def trap(self, func, *args, **kwargs): + try: + return func(*args, **kwargs) + except self.throws: + raise + except StopIteration: + raise + except Exception: + tb = _cperror.format_exc() + _cherrypy.log(tb, severity=40) + if not _cherrypy.request.show_tracebacks: + tb = '' + s, h, b = _cperror.bare_error(tb) + if six.PY3: + # What fun. + s = s.decode('ISO-8859-1') + h = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in h + ] + if self.started_response: + # Empty our iterable (so future calls raise StopIteration) + self.iter_response = iter([]) + else: + self.iter_response = iter(b) + + try: + self.start_response(s, h, _sys.exc_info()) + except Exception: + # "The application must not trap any exceptions raised by + # start_response, if it called start_response with exc_info. + # Instead, it should allow such exceptions to propagate + # back to the server or gateway." + # But we still log and call close() to clean up ourselves. + _cherrypy.log(traceback=True, severity=40) + raise + + if self.started_response: + return b''.join(b) + else: + return b + + +# WSGI-to-CP Adapter # + + +class AppResponse(object): + + """WSGI response iterable for CherryPy applications.""" + + def __init__(self, environ, start_response, cpapp): + self.cpapp = cpapp + try: + if six.PY2: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + environ = downgrade_wsgi_ux_to_1x(environ) + self.environ = environ + self.run() + + r = _cherrypy.serving.response + + outstatus = r.output_status + if not isinstance(outstatus, bytes): + raise TypeError('response.output_status is not a byte string.') + + outheaders = [] + for k, v in r.header_list: + if not isinstance(k, bytes): + tmpl = 'response.header_list key %r is not a byte string.' + raise TypeError(tmpl % k) + if not isinstance(v, bytes): + tmpl = ( + 'response.header_list value %r is not a byte string.' + ) + raise TypeError(tmpl % v) + outheaders.append((k, v)) + + if six.PY3: + # According to PEP 3333, when using Python 3, the response + # status and headers must be bytes masquerading as unicode; + # that is, they must be of type "str" but are restricted to + # code points in the "latin-1" set. + outstatus = outstatus.decode('ISO-8859-1') + outheaders = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in outheaders + ] + + self.iter_response = iter(r.body) + self.write = start_response(outstatus, outheaders) + except BaseException: + self.close() + raise + + def __iter__(self): + return self + + def __next__(self): + return next(self.iter_response) + + # todo: https://pythonhosted.org/six/#six.Iterator + if six.PY2: + next = __next__ + + def close(self): + """Close and de-reference the current request and response. (Core)""" + streaming = _cherrypy.serving.response.stream + self.cpapp.release_serving() + + # We avoid the expense of examining the iterator to see if it's + # closable unless we are streaming the response, as that's the + # only situation where we are going to have an iterator which + # may not have been exhausted yet. + if streaming and is_closable_iterator(self.iter_response): + iter_close = self.iter_response.close + try: + iter_close() + except Exception: + _cherrypy.log(traceback=True, severity=40) + + def run(self): + """Create a Request object using environ.""" + env = self.environ.get + + local = httputil.Host( + '', + int(env('SERVER_PORT', 80) or -1), + env('SERVER_NAME', ''), + ) + remote = httputil.Host( + env('REMOTE_ADDR', ''), + int(env('REMOTE_PORT', -1) or -1), + env('REMOTE_HOST', ''), + ) + scheme = env('wsgi.url_scheme') + sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1') + request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) + + # LOGON_USER is served by IIS, and is the name of the + # user after having been mapped to a local account. + # Both IIS and Apache set REMOTE_USER, when possible. + request.login = env('LOGON_USER') or env('REMOTE_USER') or None + request.multithread = self.environ['wsgi.multithread'] + request.multiprocess = self.environ['wsgi.multiprocess'] + request.wsgi_environ = self.environ + request.prev = env('cherrypy.previous_request', None) + + meth = self.environ['REQUEST_METHOD'] + + path = httputil.urljoin( + self.environ.get('SCRIPT_NAME', ''), + self.environ.get('PATH_INFO', ''), + ) + qs = self.environ.get('QUERY_STRING', '') + + path, qs = self.recode_path_qs(path, qs) or (path, qs) + + rproto = self.environ.get('SERVER_PROTOCOL') + headers = self.translate_headers(self.environ) + rfile = self.environ['wsgi.input'] + request.run(meth, path, qs, rproto, headers, rfile) + + headerNames = { + 'HTTP_CGI_AUTHORIZATION': 'Authorization', + 'CONTENT_LENGTH': 'Content-Length', + 'CONTENT_TYPE': 'Content-Type', + 'REMOTE_HOST': 'Remote-Host', + 'REMOTE_ADDR': 'Remote-Addr', + } + + def recode_path_qs(self, path, qs): + if not six.PY3: + return + + # This isn't perfect; if the given PATH_INFO is in the + # wrong encoding, it may fail to match the appropriate config + # section URI. But meh. + old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') + new_enc = self.cpapp.find_config( + self.environ.get('PATH_INFO', ''), + 'request.uri_encoding', 'utf-8', + ) + if new_enc.lower() == old_enc.lower(): + return + + # Even though the path and qs are unicode, the WSGI server + # is required by PEP 3333 to coerce them to ISO-8859-1 + # masquerading as unicode. So we have to encode back to + # bytes and then decode again using the "correct" encoding. + try: + return ( + path.encode(old_enc).decode(new_enc), + qs.encode(old_enc).decode(new_enc), + ) + except (UnicodeEncodeError, UnicodeDecodeError): + # Just pass them through without transcoding and hope. + pass + + def translate_headers(self, environ): + """Translate CGI-environ header names to HTTP header names.""" + for cgiName in environ: + # We assume all incoming header keys are uppercase already. + if cgiName in self.headerNames: + yield self.headerNames[cgiName], environ[cgiName] + elif cgiName[:5] == 'HTTP_': + # Hackish attempt at recovering original header names. + translatedHeader = cgiName[5:].replace('_', '-') + yield translatedHeader, environ[cgiName] + + +class CPWSGIApp(object): + + """A WSGI application object for a CherryPy Application.""" + + pipeline = [ + ('ExceptionTrapper', ExceptionTrapper), + ('InternalRedirector', InternalRedirector), + ] + """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a + constructor that takes an initial, positional 'nextapp' argument, + plus optional keyword arguments, and returns a WSGI application + (that takes environ and start_response arguments). The 'name' can + be any you choose, and will correspond to keys in self.config.""" + + head = None + """Rather than nest all apps in the pipeline on each call, it's only + done the first time, and the result is memoized into self.head. Set + this to None again if you change self.pipeline after calling self.""" + + config = {} + """A dict whose keys match names listed in the pipeline. Each + value is a further dict which will be passed to the corresponding + named WSGI callable (from the pipeline) as keyword arguments.""" + + response_class = AppResponse + """The class to instantiate and return as the next app in the WSGI chain. + """ + + def __init__(self, cpapp, pipeline=None): + self.cpapp = cpapp + self.pipeline = self.pipeline[:] + if pipeline: + self.pipeline.extend(pipeline) + self.config = self.config.copy() + + def tail(self, environ, start_response): + """WSGI application callable for the actual CherryPy application. + + You probably shouldn't call this; call self.__call__ instead, + so that any WSGI middleware in self.pipeline can run first. + """ + return self.response_class(environ, start_response, self.cpapp) + + def __call__(self, environ, start_response): + head = self.head + if head is None: + # Create and nest the WSGI apps in our pipeline (in reverse order). + # Then memoize the result in self.head. + head = self.tail + for name, callable in self.pipeline[::-1]: + conf = self.config.get(name, {}) + head = callable(head, **conf) + self.head = head + return head(environ, start_response) + + def namespace_handler(self, k, v): + """Config handler for the 'wsgi' namespace.""" + if k == 'pipeline': + # Note this allows multiple 'wsgi.pipeline' config entries + # (but each entry will be processed in a 'random' order). + # It should also allow developers to set default middleware + # in code (passed to self.__init__) that deployers can add to + # (but not remove) via config. + self.pipeline.extend(v) + elif k == 'response_class': + self.response_class = v + else: + name, arg = k.split('.', 1) + bucket = self.config.setdefault(name, {}) + bucket[arg] = v diff --git a/libraries/cherrypy/_cpwsgi_server.py b/libraries/cherrypy/_cpwsgi_server.py new file mode 100644 index 00000000..11dd846a --- /dev/null +++ b/libraries/cherrypy/_cpwsgi_server.py @@ -0,0 +1,110 @@ +""" +WSGI server interface (see PEP 333). + +This adds some CP-specific bits to the framework-agnostic cheroot package. +""" +import sys + +import cheroot.wsgi +import cheroot.server + +import cherrypy + + +class CPWSGIHTTPRequest(cheroot.server.HTTPRequest): + """Wrapper for cheroot.server.HTTPRequest. + + This is a layer, which preserves URI parsing mode like it which was + before Cheroot v5.8.0. + """ + + def __init__(self, server, conn): + """Initialize HTTP request container instance. + + Args: + server (cheroot.server.HTTPServer): + web server object receiving this request + conn (cheroot.server.HTTPConnection): + HTTP connection object for this request + """ + super(CPWSGIHTTPRequest, self).__init__( + server, conn, proxy_mode=True + ) + + +class CPWSGIServer(cheroot.wsgi.Server): + """Wrapper for cheroot.wsgi.Server. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. Therefore, + we wrap it here, so we can set our own mount points from cherrypy.tree + and apply some attributes from config -> cherrypy.server -> wsgi.Server. + """ + + fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' + version = fmt.format(**globals()) + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPWSGIServer instance. + + Args: + server_adapter (cherrypy._cpserver.Server): ... + """ + self.server_adapter = server_adapter + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0 + ) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0 + ) + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + self.wsgi_version = self.server_adapter.wsgi_version + + super(CPWSGIServer, self).__init__( + server_adapter.bind_addr, cherrypy.tree, + self.server_adapter.thread_pool, + server_name, + max=self.server_adapter.thread_pool_max, + request_queue_size=self.server_adapter.socket_queue_size, + timeout=self.server_adapter.socket_timeout, + shutdown_timeout=self.server_adapter.shutdown_timeout, + accepted_queue_size=self.server_adapter.accepted_queue_size, + accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, + peercreds_enabled=self.server_adapter.peercreds, + peercreds_resolve_enabled=self.server_adapter.peercreds_resolve, + ) + self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest + + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + if sys.version_info >= (3, 0): + ssl_module = self.server_adapter.ssl_module or 'builtin' + else: + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + + self.stats['Enabled'] = getattr( + self.server_adapter, 'statistics', False) + + def error_log(self, msg='', level=20, traceback=False): + """Write given message to the error log.""" + cherrypy.engine.log(msg, level, traceback) diff --git a/libraries/cherrypy/_helper.py b/libraries/cherrypy/_helper.py new file mode 100644 index 00000000..314550cb --- /dev/null +++ b/libraries/cherrypy/_helper.py @@ -0,0 +1,344 @@ +"""Helper functions for CP apps.""" + +import six +from six.moves import urllib + +from cherrypy._cpcompat import text_or_bytes + +import cherrypy + + +def expose(func=None, alias=None): + """Expose the function or class. + + Optionally provide an alias or set of aliases. + """ + def expose_(func): + func.exposed = True + if alias is not None: + if isinstance(alias, text_or_bytes): + parents[alias.replace('.', '_')] = func + else: + for a in alias: + parents[a.replace('.', '_')] = func + return func + + import sys + import types + decoratable_types = types.FunctionType, types.MethodType, type, + if six.PY2: + # Old-style classes are type types.ClassType. + decoratable_types += types.ClassType, + if isinstance(func, decoratable_types): + if alias is None: + # @expose + func.exposed = True + return func + else: + # func = expose(func, alias) + parents = sys._getframe(1).f_locals + return expose_(func) + elif func is None: + if alias is None: + # @expose() + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose(alias="alias") or + # @expose(alias=["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose("alias") or + # @expose(["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + alias = func + return expose_ + + +def popargs(*args, **kwargs): + """Decorate _cp_dispatch. + + (cherrypy.dispatch.Dispatcher.dispatch_method_name) + + Optional keyword argument: handler=(Object or Function) + + Provides a _cp_dispatch function that pops off path segments into + cherrypy.request.params under the names specified. The dispatch + is then forwarded on to the next vpath element. + + Note that any existing (and exposed) member function of the class that + popargs is applied to will override that value of the argument. For + instance, if you have a method named "list" on the class decorated with + popargs, then accessing "/list" will call that function instead of popping + it off as the requested parameter. This restriction applies to all + _cp_dispatch functions. The only way around this restriction is to create + a "blank class" whose only function is to provide _cp_dispatch. + + If there are path elements after the arguments, or more arguments + are requested than are available in the vpath, then the 'handler' + keyword argument specifies the next object to handle the parameterized + request. If handler is not specified or is None, then self is used. + If handler is a function rather than an instance, then that function + will be called with the args specified and the return value from that + function used as the next object INSTEAD of adding the parameters to + cherrypy.request.args. + + This decorator may be used in one of two ways: + + As a class decorator: + @cherrypy.popargs('year', 'month', 'day') + class Blog: + def index(self, year=None, month=None, day=None): + #Process the parameters here; any url like + #/, /2009, /2009/12, or /2009/12/31 + #will fill in the appropriate parameters. + + def create(self): + #This link will still be available at /create. Defined functions + #take precedence over arguments. + + Or as a member of a class: + class Blog: + _cp_dispatch = cherrypy.popargs('year', 'month', 'day') + #... + + The handler argument may be used to mix arguments with built in functions. + For instance, the following setup allows different activities at the + day, month, and year level: + + class DayHandler: + def index(self, year, month, day): + #Do something with this day; probably list entries + + def delete(self, year, month, day): + #Delete all entries for this day + + @cherrypy.popargs('day', handler=DayHandler()) + class MonthHandler: + def index(self, year, month): + #Do something with this month; probably list entries + + def delete(self, year, month): + #Delete all entries for this month + + @cherrypy.popargs('month', handler=MonthHandler()) + class YearHandler: + def index(self, year): + #Do something with this year + + #... + + @cherrypy.popargs('year', handler=YearHandler()) + class Root: + def index(self): + #... + + """ + # Since keyword arg comes after *args, we have to process it ourselves + # for lower versions of python. + + handler = None + handler_call = False + for k, v in kwargs.items(): + if k == 'handler': + handler = v + else: + tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'" + raise TypeError(tm.format(k)) + + import inspect + + if handler is not None \ + and (hasattr(handler, '__call__') or inspect.isclass(handler)): + handler_call = True + + def decorated(cls_or_self=None, vpath=None): + if inspect.isclass(cls_or_self): + # cherrypy.popargs is a class decorator + cls = cls_or_self + name = cherrypy.dispatch.Dispatcher.dispatch_method_name + setattr(cls, name, decorated) + return cls + + # We're in the actual function + self = cls_or_self + parms = {} + for arg in args: + if not vpath: + break + parms[arg] = vpath.pop(0) + + if handler is not None: + if handler_call: + return handler(**parms) + else: + cherrypy.request.params.update(parms) + return handler + + cherrypy.request.params.update(parms) + + # If we are the ultimate handler, then to prevent our _cp_dispatch + # from being called again, we will resolve remaining elements through + # getattr() directly. + if vpath: + return getattr(self, vpath.pop(0), None) + else: + return self + + return decorated + + +def url(path='', qs='', script_name=None, base=None, relative=None): + """Create an absolute URL for the given path. + + If 'path' starts with a slash ('/'), this will return + (base + script_name + path + qs). + If it does not start with a slash, this returns + (base + script_name [+ request.path_info] + path + qs). + + If script_name is None, cherrypy.request will be used + to find a script_name, if available. + + If base is None, cherrypy.request.base will be used (if available). + Note that you can use cherrypy.tools.proxy to change this. + + Finally, note that this function can be used to obtain an absolute URL + for the current request path (minus the querystring) by passing no args. + If you call url(qs=cherrypy.request.query_string), you should get the + original browser URL (assuming no internal redirections). + + If relative is None or not provided, request.app.relative_urls will + be used (if available, else False). If False, the output will be an + absolute URL (including the scheme, host, vhost, and script_name). + If True, the output will instead be a URL that is relative to the + current request path, perhaps including '..' atoms. If relative is + the string 'server', the output will instead be a URL that is + relative to the server root; i.e., it will start with a slash. + """ + if isinstance(qs, (tuple, list, dict)): + qs = urllib.parse.urlencode(qs) + if qs: + qs = '?' + qs + + if cherrypy.request.app: + if not path.startswith('/'): + # Append/remove trailing slash from path_info as needed + # (this is to support mistyped URL's without redirecting; + # if you want to redirect, use tools.trailing_slash). + pi = cherrypy.request.path_info + if cherrypy.request.is_index is True: + if not pi.endswith('/'): + pi = pi + '/' + elif cherrypy.request.is_index is False: + if pi.endswith('/') and pi != '/': + pi = pi[:-1] + + if path == '': + path = pi + else: + path = urllib.parse.urljoin(pi, path) + + if script_name is None: + script_name = cherrypy.request.script_name + if base is None: + base = cherrypy.request.base + + newurl = base + script_name + normalize_path(path) + qs + else: + # No request.app (we're being called outside a request). + # We'll have to guess the base from server.* attributes. + # This will produce very different results from the above + # if you're using vhosts or tools.proxy. + if base is None: + base = cherrypy.server.base() + + path = (script_name or '') + path + newurl = base + normalize_path(path) + qs + + # At this point, we should have a fully-qualified absolute URL. + + if relative is None: + relative = getattr(cherrypy.request.app, 'relative_urls', False) + + # See http://www.ietf.org/rfc/rfc2396.txt + if relative == 'server': + # "A relative reference beginning with a single slash character is + # termed an absolute-path reference, as defined by <abs_path>..." + # This is also sometimes called "server-relative". + newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) + elif relative: + # "A relative reference that does not begin with a scheme name + # or a slash character is termed a relative-path reference." + old = url(relative=False).split('/')[:-1] + new = newurl.split('/') + while old and new: + a, b = old[0], new[0] + if a != b: + break + old.pop(0) + new.pop(0) + new = (['..'] * len(old)) + new + newurl = '/'.join(new) + + return newurl + + +def normalize_path(path): + """Resolve given path from relative into absolute form.""" + if './' not in path: + return path + + # Normalize the URL by removing ./ and ../ + atoms = [] + for atom in path.split('/'): + if atom == '.': + pass + elif atom == '..': + # Don't pop from empty list + # (i.e. ignore redundant '..') + if atoms: + atoms.pop() + elif atom: + atoms.append(atom) + + newpath = '/'.join(atoms) + # Preserve leading '/' + if path.startswith('/'): + newpath = '/' + newpath + + return newpath + + +#### +# Inlined from jaraco.classes 1.4.3 +# Ref #1673 +class _ClassPropertyDescriptor(object): + """Descript for read-only class-based property. + + Turns a classmethod-decorated func into a read-only property of that class + type (means the value cannot be set). + """ + + def __init__(self, fget, fset=None): + """Initialize a class property descriptor. + + Instantiated by ``_helper.classproperty``. + """ + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + """Return property value.""" + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + +def classproperty(func): # noqa: D401; irrelevant for properties + """Decorator like classmethod to implement a static class property.""" + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return _ClassPropertyDescriptor(func) +#### diff --git a/libraries/cherrypy/daemon.py b/libraries/cherrypy/daemon.py new file mode 100644 index 00000000..74488c06 --- /dev/null +++ b/libraries/cherrypy/daemon.py @@ -0,0 +1,107 @@ +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec('import %s' % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error('You may only specify one of the cgi, fastcgi, and ' + 'scgi options.', 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload.on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + cls = ( + servers.FlupFCGIServer if fastcgi else + servers.FlupSCGIServer if scgi else + servers.FlupCGIServer + ) + f = cls(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except Exception: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +def run(): + """Run cherryd CLI.""" + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action='append', dest='config', + help='specify config file(s)') + p.add_option('-d', action='store_true', dest='daemonize', + help='run the server as a daemon') + p.add_option('-e', '--environment', dest='environment', default=None, + help='apply the given config environment') + p.add_option('-f', action='store_true', dest='fastcgi', + help='start a fastcgi server instead of the default HTTP ' + 'server') + p.add_option('-s', action='store_true', dest='scgi', + help='start a scgi server instead of the default HTTP server') + p.add_option('-x', action='store_true', dest='cgi', + help='start a cgi server instead of the default HTTP server') + p.add_option('-i', '--import', action='append', dest='imports', + help='specify modules to import') + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help='store the process id in the given file') + p.add_option('-P', '--Path', action='append', dest='Path', + help='add the given paths to sys.path') + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) diff --git a/libraries/cherrypy/favicon.ico b/libraries/cherrypy/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0d7e61badad3f332cf1e663efb97c0b5be80f5e GIT binary patch literal 1406 zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K zC3>Y)vZNK!rhR_zJd<bU|M}kMoO{0Cd!GB;KkoU0NLVT=2)RAxa?lm%Cxjr;TL_un z3VorFg&_WomX;PQBmFpaDwn?M(PWhuk)2hAmSzH76vOeACShffNm)rfo~iK|`@7<l z97Rw-F{L$?MC3~`w{s_5nul444-L&tm<D=KQCCfMeH|u7X&CxBQCVF}Ku!kswmEpk zq+ns~j;2N&v4wf)=_FFu*od~b4Gs0p1m~oYTv$M3V++>tfiw$m?BBmX0|pFW;J|@s zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO` z$*x_y*u8r<d-m*M@7}%a+qVyEYisuJ-_L;q2e7fR!PeFmJ3BiL9z4jQLx-@px99NT z!yGws1P2EPjvhVAv17+Le*8F&j*gr-aRMhNCr+L`Dg2pJoIZV;GiT1=?Cgw-iwmx< zuDH3m;qLCv*|TTy@bJLX(-SW*FV3Ai$NBT;xp3hE-rn9^ym*mImoDMs<HP04m$`D~ z3ckL+T)ldgYuB!E{rYwM{QS6a;|4cx-sIM;Tim{V8-IU)?%cUUKtKS2fq~q;dzYY~ zAnx6}M{sa3At52$zki?5&``p{!Uzu!Cn6$($jC^dqN0e7jwU81hS=Cx;^N|nkB=uI zA%O=E9`NwtLmoYP#N)@0NlZ*6DJh93Po9vRoJ>ke3QwOtB{embw6rwR)6;qO>=_vu z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQ<p`f6E zw{PF_?%g}yzkkn%4<9HjEac<Ij}#RZQCwV1Nl6K%rKOaWl~G<^&ZkeG`26`ZU%q^y zqN0MYU%&G0+czpJE2*lgqPn`8nwlDFYis%b{X2Dab=23_)6mc$%v2*yO-(d6HzS*U z0dgG$&-ej}LNBpc{SO)MBTEYMVzF4hjZi6d5R#;wK*i!-5`O<pW+8GyxA@oY?c};D z_D~ayM<VcE>T(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz? zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg Kbp8+jKgeIP@z;_7 literal 0 HcmV?d00001 diff --git a/libraries/cherrypy/lib/__init__.py b/libraries/cherrypy/lib/__init__.py new file mode 100644 index 00000000..f815f76a --- /dev/null +++ b/libraries/cherrypy/lib/__init__.py @@ -0,0 +1,96 @@ +"""CherryPy Library.""" + + +def is_iterator(obj): + """Detect if the object provided implements the iterator protocol. + + (i.e. like a generator). + + This will return False for objects which are iterable, + but not iterators themselves. + """ + from types import GeneratorType + if isinstance(obj, GeneratorType): + return True + elif not hasattr(obj, '__iter__'): + return False + else: + # Types which implement the protocol must return themselves when + # invoking 'iter' upon them. + return iter(obj) is obj + + +def is_closable_iterator(obj): + """Detect if the given object is both closable and iterator.""" + # Not an iterator. + if not is_iterator(obj): + return False + + # A generator - the easiest thing to deal with. + import inspect + if inspect.isgenerator(obj): + return True + + # A custom iterator. Look for a close method... + if not (hasattr(obj, 'close') and callable(obj.close)): + return False + + # ... which doesn't require any arguments. + try: + inspect.getcallargs(obj.close) + except TypeError: + return False + else: + return True + + +class file_generator(object): + """Yield the given input (a file object) in chunks (default 64k). + + (Core) + """ + + def __init__(self, input, chunkSize=65536): + """Initialize file_generator with file ``input`` for chunked access.""" + self.input = input + self.chunkSize = chunkSize + + def __iter__(self): + """Return iterator.""" + return self + + def __next__(self): + """Return next chunk of file.""" + chunk = self.input.read(self.chunkSize) + if chunk: + return chunk + else: + if hasattr(self.input, 'close'): + self.input.close() + raise StopIteration() + next = __next__ + + +def file_generator_limited(fileobj, count, chunk_size=65536): + """Yield the given file object in chunks. + + Stopps after `count` bytes has been emitted. + Default chunk size is 64kB. (Core) + """ + remaining = count + while remaining > 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + + +def set_vary_header(response, header_name): + """Add a Vary header to a response.""" + varies = response.headers.get('Vary', '') + varies = [x.strip() for x in varies.split(',') if x.strip()] + if header_name not in varies: + varies.append(header_name) + response.headers['Vary'] = ', '.join(varies) diff --git a/libraries/cherrypy/lib/auth_basic.py b/libraries/cherrypy/lib/auth_basic.py new file mode 100644 index 00000000..ad379a26 --- /dev/null +++ b/libraries/cherrypy/lib/auth_basic.py @@ -0,0 +1,120 @@ +# This file is part of CherryPy <http://www.cherrypy.org/> +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Basic Authentication tool. + +This module provides a CherryPy 3.x tool which implements +the server-side of HTTP Basic Access Authentication, as described in +:rfc:`2617`. + +Example usage, using the built-in checkpassword_dict function which uses a dict +as the credentials store:: + + userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} + checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) + basic_auth = {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'earth', + 'tools.auth_basic.checkpassword': checkpassword, + 'tools.auth_basic.accept_charset': 'UTF-8', + } + app_config = { '/' : basic_auth } + +""" + +import binascii +import unicodedata +import base64 + +import cherrypy +from cherrypy._cpcompat import ntou, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def checkpassword_dict(user_password_dict): + """Returns a checkpassword function which checks credentials + against a dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, use + checkpassword_dict(my_credentials_dict) as the value for the + checkpassword argument to basic_auth(). + """ + def checkpassword(realm, user, password): + p = user_password_dict.get(user) + return p and p == password or False + + return checkpassword + + +def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'): + """A CherryPy tool which hooks at before_handler to perform + HTTP Basic Access Authentication, as specified in :rfc:`2617` + and :rfc:`7617`. + + If the request has an 'authorization' header with a 'Basic' scheme, this + tool attempts to authenticate the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not 'Basic', or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Basic header. + + realm + A string containing the authentication realm. + + checkpassword + A callable which checks the authentication credentials. + Its signature is checkpassword(realm, username, password). where + username and password are the values obtained from the request's + 'authorization' header. If authentication succeeds, checkpassword + returns True, else it returns False. + + """ + + fallback_charset = 'ISO-8859-1' + + if '"' in realm: + raise ValueError('Realm cannot contain the " (quote) character.') + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + if auth_header is not None: + # split() error, base64.decodestring() error + msg = 'Bad Request' + with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg): + scheme, params = auth_header.split(' ', 1) + if scheme.lower() == 'basic': + charsets = accept_charset, fallback_charset + decoded_params = base64.b64decode(params.encode('ascii')) + decoded_params = _try_decode(decoded_params, charsets) + decoded_params = ntou(decoded_params) + decoded_params = unicodedata.normalize('NFC', decoded_params) + decoded_params = tonative(decoded_params) + username, password = decoded_params.split(':', 1) + if checkpassword(realm, username, password): + if debug: + cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') + request.login = username + return # successful authentication + + charset = accept_charset.upper() + charset_declaration = ( + (', charset="%s"' % charset) + if charset != fallback_charset + else '' + ) + # Respond with 401 status and a WWW-Authenticate header + cherrypy.serving.response.headers['www-authenticate'] = ( + 'Basic realm="%s"%s' % (realm, charset_declaration) + ) + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') + + +def _try_decode(subject, charsets): + for charset in charsets[:-1]: + try: + return tonative(subject, charset) + except ValueError: + pass + return tonative(subject, charsets[-1]) diff --git a/libraries/cherrypy/lib/auth_digest.py b/libraries/cherrypy/lib/auth_digest.py new file mode 100644 index 00000000..9b4f55c8 --- /dev/null +++ b/libraries/cherrypy/lib/auth_digest.py @@ -0,0 +1,464 @@ +# This file is part of CherryPy <http://www.cherrypy.org/> +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Digest Authentication tool. + +An implementation of the server-side of HTTP Digest Access +Authentication, which is described in :rfc:`2617`. + +Example usage, using the built-in get_ha1_dict_plain function which uses a dict +of plaintext passwords as the credentials store:: + + userpassdict = {'alice' : '4x5istwelve'} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) + digest_auth = {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'wonderland', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.accept_charset': 'UTF-8', + } + app_config = { '/' : digest_auth } +""" + +import time +import functools +from hashlib import md5 + +from six.moves.urllib.request import parse_http_list, parse_keqv_list + +import cherrypy +from cherrypy._cpcompat import ntob, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def md5_hex(s): + return md5(ntob(s, 'utf-8')).hexdigest() + + +qop_auth = 'auth' +qop_auth_int = 'auth-int' +valid_qops = (qop_auth, qop_auth_int) + +valid_algorithms = ('MD5', 'MD5-sess') + +FALLBACK_CHARSET = 'ISO-8859-1' +DEFAULT_CHARSET = 'UTF-8' + + +def TRACE(msg): + cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') + +# Three helper functions for users of the tool, providing three variants +# of get_ha1() functions for three different kinds of credential stores. + + +def get_ha1_dict_plain(user_password_dict): + """Returns a get_ha1 function which obtains a plaintext password from a + dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, with plaintext + passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the + get_ha1 argument to digest_auth(). + """ + def get_ha1(realm, username): + password = user_password_dict.get(username) + if password: + return md5_hex('%s:%s:%s' % (username, realm, password)) + return None + + return get_ha1 + + +def get_ha1_dict(user_ha1_dict): + """Returns a get_ha1 function which obtains a HA1 password hash from a + dictionary of the form: {username : HA1}. + + If you want a dictionary-based authentication scheme, but with + pre-computed HA1 hashes instead of plain-text passwords, use + get_ha1_dict(my_userha1_dict) as the value for the get_ha1 + argument to digest_auth(). + """ + def get_ha1(realm, username): + return user_ha1_dict.get(username) + + return get_ha1 + + +def get_ha1_file_htdigest(filename): + """Returns a get_ha1 function which obtains a HA1 password hash from a + flat file with lines of the same format as that produced by the Apache + htdigest utility. For example, for realm 'wonderland', username 'alice', + and password '4x5istwelve', the htdigest line would be:: + + alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c + + If you want to use an Apache htdigest file as the credentials store, + then use get_ha1_file_htdigest(my_htdigest_file) as the value for the + get_ha1 argument to digest_auth(). It is recommended that the filename + argument be an absolute path, to avoid problems. + """ + 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() + return result + + return get_ha1 + + +def synthesize_nonce(s, key, timestamp=None): + """Synthesize a nonce value which resists spoofing and can be checked + for staleness. Returns a string suitable as the value for 'nonce' in + the www-authenticate header. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + timestamp + An integer seconds-since-the-epoch timestamp + + """ + if timestamp is None: + timestamp = int(time.time()) + h = md5_hex('%s:%s:%s' % (timestamp, s, key)) + nonce = '%s:%s' % (timestamp, h) + return nonce + + +def H(s): + """The hash function H""" + return md5_hex(s) + + +def _try_decode_header(header, charset): + global FALLBACK_CHARSET + + for enc in (charset, FALLBACK_CHARSET): + try: + return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc) + except ValueError as ve: + last_err = ve + else: + raise last_err + + +class HttpDigestAuthorization(object): + """ + Parses a Digest Authorization header and performs + re-calculation of the digest. + """ + + scheme = 'digest' + + def errmsg(self, s): + return 'Digest Authorization header: %s' % s + + @classmethod + def matches(cls, header): + scheme, _, _ = header.partition(' ') + return scheme.lower() == cls.scheme + + def __init__( + self, auth_header, http_method, + debug=False, accept_charset=DEFAULT_CHARSET[:], + ): + self.http_method = http_method + self.debug = debug + + if not self.matches(auth_header): + raise ValueError('Authorization scheme is not "Digest"') + + self.auth_header = _try_decode_header(auth_header, accept_charset) + + scheme, params = self.auth_header.split(' ', 1) + + # make a dict of the params + items = parse_http_list(params) + paramsd = parse_keqv_list(items) + + self.realm = paramsd.get('realm') + self.username = paramsd.get('username') + self.nonce = paramsd.get('nonce') + self.uri = paramsd.get('uri') + self.method = paramsd.get('method') + self.response = paramsd.get('response') # the response digest + self.algorithm = paramsd.get('algorithm', 'MD5').upper() + self.cnonce = paramsd.get('cnonce') + self.opaque = paramsd.get('opaque') + self.qop = paramsd.get('qop') # qop + self.nc = paramsd.get('nc') # nonce count + + # perform some correctness checks + if self.algorithm not in valid_algorithms: + raise ValueError( + self.errmsg("Unsupported value for algorithm: '%s'" % + self.algorithm)) + + has_reqd = ( + self.username and + self.realm and + self.nonce and + self.uri and + self.response + ) + if not has_reqd: + raise ValueError( + self.errmsg('Not all required parameters are present.')) + + if self.qop: + if self.qop not in valid_qops: + raise ValueError( + self.errmsg("Unsupported value for qop: '%s'" % self.qop)) + if not (self.cnonce and self.nc): + raise ValueError( + self.errmsg('If qop is sent then ' + 'cnonce and nc MUST be present')) + else: + if self.cnonce or self.nc: + raise ValueError( + self.errmsg('If qop is not sent, ' + 'neither cnonce nor nc can be present')) + + def __str__(self): + return 'authorization : %s' % self.auth_header + + def validate_nonce(self, s, key): + """Validate the nonce. + Returns True if nonce was generated by synthesize_nonce() and the + timestamp is not spoofed, else returns False. + + s + A string related to the resource, such as the hostname of + the server. + + key + A secret string known only to the server. + + Both s and key must be the same values which were used to synthesize + the nonce we are trying to validate. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + s_timestamp, s_hashpart = synthesize_nonce( + s, key, timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + if self.debug: + TRACE('validate_nonce: %s' % is_valid) + return is_valid + except ValueError: # split() error + pass + return False + + def is_nonce_stale(self, max_age_seconds=600): + """Returns True if a validated nonce is stale. The nonce contains a + timestamp in plaintext and also a secure hash of the timestamp. + You should first validate the nonce to ensure the plaintext + timestamp is not spoofed. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + if int(timestamp) + max_age_seconds > int(time.time()): + return False + except ValueError: # int() error + pass + if self.debug: + TRACE('nonce is stale') + return True + + def HA2(self, entity_body=''): + """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" + # RFC 2617 3.2.2.3 + # If the "qop" directive's value is "auth" or is unspecified, + # then A2 is: + # A2 = method ":" digest-uri-value + # + # If the "qop" value is "auth-int", then A2 is: + # A2 = method ":" digest-uri-value ":" H(entity-body) + if self.qop is None or self.qop == 'auth': + a2 = '%s:%s' % (self.http_method, self.uri) + elif self.qop == 'auth-int': + a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) + else: + # in theory, this should never happen, since I validate qop in + # __init__() + raise ValueError(self.errmsg('Unrecognized value for qop!')) + return H(a2) + + def request_digest(self, ha1, entity_body=''): + """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. + + ha1 + The HA1 string obtained from the credentials store. + + entity_body + If 'qop' is set to 'auth-int', then A2 includes a hash + of the "entity body". The entity body is the part of the + message which follows the HTTP headers. See :rfc:`2617` section + 4.3. This refers to the entity the user agent sent in the + request which has the Authorization header. Typically GET + requests don't have an entity, and POST requests do. + + """ + ha2 = self.HA2(entity_body) + # Request-Digest -- RFC 2617 3.2.2.1 + if self.qop: + req = '%s:%s:%s:%s:%s' % ( + self.nonce, self.nc, self.cnonce, self.qop, ha2) + else: + req = '%s:%s' % (self.nonce, ha2) + + # RFC 2617 3.2.2.2 + # + # If the "algorithm" directive's value is "MD5" or is unspecified, + # then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + # + # If the "algorithm" directive's value is "MD5-sess", then A1 is + # calculated only once - on the first request by the client following + # receipt of a WWW-Authenticate challenge from the server. + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + if self.algorithm == 'MD5-sess': + ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) + + digest = H('%s:%s' % (ha1, req)) + return digest + + +def _get_charset_declaration(charset): + global FALLBACK_CHARSET + charset = charset.upper() + return ( + (', charset="%s"' % charset) + if charset != FALLBACK_CHARSET + else '' + ) + + +def www_authenticate( + realm, key, algorithm='MD5', nonce=None, qop=qop_auth, + stale=False, accept_charset=DEFAULT_CHARSET[:], +): + """Constructs a WWW-Authenticate header for Digest authentication.""" + if qop not in valid_qops: + raise ValueError("Unsupported value for qop: '%s'" % qop) + if algorithm not in valid_algorithms: + raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) + + HEADER_PATTERN = ( + 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s' + ) + + if nonce is None: + nonce = synthesize_nonce(realm, key) + + stale_param = ', stale="true"' if stale else '' + + charset_declaration = _get_charset_declaration(accept_charset) + + return HEADER_PATTERN % ( + realm, nonce, algorithm, qop, stale_param, charset_declaration, + ) + + +def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'): + """A CherryPy tool that hooks at before_handler to perform + HTTP Digest Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Digest' scheme, + this tool authenticates the credentials supplied in that header. + If the request has no 'authorization' header, or if it does but the + scheme is not "Digest", or if authentication fails, the tool sends + a 401 response with a 'WWW-Authenticate' Digest header. + + realm + A string containing the authentication realm. + + get_ha1 + A callable that looks up a username in a credentials store + and returns the HA1 string, which is defined in the RFC to be + MD5(username : realm : password). The function's signature is: + ``get_ha1(realm, username)`` + where username is obtained from the request's 'authorization' header. + If username is not found in the credentials store, get_ha1() returns + None. + + key + A secret string known only to the server, used in the synthesis + of nonces. + + """ + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + + respond_401 = functools.partial( + _respond_401, realm, key, accept_charset, debug) + + if not HttpDigestAuthorization.matches(auth_header or ''): + respond_401() + + msg = 'The Authorization header could not be parsed.' + with cherrypy.HTTPError.handle(ValueError, 400, msg): + auth = HttpDigestAuthorization( + auth_header, request.method, + debug=debug, accept_charset=accept_charset, + ) + + if debug: + TRACE(str(auth)) + + if not auth.validate_nonce(realm, key): + respond_401() + + ha1 = get_ha1(realm, auth.username) + + if ha1 is None: + respond_401() + + # note that for request.body to be available we need to + # hook in at before_handler, not on_start_resource like + # 3.1.x digest_auth does. + digest = auth.request_digest(ha1, entity_body=request.body) + if digest != auth.response: + respond_401() + + # authenticated + if debug: + TRACE('digest matches auth.response') + # Now check if nonce is stale. + # The choice of ten minutes' lifetime for nonce is somewhat + # arbitrary + if auth.is_nonce_stale(max_age_seconds=600): + respond_401(stale=True) + + request.login = auth.username + if debug: + TRACE('authentication of %s successful' % auth.username) + + +def _respond_401(realm, key, accept_charset, debug, **kwargs): + """ + Respond with 401 status and a WWW-Authenticate header + """ + header = www_authenticate( + realm, key, + accept_charset=accept_charset, + **kwargs + ) + if debug: + TRACE(header) + cherrypy.serving.response.headers['WWW-Authenticate'] = header + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') diff --git a/libraries/cherrypy/lib/caching.py b/libraries/cherrypy/lib/caching.py new file mode 100644 index 00000000..fed325a6 --- /dev/null +++ b/libraries/cherrypy/lib/caching.py @@ -0,0 +1,482 @@ +""" +CherryPy implements a simple caching system as a pluggable Tool. This tool +tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there +yet, but it's probably good enough for most sites. + +In general, GET responses are cached (along with selecting headers) and, if +another request arrives for the same resource, the caching Tool will return 304 +Not Modified if possible, or serve the cached response otherwise. It also sets +request.cached to True if serving a cached representation, and sets +request.cacheable to False (so it doesn't get cached again). + +If POST, PUT, or DELETE requests are made for a cached resource, they +invalidate (delete) any cached response. + +Usage +===== + +Configuration file example:: + + [/] + tools.caching.on = True + tools.caching.delay = 3600 + +You may use a class other than the default +:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config +entry ``cache_class``; supply the full dotted name of the replacement class +as the config value. It must implement the basic methods ``get``, ``put``, +``delete``, and ``clear``. + +You may set any attribute, including overriding methods, on the cache +instance by providing them in config. The above sets the +:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example. +""" + +import datetime +import sys +import threading +import time + +import six + +import cherrypy +from cherrypy.lib import cptools, httputil +from cherrypy._cpcompat import Event + + +class Cache(object): + + """Base class for Cache implementations.""" + + def get(self): + """Return the current variant if in the cache, else None.""" + raise NotImplemented + + def put(self, obj, size): + """Store the current variant in the cache.""" + raise NotImplemented + + def delete(self): + """Remove ALL cached variants of the current resource.""" + raise NotImplemented + + def clear(self): + """Reset the cache to its initial, empty state.""" + raise NotImplemented + + +# ------------------------------ Memory Cache ------------------------------- # +class AntiStampedeCache(dict): + + """A storage system for cached items which reduces stampede collisions.""" + + def wait(self, key, timeout=5, debug=False): + """Return the cached value for the given key, or None. + + If timeout is not None, and the value is already + being calculated by another thread, wait until the given timeout has + elapsed. If the value is available before the timeout expires, it is + returned. If not, None is returned, and a sentinel placed in the cache + to signal other threads to wait. + + If timeout is None, no waiting is performed nor sentinels used. + """ + value = self.get(key) + if isinstance(value, Event): + if timeout is None: + # Ignore the other thread and recalc it ourselves. + if debug: + cherrypy.log('No timeout', 'TOOLS.CACHING') + return None + + # Wait until it's done or times out. + if debug: + cherrypy.log('Waiting up to %s seconds' % + timeout, 'TOOLS.CACHING') + value.wait(timeout) + if value.result is not None: + # The other thread finished its calculation. Use it. + if debug: + cherrypy.log('Result!', 'TOOLS.CACHING') + return value.result + # Timed out. Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + + return None + elif value is None: + # Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + return value + + def __setitem__(self, key, value): + """Set the cached value for the given key.""" + existing = self.get(key) + dict.__setitem__(self, key, value) + if isinstance(existing, Event): + # Set Event.result so other threads waiting on it have + # immediate access without needing to poll the cache again. + existing.result = value + existing.set() + + +class MemoryCache(Cache): + + """An in-memory cache for varying response content. + + Each key in self.store is a URI, and each value is an AntiStampedeCache. + The response for any given URI may vary based on the values of + "selecting request headers"; that is, those named in the Vary + response header. We assume the list of header names to be constant + for each URI throughout the lifetime of the application, and store + that list in ``self.store[uri].selecting_headers``. + + The items contained in ``self.store[uri]`` have keys which are tuples of + request header values (in the same order as the names in its + selecting_headers), and values which are the actual responses. + """ + + maxobjects = 1000 + """The maximum number of cached objects; defaults to 1000.""" + + maxobj_size = 100000 + """The maximum size of each cached object in bytes; defaults to 100 KB.""" + + maxsize = 10000000 + """The maximum size of the entire cache in bytes; defaults to 10 MB.""" + + delay = 600 + """Seconds until the cached content expires; defaults to 600 (10 minutes). + """ + + antistampede_timeout = 5 + """Seconds to wait for other threads to release a cache lock.""" + + expire_freq = 0.1 + """Seconds to sleep between cache expiration sweeps.""" + + debug = False + + def __init__(self): + self.clear() + + # Run self.expire_cache in a separate daemon thread. + t = threading.Thread(target=self.expire_cache, name='expire_cache') + self.expiration_thread = t + t.daemon = True + t.start() + + def clear(self): + """Reset the cache to its initial, empty state.""" + self.store = {} + self.expirations = {} + self.tot_puts = 0 + self.tot_gets = 0 + self.tot_hist = 0 + self.tot_expires = 0 + self.tot_non_modified = 0 + self.cursize = 0 + + def expire_cache(self): + """Continuously examine cached objects, expiring stale ones. + + This function is designed to be run in its own daemon thread, + referenced at ``self.expiration_thread``. + """ + # It's possible that "time" will be set to None + # arbitrarily, so we check "while time" to avoid exceptions. + # See tickets #99 and #180 for more information. + while time: + now = time.time() + # Must make a copy of expirations so it doesn't change size + # during iteration + items = list(six.iteritems(self.expirations)) + for expiration_time, objects in items: + if expiration_time <= now: + for obj_size, uri, sel_header_values in objects: + try: + del self.store[uri][tuple(sel_header_values)] + self.tot_expires += 1 + self.cursize -= obj_size + except KeyError: + # the key may have been deleted elsewhere + pass + del self.expirations[expiration_time] + time.sleep(self.expire_freq) + + def get(self): + """Return the current variant if in the cache, else None.""" + request = cherrypy.serving.request + self.tot_gets += 1 + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + return None + + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + variant = uricache.wait(key=tuple(sorted(header_values)), + timeout=self.antistampede_timeout, + debug=self.debug) + if variant is not None: + self.tot_hist += 1 + return variant + + def put(self, variant, size): + """Store the current variant in the cache.""" + request = cherrypy.serving.request + response = cherrypy.serving.response + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + uricache = AntiStampedeCache() + uricache.selecting_headers = [ + e.value for e in response.headers.elements('Vary')] + self.store[uri] = uricache + + if len(self.store) < self.maxobjects: + total_size = self.cursize + size + + # checks if there's space for the object + if (size < self.maxobj_size and total_size < self.maxsize): + # add to the expirations list + expiration_time = response.time + self.delay + bucket = self.expirations.setdefault(expiration_time, []) + bucket.append((size, uri, uricache.selecting_headers)) + + # add to the cache + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + uricache[tuple(sorted(header_values))] = variant + self.tot_puts += 1 + self.cursize = total_size + + def delete(self): + """Remove ALL cached variants of the current resource.""" + uri = cherrypy.url(qs=cherrypy.serving.request.query_string) + self.store.pop(uri, None) + + +def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs): + """Try to obtain cached output. If fresh enough, raise HTTPError(304). + + If POST, PUT, or DELETE: + * invalidates (deletes) any cached response for this resource + * sets request.cached = False + * sets request.cacheable = False + + else if a cached copy exists: + * sets request.cached = True + * sets request.cacheable = False + * sets response.headers to the cached values + * checks the cached Last-Modified response header against the + current If-(Un)Modified-Since request headers; raises 304 + if necessary. + * sets response.status and response.body to the cached values + * returns True + + otherwise: + * sets request.cached = False + * sets request.cacheable = True + * returns False + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + if not hasattr(cherrypy, '_cache'): + # Make a process-wide Cache object. + cherrypy._cache = kwargs.pop('cache_class', MemoryCache)() + + # Take all remaining kwargs and set them on the Cache object. + for k, v in kwargs.items(): + setattr(cherrypy._cache, k, v) + cherrypy._cache.debug = debug + + # POST, PUT, DELETE should invalidate (delete) the cached copy. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. + if request.method in invalid_methods: + if debug: + cherrypy.log('request.method %r in invalid_methods %r' % + (request.method, invalid_methods), 'TOOLS.CACHING') + cherrypy._cache.delete() + request.cached = False + request.cacheable = False + return False + + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: + request.cached = False + request.cacheable = True + return False + + cache_data = cherrypy._cache.get() + request.cached = bool(cache_data) + request.cacheable = not request.cached + if request.cached: + # Serve the cached copy. + max_age = cherrypy._cache.delay + for v in [e.value for e in request.headers.elements('Cache-Control')]: + atoms = v.split('=', 1) + directive = atoms.pop(0) + if directive == 'max-age': + if len(atoms) != 1 or not atoms[0].isdigit(): + raise cherrypy.HTTPError( + 400, 'Invalid Cache-Control header') + max_age = int(atoms[0]) + break + elif directive == 'no-cache': + if debug: + cherrypy.log( + 'Ignoring cache due to Cache-Control: no-cache', + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + if debug: + cherrypy.log('Reading response from cache', 'TOOLS.CACHING') + s, h, b, create_time = cache_data + age = int(response.time - create_time) + if (age > max_age): + if debug: + cherrypy.log('Ignoring cache due to age > %d' % max_age, + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + # Copy the response headers. See + # https://github.com/cherrypy/cherrypy/issues/721. + response.headers = rh = httputil.HeaderMap() + for k in h: + dict.__setitem__(rh, k, dict.__getitem__(h, k)) + + # Add the required Age header + response.headers['Age'] = str(age) + + try: + # Note that validate_since depends on a Last-Modified header; + # this was put into the cached copy, and should have been + # resurrected just above (response.headers = cache_data[1]). + cptools.validate_since() + except cherrypy.HTTPRedirect: + x = sys.exc_info()[1] + if x.status == 304: + cherrypy._cache.tot_non_modified += 1 + raise + + # serve it & get out from the request + response.status = s + response.body = b + else: + if debug: + cherrypy.log('request is not cached', 'TOOLS.CACHING') + return request.cached + + +def tee_output(): + """Tee response output to cache storage. Internal.""" + # Used by CachingTool by attaching to request.hooks + + request = cherrypy.serving.request + if 'no-store' in request.headers.values('Cache-Control'): + return + + def tee(body): + """Tee response.body into a list.""" + if ('no-cache' in response.headers.values('Pragma') or + 'no-store' in response.headers.values('Cache-Control')): + for chunk in body: + yield chunk + return + + output = [] + for chunk in body: + output.append(chunk) + yield chunk + + # Save the cache data, but only if the body isn't empty. + # e.g. a 304 Not Modified on a static file response will + # have an empty body. + # If the body is empty, delete the cache because it + # contains a stale Threading._Event object that will + # stall all consecutive requests until the _Event times + # out + body = b''.join(output) + if not body: + cherrypy._cache.delete() + else: + cherrypy._cache.put((response.status, response.headers or {}, + body, response.time), len(body)) + + response = cherrypy.serving.response + response.body = tee(response.body) + + +def expires(secs=0, force=False, debug=False): + """Tool for influencing cache mechanisms using the 'Expires' header. + + secs + Must be either an int or a datetime.timedelta, and indicates the + number of seconds between response.time and when the response should + expire. The 'Expires' header will be set to response.time + secs. + If secs is zero, the 'Expires' header is set one year in the past, and + the following "cache prevention" headers are also set: + + * Pragma: no-cache + * Cache-Control': no-cache, must-revalidate + + force + If False, the following headers are checked: + + * Etag + * Last-Modified + * Age + * Expires + + If any are already present, none of the above response headers are set. + + """ + + response = cherrypy.serving.response + headers = response.headers + + cacheable = False + if not force: + # some header names that indicate that the response can be cached + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): + if indicator in headers: + cacheable = True + break + + if not cacheable and not force: + if debug: + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') + else: + if debug: + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') + if isinstance(secs, datetime.timedelta): + secs = (86400 * secs.days) + secs.seconds + + if secs == 0: + if force or ('Pragma' not in headers): + headers['Pragma'] = 'no-cache' + if cherrypy.serving.request.protocol >= (1, 1): + if force or 'Cache-Control' not in headers: + headers['Cache-Control'] = 'no-cache, must-revalidate' + # Set an explicit Expires date in the past. + expiry = httputil.HTTPDate(1169942400.0) + else: + expiry = httputil.HTTPDate(response.time + secs) + if force or 'Expires' not in headers: + headers['Expires'] = expiry diff --git a/libraries/cherrypy/lib/covercp.py b/libraries/cherrypy/lib/covercp.py new file mode 100644 index 00000000..0bafca13 --- /dev/null +++ b/libraries/cherrypy/lib/covercp.py @@ -0,0 +1,391 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_ +or Ned Batchelder's `enhanced version: +<http://www.nedbatchelder.com/code/modules/coverage.html>`_ + +To turn on coverage tracing, use the following code:: + + cherrypy.engine.subscribe('start', covercp.start) + +DO NOT subscribe anything on the 'start_thread' channel, as previously +recommended. Calling start once in the main thread should be sufficient +to start coverage on all threads. Calling start again in each thread +effectively clears any coverage data gathered up to that point. + +Run your code, then use the ``covercp.serve()`` function to browse the +results in a web browser. If you run this module from the command line, +it will call ``serve()`` for you. +""" + +import re +import sys +import cgi +import os +import os.path + +from six.moves import urllib + +import cherrypy + + +localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache') + +the_coverage = None +try: + from coverage import coverage + the_coverage = coverage(data_file=localFile) + + def start(): + the_coverage.start() +except ImportError: + # Setting the_coverage to None will raise errors + # that need to be trapped downstream. + the_coverage = None + + import warnings + warnings.warn( + 'No code coverage will be performed; ' + 'coverage.py could not be imported.') + + def start(): + pass +start.priority = 20 + +TEMPLATE_MENU = """<html> +<head> + <title>CherryPy Coverage Menu</title> + <style> + body {font: 9pt Arial, serif;} + #tree { + font-size: 8pt; + font-family: Andale Mono, monospace; + white-space: pre; + } + #tree a:active, a:focus { + background-color: black; + padding: 1px; + color: white; + border: 0px solid #9999FF; + -moz-outline-style: none; + } + .fail { color: red;} + .pass { color: #888;} + #pct { text-align: right;} + h3 { + font-size: small; + font-weight: bold; + font-style: italic; + margin-top: 5px; + } + input { border: 1px solid #ccc; padding: 2px; } + .directory { + color: #933; + font-style: italic; + font-weight: bold; + font-size: 10pt; + } + .file { + color: #400; + } + a { text-decoration: none; } + #crumbs { + color: white; + font-size: 8pt; + font-family: Andale Mono, monospace; + width: 100%; + background-color: black; + } + #crumbs a { + color: #f88; + } + #options { + line-height: 2.3em; + border: 1px solid black; + background-color: #eee; + padding: 4px; + } + #exclude { + width: 100%; + margin-bottom: 3px; + border: 1px solid #999; + } + #submit { + background-color: black; + color: white; + border: 0; + margin-bottom: -9px; + } + </style> +</head> +<body> +<h2>CherryPy Coverage</h2>""" + +TEMPLATE_FORM = """ +<div id="options"> +<form action='menu' method=GET> + <input type='hidden' name='base' value='%(base)s' /> + Show percentages + <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br /> + Hide files over + <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br /> + Exclude files matching<br /> + <input type='text' id='exclude' name='exclude' + value='%(exclude)s' size='20' /> + <br /> + + <input type='submit' value='Change view' id="submit"/> +</form> +</div>""" + +TEMPLATE_FRAMESET = """<html> +<head><title>CherryPy coverage data</title></head> +<frameset cols='250, 1*'> + <frame src='menu?base=%s' /> + <frame name='main' src='' /> +</frameset> +</html> +""" + +TEMPLATE_COVERAGE = """<html> +<head> + <title>Coverage for %(name)s</title> + <style> + h2 { margin-bottom: .25em; } + p { margin: .25em; } + .covered { color: #000; background-color: #fff; } + .notcovered { color: #fee; background-color: #500; } + .excluded { color: #00f; background-color: #fff; } + table .covered, table .notcovered, table .excluded + { font-family: Andale Mono, monospace; + font-size: 10pt; white-space: pre; } + + .lineno { background-color: #eee;} + .notcovered .lineno { background-color: #000;} + table { border-collapse: collapse; + </style> +</head> +<body> +<h2>%(name)s</h2> +<p>%(fullpath)s</p> +<p>Coverage: %(pc)s%%</p>""" + +TEMPLATE_LOC_COVERED = """<tr class="covered"> + <td class="lineno">%s </td> + <td>%s</td> +</tr>\n""" +TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered"> + <td class="lineno">%s </td> + <td>%s</td> +</tr>\n""" +TEMPLATE_LOC_EXCLUDED = """<tr class="excluded"> + <td class="lineno">%s </td> + <td>%s</td> +</tr>\n""" + +TEMPLATE_ITEM = ( + "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n" +) + + +def _percent(statements, missing): + s = len(statements) + e = s - len(missing) + if s > 0: + return int(round(100.0 * e / s)) + return 0 + + +def _show_branch(root, base, path, pct=0, showpct=False, exclude='', + coverage=the_coverage): + + # Show the directory name and any of our children + dirs = [k for k, v in root.items() if v] + dirs.sort() + for name in dirs: + newpath = os.path.join(path, name) + + if newpath.lower().startswith(base): + relpath = newpath[len(base):] + yield '| ' * relpath.count(os.sep) + yield ( + "<a class='directory' " + "href='menu?base=%s&exclude=%s'>%s</a>\n" % + (newpath, urllib.parse.quote_plus(exclude), name) + ) + + for chunk in _show_branch( + root[name], base, newpath, pct, showpct, + exclude, coverage=coverage + ): + yield chunk + + # Now list the files + if path.lower().startswith(base): + relpath = path[len(base):] + files = [k for k, v in root.items() if not v] + files.sort() + for name in files: + newpath = os.path.join(path, name) + + pc_str = '' + if showpct: + try: + _, statements, _, missing, _ = coverage.analysis2(newpath) + except Exception: + # Yes, we really want to pass on all errors. + pass + else: + pc = _percent(statements, missing) + pc_str = ('%3d%% ' % pc).replace(' ', ' ') + if pc < float(pct) or pc == -1: + pc_str = "<span class='fail'>%s</span>" % pc_str + else: + pc_str = "<span class='pass'>%s</span>" % pc_str + + yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1), + pc_str, newpath, name) + + +def _skip_file(path, exclude): + if exclude: + return bool(re.search(exclude, path)) + + +def _graft(path, tree): + d = tree + + p = path + atoms = [] + while True: + p, tail = os.path.split(p) + if not tail: + break + atoms.append(tail) + atoms.append(p) + if p != '/': + atoms.append('/') + + atoms.reverse() + for node in atoms: + if node: + d = d.setdefault(node, {}) + + +def get_tree(base, exclude, coverage=the_coverage): + """Return covered module names as a nested dict.""" + tree = {} + runs = coverage.data.executed_files() + for path in runs: + if not _skip_file(path, exclude) and not os.path.isdir(path): + _graft(path, tree) + return tree + + +class CoverStats(object): + + def __init__(self, coverage, root=None): + self.coverage = coverage + if root is None: + # Guess initial depth. Files outside this path will not be + # reachable from the web interface. + root = os.path.dirname(cherrypy.__file__) + self.root = root + + @cherrypy.expose + def index(self): + return TEMPLATE_FRAMESET % self.root.lower() + + @cherrypy.expose + def menu(self, base='/', pct='50', showpct='', + exclude=r'python\d\.\d|test|tut\d|tutorial'): + + # The coverage module uses all-lower-case names. + base = base.lower().rstrip(os.sep) + + yield TEMPLATE_MENU + yield TEMPLATE_FORM % locals() + + # Start by showing links for parent paths + yield "<div id='crumbs'>" + path = '' + atoms = base.split(os.sep) + atoms.pop() + for atom in atoms: + path += atom + os.sep + yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" + % (path, urllib.parse.quote_plus(exclude), atom, os.sep)) + yield '</div>' + + yield "<div id='tree'>" + + # Then display the tree + tree = get_tree(base, exclude, self.coverage) + if not tree: + yield '<p>No modules covered.</p>' + else: + for chunk in _show_branch(tree, base, '/', pct, + showpct == 'checked', exclude, + coverage=self.coverage): + yield chunk + + yield '</div>' + yield '</body></html>' + + def annotated_file(self, filename, statements, excluded, missing): + source = open(filename, 'r') + buffer = [] + for lineno, line in enumerate(source.readlines()): + lineno += 1 + line = line.strip('\n\r') + empty_the_buffer = True + if lineno in excluded: + template = TEMPLATE_LOC_EXCLUDED + elif lineno in missing: + template = TEMPLATE_LOC_NOT_COVERED + elif lineno in statements: + template = TEMPLATE_LOC_COVERED + else: + empty_the_buffer = False + buffer.append((lineno, line)) + if empty_the_buffer: + for lno, pastline in buffer: + yield template % (lno, cgi.escape(pastline)) + buffer = [] + yield template % (lineno, cgi.escape(line)) + + @cherrypy.expose + def report(self, name): + filename, statements, excluded, missing, _ = self.coverage.analysis2( + name) + pc = _percent(statements, missing) + yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), + fullpath=name, + pc=pc) + yield '<table>\n' + for line in self.annotated_file(filename, statements, excluded, + missing): + yield line + yield '</table>' + yield '</body>' + yield '</html>' + + +def serve(path=localFile, port=8080, root=None): + if coverage is None: + raise ImportError('The coverage module could not be imported.') + from coverage import coverage + cov = coverage(data_file=path) + cov.load() + + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': 'production', + }) + cherrypy.quickstart(CoverStats(cov, root)) + + +if __name__ == '__main__': + serve(*tuple(sys.argv[1:])) diff --git a/libraries/cherrypy/lib/cpstats.py b/libraries/cherrypy/lib/cpstats.py new file mode 100644 index 00000000..ae9f7475 --- /dev/null +++ b/libraries/cherrypy/lib/cpstats.py @@ -0,0 +1,696 @@ +"""CPStats, a package for collecting and reporting on program statistics. + +Overview +======== + +Statistics about program operation are an invaluable monitoring and debugging +tool. Unfortunately, the gathering and reporting of these critical values is +usually ad-hoc. This package aims to add a centralized place for gathering +statistical performance data, a structure for recording that data which +provides for extrapolation of that data into more useful information, +and a method of serving that data to both human investigators and +monitoring software. Let's examine each of those in more detail. + +Data Gathering +-------------- + +Just as Python's `logging` module provides a common importable for gathering +and sending messages, performance statistics would benefit from a similar +common mechanism, and one that does *not* require each package which wishes +to collect stats to import a third-party module. Therefore, we choose to +re-use the `logging` module by adding a `statistics` object to it. + +That `logging.statistics` object is a nested dict. It is not a custom class, +because that would: + + 1. require libraries and applications to import a third-party module in + order to participate + 2. inhibit innovation in extrapolation approaches and in reporting tools, and + 3. be slow. + +There are, however, some specifications regarding the structure of the dict.:: + + { + +----"SQLAlchemy": { + | "Inserts": 4389745, + | "Inserts per Second": + | lambda s: s["Inserts"] / (time() - s["Start"]), + | C +---"Table Statistics": { + | o | "widgets": {-----------+ + N | l | "Rows": 1.3M, | Record + a | l | "Inserts": 400, | + m | e | },---------------------+ + e | c | "froobles": { + s | t | "Rows": 7845, + p | i | "Inserts": 0, + a | o | }, + c | n +---}, + e | "Slow Queries": + | [{"Query": "SELECT * FROM widgets;", + | "Processing Time": 47.840923343, + | }, + | ], + +----}, + } + +The `logging.statistics` dict has four levels. The topmost level is nothing +more than a set of names to introduce modularity, usually along the lines of +package names. If the SQLAlchemy project wanted to participate, for example, +it might populate the item `logging.statistics['SQLAlchemy']`, whose value +would be a second-layer dict we call a "namespace". Namespaces help multiple +packages to avoid collisions over key names, and make reports easier to read, +to boot. The maintainers of SQLAlchemy should feel free to use more than one +namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case +or other syntax constraints on the namespace names; they should be chosen +to be maximally readable by humans (neither too short nor too long). + +Each namespace, then, is a dict of named statistical values, such as +'Requests/sec' or 'Uptime'. You should choose names which will look +good on a report: spaces and capitalization are just fine. + +In addition to scalars, values in a namespace MAY be a (third-layer) +dict, or a list, called a "collection". For example, the CherryPy +:class:`StatsTool` keeps track of what each request is doing (or has most +recently done) in a 'Requests' collection, where each key is a thread ID; each +value in the subdict MUST be a fourth dict (whew!) of statistical data about +each thread. We call each subdict in the collection a "record". Similarly, +the :class:`StatsTool` also keeps a list of slow queries, where each record +contains data about each slow query, in order. + +Values in a namespace or record may also be functions, which brings us to: + +Extrapolation +------------- + +The collection of statistical data needs to be fast, as close to unnoticeable +as possible to the host program. That requires us to minimize I/O, for example, +but in Python it also means we need to minimize function calls. So when you +are designing your namespace and record values, try to insert the most basic +scalar values you already have on hand. + +When it comes time to report on the gathered data, however, we usually have +much more freedom in what we can calculate. Therefore, whenever reporting +tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents +of `logging.statistics` for reporting, they first call +`extrapolate_statistics` (passing the whole `statistics` dict as the only +argument). This makes a deep copy of the statistics dict so that the +reporting tool can both iterate over it and even change it without harming +the original. But it also expands any functions in the dict by calling them. +For example, you might have a 'Current Time' entry in the namespace with the +value "lambda scope: time.time()". The "scope" parameter is the current +namespace dict (or record, if we're currently expanding one of those +instead), allowing you access to existing static entries. If you're truly +evil, you can even modify more than one entry at a time. + +However, don't try to calculate an entry and then use its value in further +extrapolations; the order in which the functions are called is not guaranteed. +This can lead to a certain amount of duplicated work (or a redesign of your +schema), but that's better than complicating the spec. + +After the whole thing has been extrapolated, it's time for: + +Reporting +--------- + +The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates +it all, and then transforms it to HTML for easy viewing. Each namespace gets +its own header and attribute table, plus an extra table for each collection. +This is NOT part of the statistics specification; other tools can format how +they like. + +You can control which columns are output and how they are formatted by updating +StatsPage.formatting, which is a dict that mirrors the keys and nesting of +`logging.statistics`. The difference is that, instead of data values, it has +formatting values. Use None for a given key to indicate to the StatsPage that a +given column should not be output. Use a string with formatting +(such as '%.3f') to interpolate the value(s), or use a callable (such as +lambda v: v.isoformat()) for more advanced formatting. Any entry which is not +mentioned in the formatting dict is output unchanged. + +Monitoring +---------- + +Although the HTML output takes pains to assign unique id's to each <td> with +statistical data, you're probably better off fetching /cpstats/data, which +outputs the whole (extrapolated) `logging.statistics` dict in JSON format. +That is probably easier to parse, and doesn't have any formatting controls, +so you get the "original" data in a consistently-serialized format. +Note: there's no treatment yet for datetime objects. Try time.time() instead +for now if you can. Nagios will probably thank you. + +Turning Collection Off +---------------------- + +It is recommended each namespace have an "Enabled" item which, if False, +stops collection (but not reporting) of statistical data. Applications +SHOULD provide controls to pause and resume collection by setting these +entries to False or True, if present. + + +Usage +===== + +To collect statistics on CherryPy applications:: + + from cherrypy.lib import cpstats + appconfig['/']['tools.cpstats.on'] = True + +To collect statistics on your own code:: + + import logging + # Initialize the repository + if not hasattr(logging, 'statistics'): logging.statistics = {} + # Initialize my namespace + mystats = logging.statistics.setdefault('My Stuff', {}) + # Initialize my namespace's scalars and collections + mystats.update({ + 'Enabled': True, + 'Start Time': time.time(), + 'Important Events': 0, + 'Events/Second': lambda s: ( + (s['Important Events'] / (time.time() - s['Start Time']))), + }) + ... + for event in events: + ... + # Collect stats + if mystats.get('Enabled', False): + mystats['Important Events'] += 1 + +To report statistics:: + + root.cpstats = cpstats.StatsPage() + +To format statistics reports:: + + See 'Reporting', above. + +""" + +import logging +import os +import sys +import threading +import time + +import six + +import cherrypy +from cherrypy._cpcompat import json + +# ------------------------------- Statistics -------------------------------- # + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +def extrapolate_statistics(scope): + """Return an extrapolated copy of the given scope.""" + c = {} + for k, v in list(scope.items()): + if isinstance(v, dict): + v = extrapolate_statistics(v) + elif isinstance(v, (list, tuple)): + v = [extrapolate_statistics(record) for record in v] + elif hasattr(v, '__call__'): + v = v(scope) + c[k] = v + return c + + +# -------------------- CherryPy Applications Statistics --------------------- # + +appstats = logging.statistics.setdefault('CherryPy Applications', {}) +appstats.update({ + 'Enabled': True, + 'Bytes Read/Request': lambda s: ( + s['Total Requests'] and + (s['Total Bytes Read'] / float(s['Total Requests'])) or + 0.0 + ), + 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), + 'Bytes Written/Request': lambda s: ( + s['Total Requests'] and + (s['Total Bytes Written'] / float(s['Total Requests'])) or + 0.0 + ), + 'Bytes Written/Second': lambda s: ( + s['Total Bytes Written'] / s['Uptime'](s) + ), + 'Current Time': lambda s: time.time(), + 'Current Requests': 0, + 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), + 'Server Version': cherrypy.__version__, + 'Start Time': time.time(), + 'Total Bytes Read': 0, + 'Total Bytes Written': 0, + 'Total Requests': 0, + 'Total Time': 0, + 'Uptime': lambda s: time.time() - s['Start Time'], + 'Requests': {}, +}) + + +def proc_time(s): + return time.time() - s['Start Time'] + + +class ByteCountWrapper(object): + + """Wraps a file-like object, counting the number of bytes read.""" + + def __init__(self, rfile): + self.rfile = rfile + self.bytes_read = 0 + + def read(self, size=-1): + data = self.rfile.read(size) + self.bytes_read += len(data) + return data + + def readline(self, size=-1): + data = self.rfile.readline(size) + self.bytes_read += len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + return data + + +def average_uriset_time(s): + return s['Count'] and (s['Sum'] / s['Count']) or 0 + + +def _get_threading_ident(): + if sys.version_info >= (3, 3): + return threading.get_ident() + return threading._get_ident() + + +class StatsTool(cherrypy.Tool): + + """Record various information about the current request.""" + + def __init__(self): + cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + if appstats.get('Enabled', False): + cherrypy.Tool._setup(self) + self.record_start() + + def record_start(self): + """Record the beginning of a request.""" + request = cherrypy.serving.request + if not hasattr(request.rfile, 'bytes_read'): + request.rfile = ByteCountWrapper(request.rfile) + request.body.fp = request.rfile + + r = request.remote + + appstats['Current Requests'] += 1 + appstats['Total Requests'] += 1 + appstats['Requests'][_get_threading_ident()] = { + 'Bytes Read': None, + 'Bytes Written': None, + # Use a lambda so the ip gets updated by tools.proxy later + 'Client': lambda s: '%s:%s' % (r.ip, r.port), + 'End Time': None, + 'Processing Time': proc_time, + 'Request-Line': request.request_line, + 'Response Status': None, + 'Start Time': time.time(), + } + + def record_stop( + self, uriset=None, slow_queries=1.0, slow_queries_count=100, + debug=False, **kwargs): + """Record the end of a request.""" + resp = cherrypy.serving.response + w = appstats['Requests'][_get_threading_ident()] + + r = cherrypy.request.rfile.bytes_read + w['Bytes Read'] = r + appstats['Total Bytes Read'] += r + + if resp.stream: + w['Bytes Written'] = 'chunked' + else: + cl = int(resp.headers.get('Content-Length', 0)) + w['Bytes Written'] = cl + appstats['Total Bytes Written'] += cl + + w['Response Status'] = getattr( + resp, 'output_status', None) or resp.status + + w['End Time'] = time.time() + p = w['End Time'] - w['Start Time'] + w['Processing Time'] = p + appstats['Total Time'] += p + + appstats['Current Requests'] -= 1 + + if debug: + cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') + + if uriset: + rs = appstats.setdefault('URI Set Tracking', {}) + r = rs.setdefault(uriset, { + 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, + 'Avg': average_uriset_time}) + if r['Min'] is None or p < r['Min']: + r['Min'] = p + if r['Max'] is None or p > r['Max']: + r['Max'] = p + r['Count'] += 1 + r['Sum'] += p + + if slow_queries and p > slow_queries: + sq = appstats.setdefault('Slow Queries', []) + sq.append(w.copy()) + if len(sq) > slow_queries_count: + sq.pop(0) + + +cherrypy.tools.cpstats = StatsTool() + + +# ---------------------- CherryPy Statistics Reporting ---------------------- # + +thisdir = os.path.abspath(os.path.dirname(__file__)) + +missing = object() + + +def locale_date(v): + return time.strftime('%c', time.gmtime(v)) + + +def iso_format(v): + return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) + + +def pause_resume(ns): + def _pause_resume(enabled): + pause_disabled = '' + resume_disabled = '' + if enabled: + resume_disabled = 'disabled="disabled" ' + else: + pause_disabled = 'disabled="disabled" ' + return """ + <form action="pause" method="POST" style="display:inline"> + <input type="hidden" name="namespace" value="%s" /> + <input type="submit" value="Pause" %s/> + </form> + <form action="resume" method="POST" style="display:inline"> + <input type="hidden" name="namespace" value="%s" /> + <input type="submit" value="Resume" %s/> + </form> + """ % (ns, pause_disabled, ns, resume_disabled) + return _pause_resume + + +class StatsPage(object): + + formatting = { + 'CherryPy Applications': { + 'Enabled': pause_resume('CherryPy Applications'), + 'Bytes Read/Request': '%.3f', + 'Bytes Read/Second': '%.3f', + 'Bytes Written/Request': '%.3f', + 'Bytes Written/Second': '%.3f', + 'Current Time': iso_format, + 'Requests/Second': '%.3f', + 'Start Time': iso_format, + 'Total Time': '%.3f', + 'Uptime': '%.3f', + 'Slow Queries': { + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': iso_format, + }, + 'URI Set Tracking': { + 'Avg': '%.3f', + 'Max': '%.3f', + 'Min': '%.3f', + 'Sum': '%.3f', + }, + 'Requests': { + 'Bytes Read': '%s', + 'Bytes Written': '%s', + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': None, + }, + }, + 'CherryPy WSGIServer': { + 'Enabled': pause_resume('CherryPy WSGIServer'), + 'Connections/second': '%.3f', + 'Start time': iso_format, + }, + } + + @cherrypy.expose + def index(self): + # Transform the raw data into pretty output for HTML + yield """ +<html> +<head> + <title>Statistics</title> +<style> + +th, td { + padding: 0.25em 0.5em; + border: 1px solid #666699; +} + +table { + border-collapse: collapse; +} + +table.stats1 { + width: 100%; +} + +table.stats1 th { + font-weight: bold; + text-align: right; + background-color: #CCD5DD; +} + +table.stats2, h2 { + margin-left: 50px; +} + +table.stats2 th { + font-weight: bold; + text-align: center; + background-color: #CCD5DD; +} + +</style> +</head> +<body> +""" + for title, scalars, collections in self.get_namespaces(): + yield """ +<h1>%s</h1> + +<table class='stats1'> + <tbody> +""" % title + for i, (key, value) in enumerate(scalars): + colnum = i % 3 + if colnum == 0: + yield """ + <tr>""" + yield ( + """ + <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % + vars() + ) + if colnum == 2: + yield """ + </tr>""" + + if colnum == 0: + yield """ + <th></th><td></td> + <th></th><td></td> + </tr>""" + elif colnum == 1: + yield """ + <th></th><td></td> + </tr>""" + yield """ + </tbody> +</table>""" + + for subtitle, headers, subrows in collections: + yield """ +<h2>%s</h2> +<table class='stats2'> + <thead> + <tr>""" % subtitle + for key in headers: + yield """ + <th>%s</th>""" % key + yield """ + </tr> + </thead> + <tbody>""" + for subrow in subrows: + yield """ + <tr>""" + for value in subrow: + yield """ + <td>%s</td>""" % value + yield """ + </tr>""" + yield """ + </tbody> +</table>""" + yield """ +</body> +</html> +""" + + def get_namespaces(self): + """Yield (title, scalars, collections) for each namespace.""" + s = extrapolate_statistics(logging.statistics) + for title, ns in sorted(s.items()): + scalars = [] + collections = [] + ns_fmt = self.formatting.get(title, {}) + for k, v in sorted(ns.items()): + fmt = ns_fmt.get(k, {}) + if isinstance(v, dict): + headers, subrows = self.get_dict_collection(v, fmt) + collections.append((k, ['ID'] + headers, subrows)) + elif isinstance(v, (list, tuple)): + headers, subrows = self.get_list_collection(v, fmt) + collections.append((k, headers, subrows)) + else: + format = ns_fmt.get(k, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v = format(v) + elif format is not missing: + v = format % v + scalars.append((k, v)) + yield title, scalars, collections + + def get_dict_collection(self, v, formatting): + """Return ([headers], [rows]) for the given collection.""" + # E.g., the 'Requests' dict. + headers = [] + vals = six.itervalues(v) + for record in vals: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for k2, record in sorted(v.items()): + subrow = [k2] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + def get_list_collection(self, v, formatting): + """Return ([headers], [subrows]) for the given collection.""" + # E.g., the 'Slow Queries' list. + headers = [] + for record in v: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for record in v: + subrow = [] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + if json is not None: + @cherrypy.expose + def data(self): + s = extrapolate_statistics(logging.statistics) + cherrypy.response.headers['Content-Type'] = 'application/json' + return json.dumps(s, sort_keys=True, indent=4) + + @cherrypy.expose + def pause(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = False + raise cherrypy.HTTPRedirect('./') + pause.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + + @cherrypy.expose + def resume(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = True + raise cherrypy.HTTPRedirect('./') + resume.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} diff --git a/libraries/cherrypy/lib/cptools.py b/libraries/cherrypy/lib/cptools.py new file mode 100644 index 00000000..1c079634 --- /dev/null +++ b/libraries/cherrypy/lib/cptools.py @@ -0,0 +1,640 @@ +"""Functions for builtin CherryPy tools.""" + +import logging +import re +from hashlib import md5 + +import six +from six.moves import urllib + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import httputil as _httputil +from cherrypy.lib import is_iterator + + +# Conditional HTTP request support # + +def validate_etags(autotags=False, debug=False): + """Validate the current ETag against If-Match, If-None-Match headers. + + If autotags is True, an ETag response-header value will be provided + from an MD5 hash of the response body (unless some other code has + already provided an ETag header). If False (the default), the ETag + will not be automatic. + + WARNING: the autotags feature is not designed for URL's which allow + methods other than GET. For example, if a POST to the same URL returns + no content, the automatic ETag will be incorrect, breaking a fundamental + use for entity tags in a possibly destructive fashion. Likewise, if you + raise 304 Not Modified, the response body will be empty, the ETag hash + will be incorrect, and your application will break. + See :rfc:`2616` Section 14.24. + """ + response = cherrypy.serving.response + + # Guard against being run twice. + if hasattr(response, 'ETag'): + return + + status, reason, msg = _httputil.valid_status(response.status) + + etag = response.headers.get('ETag') + + # Automatic ETag generation. See warning in docstring. + if etag: + if debug: + cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') + elif not autotags: + if debug: + cherrypy.log('Autotags off', 'TOOLS.ETAGS') + elif status != 200: + if debug: + cherrypy.log('Status not 200', 'TOOLS.ETAGS') + else: + etag = response.collapse_body() + etag = '"%s"' % md5(etag).hexdigest() + if debug: + cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') + response.headers['ETag'] = etag + + response.ETag = etag + + # "If the request would, without the If-Match header field, result in + # anything other than a 2xx or 412 status, then the If-Match header + # MUST be ignored." + if debug: + cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') + if status >= 200 and status <= 299: + request = cherrypy.serving.request + + conditions = request.headers.elements('If-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions and not (conditions == ['*'] or etag in conditions): + raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did ' + 'not match %r' % (etag, conditions)) + + conditions = request.headers.elements('If-None-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-None-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions == ['*'] or etag in conditions: + if debug: + cherrypy.log('request.method: %s' % + request.method, 'TOOLS.ETAGS') + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r ' + 'matched %r' % (etag, conditions)) + + +def validate_since(): + """Validate the current Last-Modified against If-Modified-Since headers. + + If no code has set the Last-Modified response header, then no validation + will be performed. + """ + response = cherrypy.serving.response + lastmod = response.headers.get('Last-Modified') + if lastmod: + status, reason, msg = _httputil.valid_status(response.status) + + request = cherrypy.serving.request + + since = request.headers.get('If-Unmodified-Since') + if since and since != lastmod: + if (status >= 200 and status <= 299) or status == 412: + raise cherrypy.HTTPError(412) + + since = request.headers.get('If-Modified-Since') + if since and since == lastmod: + if (status >= 200 and status <= 299) or status == 304: + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412) + + +# Tool code # + +def allow(methods=None, debug=False): + """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). + + The given methods are case-insensitive, and may be in any order. + If only one method is allowed, you may supply a single string; + if more than one, supply a list of strings. + + Regardless of whether the current method is allowed or not, this + also emits an 'Allow' response header, containing the given methods. + """ + if not isinstance(methods, (tuple, list)): + methods = [methods] + methods = [m.upper() for m in methods if m] + if not methods: + methods = ['GET', 'HEAD'] + elif 'GET' in methods and 'HEAD' not in methods: + methods.append('HEAD') + + cherrypy.response.headers['Allow'] = ', '.join(methods) + if cherrypy.request.method not in methods: + if debug: + cherrypy.log('request.method %r not in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + raise cherrypy.HTTPError(405) + else: + if debug: + cherrypy.log('request.method %r in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + + +def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', + scheme='X-Forwarded-Proto', debug=False): + """Change the base URL (scheme://host[:port][/path]). + + For running a CP server behind Apache, lighttpd, or other HTTP server. + + For Apache and lighttpd, you should leave the 'local' argument at the + default value of 'X-Forwarded-Host'. For Squid, you probably want to set + tools.proxy.local = 'Origin'. + + If you want the new request.base to include path info (not just the host), + you must explicitly set base to the full base path, and ALSO set 'local' + to '', so that the X-Forwarded-Host request header (which never includes + path info) does not override it. Regardless, the value for 'base' MUST + NOT end in a slash. + + cherrypy.request.remote.ip (the IP address of the client) will be + rewritten if the header specified by the 'remote' arg is valid. + By default, 'remote' is set to 'X-Forwarded-For'. If you do not + want to rewrite remote.ip, set the 'remote' arg to an empty string. + """ + + request = cherrypy.serving.request + + if scheme: + s = request.headers.get(scheme, None) + if debug: + cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') + if s == 'on' and 'ssl' in scheme.lower(): + # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header + scheme = 'https' + else: + # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' + scheme = s + if not scheme: + scheme = request.base[:request.base.find('://')] + + if local: + lbase = request.headers.get(local, None) + if debug: + cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') + if lbase is not None: + base = lbase.split(',')[0] + if not base: + default = urllib.parse.urlparse(request.base).netloc + base = request.headers.get('Host', default) + + if base.find('://') == -1: + # add http:// or https:// if needed + base = scheme + '://' + base + + request.base = base + + if remote: + xff = request.headers.get(remote) + if debug: + cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') + if xff: + if remote == 'X-Forwarded-For': + # Grab the first IP in a comma-separated list. Ref #1268. + xff = next(ip.strip() for ip in xff.split(',')) + request.remote.ip = xff + + +def ignore_headers(headers=('Range',), debug=False): + """Delete request headers whose field names are included in 'headers'. + + This is a useful tool for working behind certain HTTP servers; + for example, Apache duplicates the work that CP does for 'Range' + headers, and will doubly-truncate the response. + """ + request = cherrypy.serving.request + for name in headers: + if name in request.headers: + if debug: + cherrypy.log('Ignoring request header %r' % name, + 'TOOLS.IGNORE_HEADERS') + del request.headers[name] + + +def response_headers(headers=None, debug=False): + """Set headers on the response.""" + if debug: + cherrypy.log('Setting response headers: %s' % repr(headers), + 'TOOLS.RESPONSE_HEADERS') + for name, value in (headers or []): + cherrypy.serving.response.headers[name] = value + + +response_headers.failsafe = True + + +def referer(pattern, accept=True, accept_missing=False, error=403, + message='Forbidden Referer header.', debug=False): + """Raise HTTPError if Referer header does/does not match the given pattern. + + pattern + A regular expression pattern to test against the Referer. + + accept + If True, the Referer must match the pattern; if False, + the Referer must NOT match the pattern. + + accept_missing + If True, permit requests with no Referer header. + + error + The HTTP error code to return to the client on failure. + + message + A string to include in the response body on failure. + + """ + try: + ref = cherrypy.serving.request.headers['Referer'] + match = bool(re.match(pattern, ref)) + if debug: + cherrypy.log('Referer %r matches %r' % (ref, pattern), + 'TOOLS.REFERER') + if accept == match: + return + except KeyError: + if debug: + cherrypy.log('No Referer header', 'TOOLS.REFERER') + if accept_missing: + return + + raise cherrypy.HTTPError(error, message) + + +class SessionAuth(object): + + """Assert that the user is logged in.""" + + session_key = 'username' + debug = False + + def check_username_and_password(self, username, password): + pass + + def anonymous(self): + """Provide a temporary user name for anonymous users.""" + pass + + def on_login(self, username): + pass + + def on_logout(self, username): + pass + + def on_check(self, username): + pass + + def login_screen(self, from_page='..', username='', error_msg='', + **kwargs): + return (six.text_type("""<html><body> +Message: %(error_msg)s +<form method="post" action="do_login"> + Login: <input type="text" name="username" value="%(username)s" size="10" /> + <br /> + Password: <input type="password" name="password" size="10" /> + <br /> + <input type="hidden" name="from_page" value="%(from_page)s" /> + <br /> + <input type="submit" /> +</form> +</body></html>""") % vars()).encode('utf-8') + + def do_login(self, username, password, from_page='..', **kwargs): + """Login. May raise redirect, or return True if request handled.""" + response = cherrypy.serving.response + error_msg = self.check_username_and_password(username, password) + if error_msg: + body = self.login_screen(from_page, username, error_msg) + response.body = body + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + else: + cherrypy.serving.request.login = username + cherrypy.session[self.session_key] = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or '/') + + def do_logout(self, from_page='..', **kwargs): + """Logout. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + username = sess.get(self.session_key) + sess[self.session_key] = None + if username: + cherrypy.serving.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page) + + def do_check(self): + """Assert username. Raise redirect, or return True if request handled. + """ + sess = cherrypy.session + request = cherrypy.serving.request + response = cherrypy.serving.response + + username = sess.get(self.session_key) + if not username: + sess[self.session_key] = username = self.anonymous() + self._debug_message('No session[username], trying anonymous') + if not username: + url = cherrypy.url(qs=request.query_string) + self._debug_message( + 'No username, routing to login_screen with from_page %(url)r', + locals(), + ) + response.body = self.login_screen(url) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + self._debug_message('Setting request.login to %(username)r', locals()) + request.login = username + self.on_check(username) + + def _debug_message(self, template, context={}): + if not self.debug: + return + cherrypy.log(template % context, 'TOOLS.SESSAUTH') + + def run(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + path = request.path_info + if path.endswith('login_screen'): + self._debug_message('routing %(path)r to login_screen', locals()) + response.body = self.login_screen() + return True + elif path.endswith('do_login'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + self._debug_message('do_login requires POST') + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_login', locals()) + return self.do_login(**request.params) + elif path.endswith('do_logout'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_logout', locals()) + return self.do_logout(**request.params) + else: + self._debug_message('No special path, running do_check') + return self.do_check() + + +def session_auth(**kwargs): + sa = SessionAuth() + for k, v in kwargs.items(): + setattr(sa, k, v) + return sa.run() + + +session_auth.__doc__ = ( + """Session authentication hook. + + Any attribute of the SessionAuth class may be overridden via a keyword arg + to this function: + + """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__) + for k in dir(SessionAuth) if not k.startswith('__')]) +) + + +def log_traceback(severity=logging.ERROR, debug=False): + """Write the last error's traceback to the cherrypy error log.""" + cherrypy.log('', 'HTTP', severity=severity, traceback=True) + + +def log_request_headers(debug=False): + """Write request headers to the cherrypy error log.""" + h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list] + cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP') + + +def log_hooks(debug=False): + """Write request.hooks to the cherrypy error log.""" + request = cherrypy.serving.request + + msg = [] + # Sort by the standard points if possible. + from cherrypy import _cprequest + points = _cprequest.hookpoints + for k in request.hooks.keys(): + if k not in points: + points.append(k) + + for k in points: + msg.append(' %s:' % k) + v = request.hooks.get(k, []) + v.sort() + for h in v: + msg.append(' %r' % h) + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + + ':\n' + '\n'.join(msg), 'HTTP') + + +def redirect(url='', internal=True, debug=False): + """Raise InternalRedirect or HTTPRedirect to the given url.""" + if debug: + cherrypy.log('Redirecting %sto: %s' % + ({True: 'internal ', False: ''}[internal], url), + 'TOOLS.REDIRECT') + if internal: + raise cherrypy.InternalRedirect(url) + else: + raise cherrypy.HTTPRedirect(url) + + +def trailing_slash(missing=True, extra=False, status=None, debug=False): + """Redirect if path_info has (missing|extra) trailing slash.""" + request = cherrypy.serving.request + pi = request.path_info + + if debug: + cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % + (request.is_index, missing, extra, pi), + 'TOOLS.TRAILING_SLASH') + if request.is_index is True: + if missing: + if not pi.endswith('/'): + new_url = cherrypy.url(pi + '/', request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + elif request.is_index is False: + if extra: + # If pi == '/', don't redirect to ''! + if pi.endswith('/') and pi != '/': + new_url = cherrypy.url(pi[:-1], request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + + +def flatten(debug=False): + """Wrap response.body in a generator that recursively iterates over body. + + This allows cherrypy.response.body to consist of 'nested generators'; + that is, a set of generators that yield generators. + """ + def flattener(input): + numchunks = 0 + for x in input: + if not is_iterator(x): + numchunks += 1 + yield x + else: + for y in flattener(x): + numchunks += 1 + yield y + if debug: + cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') + response = cherrypy.serving.response + response.body = flattener(response.body) + + +def accept(media=None, debug=False): + """Return the client's preferred media-type (from the given Content-Types). + + If 'media' is None (the default), no test will be performed. + + If 'media' is provided, it should be the Content-Type value (as a string) + or values (as a list or tuple of strings) which the current resource + can emit. The client's acceptable media ranges (as declared in the + Accept request header) will be matched in order to these Content-Type + values; the first such string is returned. That is, the return value + will always be one of the strings provided in the 'media' arg (or None + if 'media' is None). + + If no match is found, then HTTPError 406 (Not Acceptable) is raised. + Note that most web browsers send */* as a (low-quality) acceptable + media range, which should match any Content-Type. In addition, "...if + no Accept header field is present, then it is assumed that the client + accepts all media types." + + Matching types are checked in order of client preference first, + and then in the order of the given 'media' values. + + Note that this function does not honor accept-params (other than "q"). + """ + if not media: + return + if isinstance(media, text_or_bytes): + media = [media] + request = cherrypy.serving.request + + # Parse the Accept request header, and try to match one + # of the requested media-ranges (in order of preference). + ranges = request.headers.elements('Accept') + if not ranges: + # Any media type is acceptable. + if debug: + cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') + return media[0] + else: + # Note that 'ranges' is sorted in order of preference + for element in ranges: + if element.qvalue > 0: + if element.value == '*/*': + # Matches any type or subtype + if debug: + cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') + return media[0] + elif element.value.endswith('/*'): + # Matches any subtype + mtype = element.value[:-1] # Keep the slash + for m in media: + if m.startswith(mtype): + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return m + else: + # Matches exact value + if element.value in media: + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return element.value + + # No suitable media-range found. + ah = request.headers.get('Accept') + if ah is None: + msg = 'Your client did not send an Accept header.' + else: + msg = 'Your client sent this Accept header: %s.' % ah + msg += (' But this resource only emits these media types: %s.' % + ', '.join(media)) + raise cherrypy.HTTPError(406, msg) + + +class MonitoredHeaderMap(_httputil.HeaderMap): + + def transform_key(self, key): + self.accessed_headers.add(key) + return super(MonitoredHeaderMap, self).transform_key(key) + + def __init__(self): + self.accessed_headers = set() + super(MonitoredHeaderMap, self).__init__() + + +def autovary(ignore=None, debug=False): + """Auto-populate the Vary response header based on request.header access. + """ + request = cherrypy.serving.request + + req_h = request.headers + request.headers = MonitoredHeaderMap() + request.headers.update(req_h) + if ignore is None: + ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) + + def set_response_header(): + resp_h = cherrypy.serving.response.headers + v = set([e.value for e in resp_h.elements('Vary')]) + if debug: + cherrypy.log( + 'Accessed headers: %s' % request.headers.accessed_headers, + 'TOOLS.AUTOVARY') + v = v.union(request.headers.accessed_headers) + v = v.difference(ignore) + v = list(v) + v.sort() + resp_h['Vary'] = ', '.join(v) + request.hooks.attach('before_finalize', set_response_header, 95) + + +def convert_params(exception=ValueError, error=400): + """Convert request params based on function annotations, with error handling. + + exception + Exception class to catch. + + status + The HTTP error code to return to the client on failure. + """ + request = cherrypy.serving.request + types = request.handler.callable.__annotations__ + with cherrypy.HTTPError.handle(exception, error): + for key in set(types).intersection(request.params): + request.params[key] = types[key](request.params[key]) diff --git a/libraries/cherrypy/lib/encoding.py b/libraries/cherrypy/lib/encoding.py new file mode 100644 index 00000000..3d001ca6 --- /dev/null +++ b/libraries/cherrypy/lib/encoding.py @@ -0,0 +1,436 @@ +import struct +import time +import io + +import six + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import file_generator +from cherrypy.lib import is_closable_iterator +from cherrypy.lib import set_vary_header + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is + given in the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request + entity is *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class UTF8StreamEncoder: + def __init__(self, iterator): + self._iterator = iterator + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + res = next(self._iterator) + if isinstance(res, six.text_type): + res = res.encode('utf-8') + return res + + def close(self): + if is_closable_iterator(self._iterator): + self._iterator.close() + + def __getattr__(self, attr): + if attr.startswith('__'): + raise AttributeError(self, attr) + return getattr(self._iterator, attr) + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = 'Response body could not be encoded with %r.' + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, six.text_type): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + body = [] + for chunk in self.body: + if isinstance(chunk, six.text_type): + try: + chunk = chunk.encode(encoding, self.errors) + except (LookupError, UnicodeError): + return False + body.append(chunk) + self.body = body + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % + response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> t = u"\u7007\u3040" + # >>> len(t) + # 2 + # >>> len(t.encode("UTF-8")) + # 6 + # >>> len(t.encode("utf7")) + # 8 + del response.headers['Content-Length'] + + # Parse the Accept-Charset request header, and try to provide one + # of the requested charsets (in order of user preference). + encs = request.headers.elements('Accept-Charset') + charsets = [enc.value.lower() for enc in encs] + if self.debug: + cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') + + if self.encoding is not None: + # If specified, force this encoding to be used, or fail. + encoding = self.encoding.lower() + if self.debug: + cherrypy.log('Specified encoding %r' % + encoding, 'TOOLS.ENCODE') + if (not charsets) or '*' in charsets or encoding in charsets: + if self.debug: + cherrypy.log('Attempting encoding %r' % + encoding, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + else: + if not encs: + if self.debug: + cherrypy.log('Attempting default encoding %r' % + self.default_encoding, 'TOOLS.ENCODE') + # Any character-set is acceptable. + if encoder(self.default_encoding): + return self.default_encoding + else: + raise cherrypy.HTTPError(500, self.failmsg % + self.default_encoding) + else: + for element in encs: + if element.qvalue > 0: + if element.value == '*': + # Matches any charset. Try our default. + if self.debug: + cherrypy.log('Attempting default encoding due ' + 'to %r' % element, 'TOOLS.ENCODE') + if encoder(self.default_encoding): + return self.default_encoding + else: + encoding = element.value + if self.debug: + cherrypy.log('Attempting encoding %s (qvalue >' + '0)' % element, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + + if '*' not in charsets: + # If no "*" is present in an Accept-Charset field, then all + # character sets not explicitly mentioned get a quality + # value of 0, except for ISO-8859-1, which gets a quality + # value of 1 if not explicitly mentioned. + iso = 'iso-8859-1' + if iso not in charsets: + if self.debug: + cherrypy.log('Attempting ISO-8859-1 encoding', + 'TOOLS.ENCODE') + if encoder(iso): + return iso + + # No suitable encoding found. + ac = request.headers.get('Accept-Charset') + if ac is None: + msg = 'Your client did not send an Accept-Charset header.' + else: + msg = 'Your client sent this Accept-Charset header: %s.' % ac + _charsets = ', '.join(sorted(self.attempted_charsets)) + msg += ' We tried these charsets: %s.' % (_charsets,) + raise cherrypy.HTTPError(406, msg) + + def __call__(self, *args, **kwargs): + response = cherrypy.serving.response + self.body = self.oldhandler(*args, **kwargs) + + self.body = prepare_iter(self.body) + + ct = response.headers.elements('Content-Type') + if self.debug: + cherrypy.log('Content-Type: %r' % [str(h) + for h in ct], 'TOOLS.ENCODE') + if ct and self.add_charset: + ct = ct[0] + if self.text_only: + if ct.value.lower().startswith('text/'): + if self.debug: + cherrypy.log( + 'Content-Type %s starts with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = True + else: + if self.debug: + cherrypy.log('Not finding because Content-Type %s ' + 'does not start with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = False + else: + if self.debug: + cherrypy.log('Finding because not text_only', + 'TOOLS.ENCODE') + do_find = True + + if do_find: + # Set "charset=..." param on response Content-Type header + ct.params['charset'] = self.find_acceptable_charset() + if self.debug: + cherrypy.log('Setting Content-Type %s' % ct, + 'TOOLS.ENCODE') + response.headers['Content-Type'] = str(ct) + + return self.body + + +def prepare_iter(value): + """ + Ensure response body is iterable and resolves to False when empty. + """ + if isinstance(value, text_or_bytes): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if value: + value = [value] + else: + # [''] doesn't evaluate to False, so replace it with []. + value = [] + # Don't use isinstance here; io.IOBase which has an ABC takes + # 1000 times as long as, say, isinstance(value, str) + elif hasattr(value, 'read'): + value = file_generator(value) + elif value is None: + value = [] + return value + + +# GZIP + + +def compress(body, compress_level): + """Compress 'body' at the given compress_level.""" + import zlib + + # See http://www.gzip.org/zlib/rfc-gzip.html + yield b'\x1f\x8b' # ID1 and ID2: gzip marker + yield b'\x08' # CM: compression method + yield b'\x00' # FLG: none set + # MTIME: 4 bytes + yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16)) + yield b'\x02' # XFL: max compression, slowest algo + yield b'\xff' # OS: unknown + + crc = zlib.crc32(b'') + size = 0 + zobj = zlib.compressobj(compress_level, + zlib.DEFLATED, -zlib.MAX_WBITS, + zlib.DEF_MEM_LEVEL, 0) + for line in body: + size += len(line) + crc = zlib.crc32(line, crc) + yield zobj.compress(line) + yield zobj.flush() + + # CRC32: 4 bytes + yield struct.pack('<L', crc & int('FFFFFFFF', 16)) + # ISIZE: 4 bytes + yield struct.pack('<L', size & int('FFFFFFFF', 16)) + + +def decompress(body): + import gzip + + zbuf = io.BytesIO() + zbuf.write(body) + zbuf.seek(0) + zfile = gzip.GzipFile(mode='rb', fileobj=zbuf) + data = zfile.read() + zfile.close() + return data + + +def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], + debug=False): + """Try to gzip the response body if Content-Type in mime_types. + + cherrypy.response.headers['Content-Type'] must be set to one of the + values in the mime_types arg before calling this function. + + The provided list of mime-types must be of one of the following form: + * `type/subtype` + * `type/*` + * `type/*+subtype` + + No compression is performed if any of the following hold: + * The client sends no Accept-Encoding request header + * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header + * No 'gzip' or 'x-gzip' with a qvalue > 0 is present + * The 'identity' value is given with a qvalue > 0. + + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + set_vary_header(response, 'Accept-Encoding') + + if not response.body: + # Response body is empty (might be a 304 for instance) + if debug: + cherrypy.log('No response body', context='TOOLS.GZIP') + return + + # If returning cached content (which should already have been gzipped), + # don't re-zip. + if getattr(request, 'cached', False): + if debug: + cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') + return + + acceptable = request.headers.elements('Accept-Encoding') + if not acceptable: + # If no Accept-Encoding field is present in a request, + # the server MAY assume that the client will accept any + # content coding. In this case, if "identity" is one of + # the available content-codings, then the server SHOULD use + # the "identity" content-coding, unless it has additional + # information that a different content-coding is meaningful + # to the client. + if debug: + cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') + return + + ct = response.headers.get('Content-Type', '').split(';')[0] + for coding in acceptable: + if coding.value == 'identity' and coding.qvalue != 0: + if debug: + cherrypy.log('Non-zero identity qvalue: %s' % coding, + context='TOOLS.GZIP') + return + if coding.value in ('gzip', 'x-gzip'): + if coding.qvalue == 0: + if debug: + cherrypy.log('Zero gzip qvalue: %s' % coding, + context='TOOLS.GZIP') + return + + if ct not in mime_types: + # If the list of provided mime-types contains tokens + # such as 'text/*' or 'application/*+xml', + # we go through them and find the most appropriate one + # based on the given content-type. + # The pattern matching is only caring about the most + # common cases, as stated above, and doesn't support + # for extra parameters. + found = False + if '/' in ct: + ct_media_type, ct_sub_type = ct.split('/') + for mime_type in mime_types: + if '/' in mime_type: + media_type, sub_type = mime_type.split('/') + if ct_media_type == media_type: + if sub_type == '*': + found = True + break + elif '+' in sub_type and '+' in ct_sub_type: + ct_left, ct_right = ct_sub_type.split('+') + left, right = sub_type.split('+') + if left == '*' and ct_right == right: + found = True + break + + if not found: + if debug: + cherrypy.log('Content-Type %s not in mime_types %r' % + (ct, mime_types), context='TOOLS.GZIP') + return + + if debug: + cherrypy.log('Gzipping', context='TOOLS.GZIP') + # Return a generator that compresses the page + response.headers['Content-Encoding'] = 'gzip' + response.body = compress(response.body, compress_level) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + return + + if debug: + cherrypy.log('No acceptable encoding found.', context='GZIP') + cherrypy.HTTPError(406, 'identity, gzip').set_response() diff --git a/libraries/cherrypy/lib/gctools.py b/libraries/cherrypy/lib/gctools.py new file mode 100644 index 00000000..26746d78 --- /dev/null +++ b/libraries/cherrypy/lib/gctools.py @@ -0,0 +1,218 @@ +import gc +import inspect +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > self.maxparents: + return [('[%s referrers]' % len(refs), [])] + + try: + ascendcode = self.ascend.__code__ + except AttributeError: + ascendcode = self.ascend.im_func.func_code + for parent in refs: + if inspect.isframe(parent) and parent.f_code is ascendcode: + continue + if parent in self.ignore: + continue + if depth <= self.maxdepth: + parents.append((parent, self.ascend(parent, depth))) + else: + parents.append((parent, [])) + + return parents + + def peek(self, s): + """Return s, restricted to a sane length.""" + if len(s) > (self.peek_length + 3): + half = self.peek_length // 2 + return s[:half] + '...' + s[-half:] + else: + return s + + def _format(self, obj, descend=True): + """Return a string representation of a single object.""" + if inspect.isframe(obj): + filename, lineno, func, context, index = inspect.getframeinfo(obj) + return "<frame of function '%s'>" % func + + if not descend: + return self.peek(repr(obj)) + + if isinstance(obj, dict): + return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False), + self._format(v, descend=False)) + for k, v in obj.items()]) + '}' + elif isinstance(obj, list): + return '[' + ', '.join([self._format(item, descend=False) + for item in obj]) + ']' + elif isinstance(obj, tuple): + return '(' + ', '.join([self._format(item, descend=False) + for item in obj]) + ')' + + r = self.peek(repr(obj)) + if isinstance(obj, (str, int, float)): + return r + return '%s: %s' % (type(obj), r) + + def format(self, tree): + """Return a list of string reprs from a nested list of referrers.""" + output = [] + + def ascend(branch, depth=1): + for parent, grandparents in branch: + output.append((' ' * depth) + self._format(parent)) + if grandparents: + ascend(grandparents, depth + 1) + ascend(tree) + return output + + +def get_instances(cls): + return [x for x in gc.get_objects() if isinstance(x, cls)] + + +class RequestCounter(SimplePlugin): + + def start(self): + self.count = 0 + + def before_request(self): + self.count += 1 + + def after_request(self): + self.count -= 1 + + +request_counter = RequestCounter(cherrypy.engine) +request_counter.subscribe() + + +def get_context(obj): + if isinstance(obj, _cprequest.Request): + return 'path=%s;stage=%s' % (obj.path_info, obj.stage) + elif isinstance(obj, _cprequest.Response): + return 'status=%s' % obj.status + elif isinstance(obj, _cpwsgi.AppResponse): + return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '') + elif hasattr(obj, 'tb_lineno'): + return 'tb_lineno=%s' % obj.tb_lineno + return '' + + +class GCRoot(object): + + """A CherryPy page handler for testing reference leaks.""" + + classes = [ + (_cprequest.Request, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cprequest.Response, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cpwsgi.AppResponse, 1, 1, + 'Should be 1 in this request thread only.'), + ] + + @cherrypy.expose + def index(self): + return 'Hello, world!' + + @cherrypy.expose + def stats(self): + output = ['Statistics:'] + + for trial in range(10): + if request_counter.count > 0: + break + time.sleep(0.5) + else: + output.append('\nNot all requests closed properly.') + + # gc_collect isn't perfectly synchronous, because it may + # break reference cycles that then take time to fully + # finalize. Call it thrice and hope for the best. + gc.collect() + gc.collect() + unreachable = gc.collect() + if unreachable: + if objgraph is not None: + final = objgraph.by_type('Nondestructible') + if final: + objgraph.show_backrefs(final, filename='finalizers.png') + + trash = {} + for x in gc.garbage: + trash[type(x)] = trash.get(type(x), 0) + 1 + if trash: + output.insert(0, '\n%s unreachable objects:' % unreachable) + trash = [(v, k) for k, v in trash.items()] + trash.sort() + for pair in trash: + output.append(' ' + repr(pair)) + + # Check declared classes to verify uncollected instances. + # These don't have to be part of a cycle; they can be + # any objects that have unanticipated referrers that keep + # them from being collected. + allobjs = {} + for cls, minobj, maxobj, msg in self.classes: + allobjs[cls] = get_instances(cls) + + for cls, minobj, maxobj, msg in self.classes: + objs = allobjs[cls] + lenobj = len(objs) + if lenobj < minobj or lenobj > maxobj: + if minobj == maxobj: + output.append( + '\nExpected %s %r references, got %s.' % + (minobj, cls, lenobj)) + else: + output.append( + '\nExpected %s to %s %r references, got %s.' % + (minobj, maxobj, cls, lenobj)) + + for obj in objs: + if objgraph is not None: + ig = [id(objs), id(inspect.currentframe())] + fname = 'graph_%s_%s.png' % (cls.__name__, id(obj)) + objgraph.show_backrefs( + obj, extra_ignore=ig, max_depth=4, too_many=20, + filename=fname, extra_info=get_context) + output.append('\nReferrers for %s (refcount=%s):' % + (repr(obj), sys.getrefcount(obj))) + t = ReferrerTree(ignore=[objs], maxdepth=3) + tree = t.ascend(obj) + output.extend(t.format(tree)) + + return '\n'.join(output) diff --git a/libraries/cherrypy/lib/httputil.py b/libraries/cherrypy/lib/httputil.py new file mode 100644 index 00000000..b68d8dd5 --- /dev/null +++ b/libraries/cherrypy/lib/httputil.py @@ -0,0 +1,581 @@ +"""HTTP library functions. + +This module contains functions for building an HTTP application +framework: any one, not just one whose name starts with "Ch". ;) If you +reference any modules from some popular framework inside *this* module, +FuManChu will personally hang you up by your thumbs and submit you +to a public caning. +""" + +import functools +import email.utils +import re +from binascii import b2a_base64 +from cgi import parse_header +from email.header import decode_header + +import six +from six.moves import range, builtins, map +from six.moves.BaseHTTPServer import BaseHTTPRequestHandler + +import cherrypy +from cherrypy._cpcompat import ntob, ntou +from cherrypy._cpcompat import unquote_plus + +response_codes = BaseHTTPRequestHandler.responses.copy() + +# From https://github.com/cherrypy/cherrypy/issues/361 +response_codes[500] = ('Internal Server Error', + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.') +response_codes[503] = ('Service Unavailable', + 'The server is currently unable to handle the ' + 'request due to a temporary overloading or ' + 'maintenance of the server.') + + +HTTPDate = functools.partial(email.utils.formatdate, usegmt=True) + + +def urljoin(*atoms): + r"""Return the given path \*atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = '/'.join([x for x in atoms if x]) + while '//' in url: + url = url.replace('//', '/') + # Special-case the final url of "", and return "/" instead. + return url or '/' + + +def urljoin_bytes(*atoms): + """Return the given path `*atoms`, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = b'/'.join([x for x in atoms if x]) + while b'//' in url: + url = url.replace(b'//', b'/') + # Special-case the final url of "", and return "/" instead. + return url or b'/' + + +def protocol_from_http(protocol_str): + """Return a protocol tuple from the given 'HTTP/x.y' string.""" + return int(protocol_str[5]), int(protocol_str[7]) + + +def get_ranges(headervalue, content_length): + """Return a list of (start, stop) indices from a Range header, or None. + + Each (start, stop) tuple will be composed of two ints, which are suitable + for use in a slicing operation. That is, the header "Range: bytes=3-6", + if applied against a Python string, is requesting resource[3:7]. This + function will return the list [(3, 7)]. + + If this function returns an empty list, you should return HTTP 416. + """ + + if not headervalue: + return None + + result = [] + bytesunit, byteranges = headervalue.split('=', 1) + for brange in byteranges.split(','): + start, stop = [x.strip() for x in brange.split('-', 1)] + if start: + if not stop: + stop = content_length - 1 + start, stop = int(start), int(stop) + if start >= content_length: + # From rfc 2616 sec 14.16: + # "If the server receives a request (other than one + # including an If-Range request-header field) with an + # unsatisfiable Range request-header field (that is, + # all of whose byte-range-spec values have a first-byte-pos + # value greater than the current length of the selected + # resource), it SHOULD return a response code of 416 + # (Requested range not satisfiable)." + continue + if stop < start: + # From rfc 2616 sec 14.16: + # "If the server ignores a byte-range-spec because it + # is syntactically invalid, the server SHOULD treat + # the request as if the invalid Range header field + # did not exist. (Normally, this means return a 200 + # response containing the full entity)." + return None + result.append((start, stop + 1)) + else: + if not stop: + # See rfc quote above. + return None + # Negative subscript (last N bytes) + # + # RFC 2616 Section 14.35.1: + # If the entity is shorter than the specified suffix-length, + # the entire entity-body is used. + if int(stop) > content_length: + result.append((0, content_length)) + else: + result.append((content_length - int(stop), content_length)) + + return result + + +class HeaderElement(object): + + """An element (with parameters) from an HTTP header's element list.""" + + def __init__(self, value, params=None): + self.value = value + if params is None: + params = {} + self.params = params + + def __cmp__(self, other): + return builtins.cmp(self.value, other.value) + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)] + return str('%s%s' % (self.value, ''.join(p))) + + def __bytes__(self): + return ntob(self.__str__()) + + def __unicode__(self): + return ntou(self.__str__()) + + @staticmethod + def parse(elementstr): + """Transform 'token;key=val' to ('token', {'key': 'val'}).""" + initial_value, params = parse_header(elementstr) + return initial_value, params + + @classmethod + def from_str(cls, elementstr): + """Construct an instance from a string of the form 'token;key=val'.""" + ival, params = cls.parse(elementstr) + return cls(ival, params) + + +q_separator = re.compile(r'; *q *=') + + +class AcceptElement(HeaderElement): + + """An element (with parameters) from an Accept* header's element list. + + AcceptElement objects are comparable; the more-preferred object will be + "less than" the less-preferred object. They are also therefore sortable; + if you sort a list of AcceptElement objects, they will be listed in + priority order; the most preferred value will be first. Yes, it should + have been the other way around, but it's too late to fix now. + """ + + @classmethod + def from_str(cls, elementstr): + qvalue = None + # The first "q" parameter (if any) separates the initial + # media-range parameter(s) (if any) from the accept-params. + atoms = q_separator.split(elementstr, 1) + media_range = atoms.pop(0).strip() + if atoms: + # The qvalue for an Accept header can have extensions. The other + # headers cannot, but it's easier to parse them as if they did. + qvalue = HeaderElement.from_str(atoms[0].strip()) + + media_type, params = cls.parse(media_range) + if qvalue is not None: + params['q'] = qvalue + return cls(media_type, params) + + @property + def qvalue(self): + 'The qvalue, or priority, of this value.' + val = self.params.get('q', '1') + if isinstance(val, HeaderElement): + val = val.value + try: + return float(val) + except ValueError as val_err: + """Fail client requests with invalid quality value. + + Ref: https://github.com/cherrypy/cherrypy/issues/1370 + """ + six.raise_from( + cherrypy.HTTPError( + 400, + 'Malformed HTTP header: `{}`'. + format(str(self)), + ), + val_err, + ) + + def __cmp__(self, other): + diff = builtins.cmp(self.qvalue, other.qvalue) + if diff == 0: + diff = builtins.cmp(str(self), str(other)) + return diff + + def __lt__(self, other): + if self.qvalue == other.qvalue: + return str(self) < str(other) + else: + return self.qvalue < other.qvalue + + +RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') + + +def header_elements(fieldname, fieldvalue): + """Return a sorted HeaderElement list from a comma-separated header string. + """ + if not fieldvalue: + return [] + + result = [] + for element in RE_HEADER_SPLIT.split(fieldvalue): + if fieldname.startswith('Accept') or fieldname == 'TE': + hv = AcceptElement.from_str(element) + else: + hv = HeaderElement.from_str(element) + result.append(hv) + + return list(reversed(sorted(result))) + + +def decode_TEXT(value): + r""" + Decode :rfc:`2047` TEXT + + >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') + True + """ + atoms = decode_header(value) + decodedvalue = '' + for atom, charset in atoms: + if charset is not None: + atom = atom.decode(charset) + decodedvalue += atom + return decodedvalue + + +def decode_TEXT_maybe(value): + """ + Decode the text but only if '=?' appears in it. + """ + return decode_TEXT(value) if '=?' in value else value + + +def valid_status(status): + """Return legal HTTP status Code, Reason-phrase and Message. + + The status arg must be an int, a str that begins with an int + or the constant from ``http.client`` stdlib module. + + If status has no reason-phrase is supplied, a default reason- + phrase will be provided. + + >>> from six.moves import http_client + >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler + >>> valid_status(http_client.ACCEPTED) == ( + ... int(http_client.ACCEPTED), + ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED] + True + """ + + if not status: + status = 200 + + code, reason = status, None + if isinstance(status, six.string_types): + code, _, reason = status.partition(' ') + reason = reason.strip() or None + + try: + code = int(code) + except (TypeError, ValueError): + raise ValueError('Illegal response status from server ' + '(%s is non-numeric).' % repr(code)) + + if code < 100 or code > 599: + raise ValueError('Illegal response status from server ' + '(%s is out of range).' % repr(code)) + + if code not in response_codes: + # code is unknown but not illegal + default_reason, message = '', '' + else: + default_reason, message = response_codes[code] + + if reason is None: + reason = default_reason + + return code, reason, message + + +# NOTE: the parse_qs functions that follow are modified version of those +# in the python3.0 source - we need to pass through an encoding to the unquote +# method, but the default parse_qs function doesn't allow us to. These do. + +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): + """Parse a query given as a string argument. + + Arguments: + + qs: URL-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + URL encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + Returns a dict, as G-d intended. + """ + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + d = {} + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError('bad query field: %r' % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = unquote_plus(nv[0], encoding, errors='strict') + value = unquote_plus(nv[1], encoding, errors='strict') + if name in d: + if not isinstance(d[name], list): + d[name] = [d[name]] + d[name].append(value) + else: + d[name] = value + return d + + +image_map_pattern = re.compile(r'[0-9]+,[0-9]+') + + +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): + """Build a params dictionary from a query_string. + + Duplicate key/value pairs in the provided query_string will be + returned as {'key': [val1, val2, ...]}. Single key/values will + be returned as strings: {'key': 'value'}. + """ + if image_map_pattern.match(query_string): + # Server-side image map. Map the coords to 'x' and 'y' + # (like CGI::Request does). + pm = query_string.split(',') + pm = {'x': int(pm[0]), 'y': int(pm[1])} + else: + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) + return pm + + +#### +# Inlined from jaraco.collections 1.5.2 +# Ref #1673 +class KeyTransformingDict(dict): + """ + A dict subclass that transforms the keys before they're used. + Subclasses may override the default transform_key to customize behavior. + """ + @staticmethod + def transform_key(key): + return key + + def __init__(self, *args, **kargs): + super(KeyTransformingDict, self).__init__() + # build a dictionary using the default constructs + d = dict(*args, **kargs) + # build this dictionary using transformed keys. + for item in d.items(): + self.__setitem__(*item) + + def __setitem__(self, key, val): + key = self.transform_key(key) + super(KeyTransformingDict, self).__setitem__(key, val) + + def __getitem__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__getitem__(key) + + def __contains__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__contains__(key) + + def __delitem__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__delitem__(key) + + def get(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).get(key, *args, **kwargs) + + def setdefault(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).setdefault( + key, *args, **kwargs) + + def pop(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).pop(key, *args, **kwargs) + + def matching_key_for(self, key): + """ + Given a key, return the actual key stored in self that matches. + Raise KeyError if the key isn't found. + """ + try: + return next(e_key for e_key in self.keys() if e_key == key) + except StopIteration: + raise KeyError(key) +#### + + +class CaseInsensitiveDict(KeyTransformingDict): + + """A case-insensitive dict subclass. + + Each key is changed on entry to str(key).title(). + """ + + @staticmethod + def transform_key(key): + return str(key).title() + + +# TEXT = <any OCTET except CTLs, but including LWS> +# +# A CRLF is allowed in the definition of TEXT only as part of a header +# field continuation. It is expected that the folding LWS will be +# replaced with a single SP before interpretation of the TEXT value." +if str == bytes: + header_translate_table = ''.join([chr(i) for i in range(256)]) + header_translate_deletechars = ''.join( + [chr(i) for i in range(32)]) + chr(127) +else: + header_translate_table = None + header_translate_deletechars = bytes(range(32)) + bytes([127]) + + +class HeaderMap(CaseInsensitiveDict): + + """A dict subclass for HTTP request and response headers. + + Each key is changed on entry to str(key).title(). This allows headers + to be case-insensitive and avoid duplicates. + + Values are header values (decoded according to :rfc:`2047` if necessary). + """ + + protocol = (1, 1) + encodings = ['ISO-8859-1'] + + # Someday, when http-bis is done, this will probably get dropped + # since few servers, clients, or intermediaries do it. But until then, + # we're going to obey the spec as is. + # "Words of *TEXT MAY contain characters from character sets other than + # ISO-8859-1 only when encoded according to the rules of RFC 2047." + use_rfc_2047 = True + + def elements(self, key): + """Return a sorted list of HeaderElements for the given header.""" + key = str(key).title() + value = self.get(key) + return header_elements(key, value) + + def values(self, key): + """Return a sorted list of HeaderElement.value for the given header.""" + return [e.value for e in self.elements(key)] + + def output(self): + """Transform self into a list of (name, value) tuples.""" + return list(self.encode_header_items(self.items())) + + @classmethod + def encode_header_items(cls, header_items): + """ + Prepare the sequence of name, value tuples into a form suitable for + transmitting on the wire for HTTP. + """ + for k, v in header_items: + if not isinstance(v, six.string_types): + v = six.text_type(v) + + yield tuple(map(cls.encode_header_item, (k, v))) + + @classmethod + def encode_header_item(cls, item): + if isinstance(item, six.text_type): + item = cls.encode(item) + + # See header_translate_* constants above. + # Replace only if you really know what you're doing. + return item.translate( + header_translate_table, header_translate_deletechars) + + @classmethod + def encode(cls, v): + """Return the given header name or value, encoded for HTTP output.""" + for enc in cls.encodings: + try: + return v.encode(enc) + except UnicodeEncodeError: + continue + + if cls.protocol == (1, 1) and cls.use_rfc_2047: + # Encode RFC-2047 TEXT + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). + # We do our own here instead of using the email module + # because we never want to fold lines--folding has + # been deprecated by the HTTP working group. + v = b2a_base64(v.encode('utf-8')) + return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=') + + raise ValueError('Could not encode header part %r using ' + 'any of the encodings %r.' % + (v, cls.encodings)) + + +class Host(object): + + """An internet address. + + name + Should be the client's host name. If not available (because no DNS + lookup is performed), the IP address should be used instead. + + """ + + ip = '0.0.0.0' + port = 80 + name = 'unknown.tld' + + def __init__(self, ip, port, name=None): + self.ip = ip + self.port = port + if name is None: + name = ip + self.name = name + + def __repr__(self): + return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) diff --git a/libraries/cherrypy/lib/jsontools.py b/libraries/cherrypy/lib/jsontools.py new file mode 100644 index 00000000..48683097 --- /dev/null +++ b/libraries/cherrypy/lib/jsontools.py @@ -0,0 +1,88 @@ +import cherrypy +from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode + + +def json_processor(entity): + """Read application/json data into request.json.""" + if not entity.headers.get(ntou('Content-Length'), ntou('')): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): + cherrypy.serving.request.json = json_decode(body.decode('utf-8')) + + +def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], + force=True, debug=False, processor=json_processor): + """Add a processor to parse JSON request entities: + The default processor places the parsed data into request.json. + + Incoming request entities which match the given content_type(s) will + be deserialized from JSON to the Python equivalent, and the result + stored at cherrypy.request.json. The 'content_type' argument may + be a Content-Type string or a list of allowable Content-Type strings. + + If the 'force' argument is True (the default), then entities of other + content types will not be allowed; "415 Unsupported Media Type" is + raised instead. + + Supply your own processor to use a custom decoder, or to handle the parsed + data differently. The processor can be configured via + tools.json_in.processor or via the decorator method. + + Note that the deserializer requires the client send a Content-Length + request header, or it will raise "411 Length Required". If for any + other reason the request entity cannot be deserialized from JSON, + it will raise "400 Bad Request: Invalid JSON document". + """ + request = cherrypy.serving.request + if isinstance(content_type, text_or_bytes): + content_type = [content_type] + + if force: + if debug: + cherrypy.log('Removing body processors %s' % + repr(request.body.processors.keys()), 'TOOLS.JSON_IN') + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an entity of content type %s' % + ', '.join(content_type)) + + for ct in content_type: + if debug: + cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') + request.body.processors[ct] = processor + + +def json_handler(*args, **kwargs): + value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) + return json_encode(value) + + +def json_out(content_type='application/json', debug=False, + handler=json_handler): + """Wrap request.handler to serialize its output to JSON. Sets Content-Type. + + If the given content_type is None, the Content-Type response header + is not set. + + Provide your own handler to use a custom encoder. For example + cherrypy.config['tools.json_out.handler'] = <function>, or + @json_out(handler=function). + """ + request = cherrypy.serving.request + # request.handler may be set to None by e.g. the caching tool + # to signal to all components that a response body has already + # been attached, in which case we don't need to wrap anything. + if request.handler is None: + return + if debug: + cherrypy.log('Replacing %s with JSON handler' % request.handler, + 'TOOLS.JSON_OUT') + request._json_inner_handler = request.handler + request.handler = handler + if content_type is not None: + if debug: + cherrypy.log('Setting Content-Type to %s' % + content_type, 'TOOLS.JSON_OUT') + cherrypy.serving.response.headers['Content-Type'] = content_type diff --git a/libraries/cherrypy/lib/locking.py b/libraries/cherrypy/lib/locking.py new file mode 100644 index 00000000..317fb58c --- /dev/null +++ b/libraries/cherrypy/lib/locking.py @@ -0,0 +1,47 @@ +import datetime + + +class NeverExpires(object): + def expired(self): + return False + + +class Timer(object): + """ + A simple timer that will indicate when an expiration time has passed. + """ + def __init__(self, expiration): + 'Create a timer that expires at `expiration` (UTC datetime)' + self.expiration = expiration + + @classmethod + def after(cls, elapsed): + """ + Return a timer that will expire after `elapsed` passes. + """ + return cls(datetime.datetime.utcnow() + elapsed) + + def expired(self): + return datetime.datetime.utcnow() >= self.expiration + + +class LockTimeout(Exception): + 'An exception when a lock could not be acquired before a timeout period' + + +class LockChecker(object): + """ + Keep track of the time and detect if a timeout has expired + """ + def __init__(self, session_id, timeout): + self.session_id = session_id + if timeout: + self.timer = Timer.after(timeout) + else: + self.timer = NeverExpires() + + def expired(self): + if self.timer.expired(): + raise LockTimeout( + 'Timeout acquiring lock for %(session_id)s' % vars(self)) + return False diff --git a/libraries/cherrypy/lib/profiler.py b/libraries/cherrypy/lib/profiler.py new file mode 100644 index 00000000..fccf2eb8 --- /dev/null +++ b/libraries/cherrypy/lib/profiler.py @@ -0,0 +1,221 @@ +"""Profiler tools for CherryPy. + +CherryPy users +============== + +You can profile any of your pages as follows:: + + from cherrypy.lib import profiler + + class Root: + p = profiler.Profiler("/path/to/profile/dir") + + @cherrypy.expose + def index(self): + self.p.run(self._index) + + def _index(self): + return "Hello, world!" + + cherrypy.tree.mount(Root()) + +You can also turn on profiling for all requests +using the ``make_app`` function as WSGI middleware. + +CherryPy developers +=================== + +This module can be used whenever you make changes to CherryPy, +to get a quick sanity-check on overall CP performance. Use the +``--profile`` flag when running the test suite. Then, use the ``serve()`` +function to browse the results in a web browser. If you run this +module from the command line, it will call ``serve()`` for you. + +""" + +import io +import os +import os.path +import sys +import warnings + +import cherrypy + + +try: + import profile + import pstats + + def new_func_strip_path(func_name): + """Make profiler output more readable by adding `__init__` modules' parents + """ + filename, line, name = func_name + if filename.endswith('__init__.py'): + return ( + os.path.basename(filename[:-12]) + filename[-12:], + line, + name, + ) + return os.path.basename(filename), line, name + + pstats.func_strip_path = new_func_strip_path +except ImportError: + profile = None + pstats = None + + +_count = 0 + + +class Profiler(object): + + def __init__(self, path=None): + if not path: + path = os.path.join(os.path.dirname(__file__), 'profile') + self.path = path + if not os.path.exists(path): + os.makedirs(path) + + def run(self, func, *args, **params): + """Dump profile data into self.path.""" + global _count + c = _count = _count + 1 + path = os.path.join(self.path, 'cp_%04d.prof' % c) + prof = profile.Profile() + result = prof.runcall(func, *args, **params) + prof.dump_stats(path) + return result + + def statfiles(self): + """:rtype: list of available profiles. + """ + return [f for f in os.listdir(self.path) + if f.startswith('cp_') and f.endswith('.prof')] + + def stats(self, filename, sortby='cumulative'): + """:rtype stats(index): output of print_stats() for the given profile. + """ + sio = io.StringIO() + if sys.version_info >= (2, 5): + s = pstats.Stats(os.path.join(self.path, filename), stream=sio) + s.strip_dirs() + s.sort_stats(sortby) + s.print_stats() + else: + # pstats.Stats before Python 2.5 didn't take a 'stream' arg, + # but just printed to stdout. So re-route stdout. + s = pstats.Stats(os.path.join(self.path, filename)) + s.strip_dirs() + s.sort_stats(sortby) + oldout = sys.stdout + try: + sys.stdout = sio + s.print_stats() + finally: + sys.stdout = oldout + response = sio.getvalue() + sio.close() + return response + + @cherrypy.expose + def index(self): + return """<html> + <head><title>CherryPy profile data</title></head> + <frameset cols='200, 1*'> + <frame src='menu' /> + <frame name='main' src='' /> + </frameset> + </html> + """ + + @cherrypy.expose + def menu(self): + yield '<h2>Profiling runs</h2>' + yield '<p>Click on one of the runs below to see profiling data.</p>' + runs = self.statfiles() + runs.sort() + for i in runs: + yield "<a href='report?filename=%s' target='main'>%s</a><br />" % ( + i, i) + + @cherrypy.expose + def report(self, filename): + cherrypy.response.headers['Content-Type'] = 'text/plain' + return self.stats(filename) + + +class ProfileAggregator(Profiler): + + def __init__(self, path=None): + Profiler.__init__(self, path) + global _count + self.count = _count = _count + 1 + self.profiler = profile.Profile() + + def run(self, func, *args, **params): + path = os.path.join(self.path, 'cp_%04d.prof' % self.count) + result = self.profiler.runcall(func, *args, **params) + self.profiler.dump_stats(path) + return result + + +class make_app: + + def __init__(self, nextapp, path=None, aggregate=False): + """Make a WSGI middleware app which wraps 'nextapp' with profiling. + + nextapp + the WSGI application to wrap, usually an instance of + cherrypy.Application. + + path + where to dump the profiling output. + + aggregate + if True, profile data for all HTTP requests will go in + a single file. If False (the default), each HTTP request will + dump its profile data into a separate file. + + """ + if profile is None or pstats is None: + msg = ('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 ' + 'for details.') + warnings.warn(msg) + + self.nextapp = nextapp + self.aggregate = aggregate + if aggregate: + self.profiler = ProfileAggregator(path) + else: + self.profiler = Profiler(path) + + def __call__(self, environ, start_response): + def gather(): + result = [] + for line in self.nextapp(environ, start_response): + result.append(line) + return result + return self.profiler.run(gather) + + +def serve(path=None, port=8080): + if profile is None or pstats is None: + msg = ('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 ' + 'for details.') + warnings.warn(msg) + + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': 'production', + }) + cherrypy.quickstart(Profiler(path)) + + +if __name__ == '__main__': + serve(*tuple(sys.argv[1:])) diff --git a/libraries/cherrypy/lib/reprconf.py b/libraries/cherrypy/lib/reprconf.py new file mode 100644 index 00000000..291ab663 --- /dev/null +++ b/libraries/cherrypy/lib/reprconf.py @@ -0,0 +1,514 @@ +"""Generic configuration system using unrepr. + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, Python's +builtin ConfigParser is used (with some extensions). + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. + +The only key that cannot exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +the Config.environments dict. + +You can define your own namespaces to be called when new config is merged +by adding a named handler to Config.namespaces. The name can be any string, +and the handler must be either a callable or a context manager. +""" + +from cherrypy._cpcompat import text_or_bytes +from six.moves import configparser +from six.moves import builtins + +import operator +import sys + + +class NamespaceSet(dict): + + """A dict of config namespace names and handlers. + + Each config entry should begin with a namespace name; the corresponding + namespace handler will be called once for each config entry in that + namespace, and will be passed two arguments: the config key (with the + namespace removed) and the config value. + + Namespace handlers may be any Python callable; they may also be + Python 2.5-style 'context managers', in which case their __enter__ + method should return a callable to be used as the handler. + See cherrypy.tools (the Toolbox class) for an example. + """ + + def __call__(self, config): + """Iterate through config and pass it to each namespace handler. + + config + A flat dict, where keys use dots to separate + namespaces, and values are arbitrary. + + The first name in each config key is used to look up the corresponding + namespace handler. For example, a config entry of {'tools.gzip.on': v} + will call the 'tools' namespace handler with the args: ('gzip.on', v) + """ + # Separate the given config into namespaces + ns_confs = {} + for k in config: + if '.' in k: + ns, name = k.split('.', 1) + bucket = ns_confs.setdefault(ns, {}) + bucket[name] = config[k] + + # I chose __enter__ and __exit__ so someday this could be + # rewritten using Python 2.5's 'with' statement: + # for ns, handler in six.iteritems(self): + # with handler as callable: + # for k, v in six.iteritems(ns_confs.get(ns, {})): + # callable(k, v) + for ns, handler in self.items(): + exit = getattr(handler, '__exit__', None) + if exit: + callable = handler.__enter__() + no_exc = True + try: + try: + for k, v in ns_confs.get(ns, {}).items(): + callable(k, v) + except Exception: + # The exceptional case is handled here + no_exc = False + if exit is None: + raise + if not exit(*sys.exc_info()): + raise + # The exception is swallowed if exit() returns true + finally: + # The normal and non-local-goto cases are handled here + if no_exc and exit: + exit(None, None, None) + else: + for k, v in ns_confs.get(ns, {}).items(): + handler(k, v) + + def __repr__(self): + return '%s.%s(%s)' % (self.__module__, self.__class__.__name__, + dict.__repr__(self)) + + def __copy__(self): + newobj = self.__class__() + newobj.update(self) + return newobj + copy = __copy__ + + +class Config(dict): + + """A dict-like set of configuration data, with defaults and namespaces. + + May take a file, filename, or dict. + """ + + defaults = {} + environments = {} + namespaces = NamespaceSet() + + def __init__(self, file=None, **kwargs): + self.reset() + if file is not None: + self.update(file) + if kwargs: + self.update(kwargs) + + def reset(self): + """Reset self to default values.""" + self.clear() + dict.update(self, self.defaults) + + def update(self, config): + """Update self from a dict, file, or filename.""" + self._apply(Parser.load(config)) + + def _apply(self, config): + """Update self from a dict.""" + which_env = config.get('environment') + if which_env: + env = self.environments[which_env] + for k in env: + if k not in config: + config[k] = env[k] + + dict.update(self, config) + self.namespaces(config) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, v) + self.namespaces({k: v}) + + +class Parser(configparser.ConfigParser): + + """Sub-class of ConfigParser that keeps the case of options and that + raises an exception if the file cannot be read. + """ + + def optionxform(self, optionstr): + return optionstr + + def read(self, filenames): + if isinstance(filenames, text_or_bytes): + filenames = [filenames] + for filename in filenames: + # try: + # fp = open(filename) + # except IOError: + # continue + fp = open(filename) + try: + self._read(fp, filename) + finally: + fp.close() + + def as_dict(self, raw=False, vars=None): + """Convert an INI file to a dictionary""" + # Load INI file into a dict + result = {} + for section in self.sections(): + if section not in result: + result[section] = {} + for option in self.options(section): + value = self.get(section, option, raw=raw, vars=vars) + try: + value = unrepr(value) + except Exception: + x = sys.exc_info()[1] + msg = ('Config error in section: %r, option: %r, ' + 'value: %r. Config values must be valid Python.' % + (section, option, value)) + raise ValueError(msg, x.__class__.__name__, x.args) + result[section][option] = value + return result + + def dict_from_file(self, file): + if hasattr(file, 'read'): + self.readfp(file) + else: + self.read(file) + return self.as_dict() + + @classmethod + def load(self, input): + """Resolve 'input' to dict from a dict, file, or filename.""" + is_file = ( + # Filename + isinstance(input, text_or_bytes) + # Open file object + or hasattr(input, 'read') + ) + return Parser().dict_from_file(input) if is_file else input.copy() + + +# public domain "unrepr" implementation, found on the web and then improved. + + +class _Builder2: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError('unrepr does not recognize %s' % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python2 ast Node compiled from a string.""" + try: + import compiler + except ImportError: + # Fallback to eval when compiler package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = compiler.parse('__tempvalue__ = ' + s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + + def build_Subscript(self, o): + expr, flags, subs = o.getChildren() + expr = self.build(expr) + subs = self.build(subs) + return expr[subs] + + def build_CallFunc(self, o): + children = o.getChildren() + # Build callee from first child + callee = self.build(children[0]) + # Build args and kwargs from remaining children + args = [] + kwargs = {} + for child in children[1:]: + class_name = child.__class__.__name__ + # None is ignored + if class_name == 'NoneType': + continue + # Keywords become kwargs + if class_name == 'Keyword': + kwargs.update(self.build(child)) + # Everything else becomes args + else: + args.append(self.build(child)) + + return callee(*args, **kwargs) + + def build_Keyword(self, o): + key, value_obj = o.getChildren() + value = self.build(value_obj) + kw_dict = {key: value} + return kw_dict + + def build_List(self, o): + return map(self.build, o.getChildren()) + + def build_Const(self, o): + return o.value + + def build_Dict(self, o): + d = {} + i = iter(map(self.build, o.getChildren())) + for el in i: + d[el] = i.next() + return d + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.name + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError('unrepr could not resolve the name %s' % repr(name)) + + def build_Add(self, o): + left, right = map(self.build, o.getChildren()) + return left + right + + def build_Mul(self, o): + left, right = map(self.build, o.getChildren()) + return left * right + + def build_Getattr(self, o): + parent = self.build(o.expr) + return getattr(parent, o.attrname) + + def build_NoneType(self, o): + return None + + def build_UnarySub(self, o): + return -self.build(o.getChildren()[0]) + + def build_UnaryAdd(self, o): + return self.build(o.getChildren()[0]) + + +class _Builder3: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError('unrepr does not recognize %s' % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python3 ast Node compiled from a string.""" + try: + import ast + except ImportError: + # Fallback to eval when ast package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = ast.parse('__tempvalue__ = ' + s) + return p.body[0].value + + def build_Subscript(self, o): + return self.build(o.value)[self.build(o.slice)] + + def build_Index(self, o): + return self.build(o.value) + + def _build_call35(self, o): + """ + Workaround for python 3.5 _ast.Call signature, docs found here + https://greentreesnakes.readthedocs.org/en/latest/nodes.html + """ + import ast + callee = self.build(o.func) + args = [] + if o.args is not None: + for a in o.args: + if isinstance(a, ast.Starred): + args.append(self.build(a.value)) + else: + args.append(self.build(a)) + kwargs = {} + for kw in o.keywords: + if kw.arg is None: # double asterix `**` + rst = self.build(kw.value) + if not isinstance(rst, dict): + raise TypeError('Invalid argument for call.' + 'Must be a mapping object.') + # give preference to the keys set directly from arg=value + for k, v in rst.items(): + if k not in kwargs: + kwargs[k] = v + else: # defined on the call as: arg=value + kwargs[kw.arg] = self.build(kw.value) + return callee(*args, **kwargs) + + def build_Call(self, o): + if sys.version_info >= (3, 5): + return self._build_call35(o) + + callee = self.build(o.func) + + if o.args is None: + args = () + else: + args = tuple([self.build(a) for a in o.args]) + + if o.starargs is None: + starargs = () + else: + starargs = tuple(self.build(o.starargs)) + + if o.kwargs is None: + kwargs = {} + else: + kwargs = self.build(o.kwargs) + if o.keywords is not None: # direct a=b keywords + for kw in o.keywords: + # preference because is a direct keyword against **kwargs + kwargs[kw.arg] = self.build(kw.value) + return callee(*(args + starargs), **kwargs) + + def build_List(self, o): + return list(map(self.build, o.elts)) + + def build_Str(self, o): + return o.s + + def build_Num(self, o): + return o.n + + def build_Dict(self, o): + return dict([(self.build(k), self.build(v)) + for k, v in zip(o.keys, o.values)]) + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.id + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + import builtins + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError('unrepr could not resolve the name %s' % repr(name)) + + def build_NameConstant(self, o): + return o.value + + def build_UnaryOp(self, o): + op, operand = map(self.build, [o.op, o.operand]) + return op(operand) + + def build_BinOp(self, o): + left, op, right = map(self.build, [o.left, o.op, o.right]) + return op(left, right) + + def build_Add(self, o): + return operator.add + + def build_Mult(self, o): + return operator.mul + + def build_USub(self, o): + return operator.neg + + def build_Attribute(self, o): + parent = self.build(o.value) + return getattr(parent, o.attr) + + def build_NoneType(self, o): + return None + + +def unrepr(s): + """Return a Python object compiled from a string.""" + if not s: + return s + if sys.version_info < (3, 0): + b = _Builder2() + else: + b = _Builder3() + obj = b.astnode(s) + return b.build(obj) + + +def modules(modulePath): + """Load a module and retrieve a reference to that module.""" + __import__(modulePath) + return sys.modules[modulePath] + + +def attributes(full_attribute_name): + """Load a module and retrieve an attribute of that module.""" + + # Parse out the path, module, and attribute + last_dot = full_attribute_name.rfind('.') + attr_name = full_attribute_name[last_dot + 1:] + mod_path = full_attribute_name[:last_dot] + + mod = modules(mod_path) + # Let an AttributeError propagate outward. + try: + attr = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + # Return a reference to the attribute. + return attr diff --git a/libraries/cherrypy/lib/sessions.py b/libraries/cherrypy/lib/sessions.py new file mode 100644 index 00000000..5b49ee13 --- /dev/null +++ b/libraries/cherrypy/lib/sessions.py @@ -0,0 +1,919 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_class = cherrypy.lib.sessions.FileSession + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory +/home/site/sessions, and the session timeout to 60 minutes. If you omit +``storage_class``, the sessions will be saved in RAM. +``tools.sessions.on`` is the only required line for working sessions, +the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. Be mindful of this default mode +for any requests that take a long time to process (streaming responses, +expensive calculations, database lookups, API calls, etc), as other concurrent +requests that also utilize sessions will hang until the session is unlocked. + +If you want to control when the session data is locked and unlocked, +set ``tools.sessions.locking = 'explicit'``. Then call +``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_. +However, CherryPy "recognizes" a session id by looking up the saved session +data for that id. Therefore, if you never save any session data, +**you will get a new session id for every request**. + +A side effect of CherryPy overwriting unrecognised session ids is that if you +have multiple, separate CherryPy applications running on a single domain (e.g. +on different ports), each app will overwrite the other's session id because by +default they use the same cookie name (``"session_id"``) but do not recognise +each others sessions. It is therefore a good idea to use a different name for +each, for example:: + + [/] + ... + tools.sessions.name = "my_app_session_id" + +================ +Sharing Sessions +================ + +If you run multiple instances of CherryPy (for example via mod_python behind +Apache prefork), you most likely cannot use the RAM session backend, since each +instance of CherryPy will have its own memory space. Use a different backend +instead, and verify that all instances are pointing at the same file or db +location. Alternately, you might try a load balancer which makes sessions +"sticky". Google is your friend, there. + +================ +Expiration Dates +================ + +The response cookie will possess an expiration date to inform the client at +which point to stop sending the cookie back in requests. If the server time +and client time differ, expect sessions to be unreliable. **Make sure the +system time of your server is accurate**. + +CherryPy defaults to a 60-minute session timeout, which also applies to the +cookie which is sent to the client. Unfortunately, some versions of Safari +("4 public beta" on Windows XP at least) appear to have a bug in their parsing +of the GMT expiration date--they appear to interpret the date as one hour in +the past. Sixty minutes minus one hour is pretty close to zero, so you may +experience this bug as a new session id for every request, unless the requests +are less than one second apart. To fix, try increasing the session.timeout. + +On the other extreme, some users report Firefox sending cookies after their +expiration date, although this was on a system with an inaccurate system time. +Maybe FF doesn't trust system time. +""" +import sys +import datetime +import os +import time +import threading +import binascii + +import six +from six.moves import cPickle as pickle +import contextlib2 + +import zc.lockfile + +import cherrypy +from cherrypy.lib import httputil +from cherrypy.lib import locking +from cherrypy.lib import is_iterator + + +if six.PY2: + FileNotFoundError = OSError + + +missing = object() + + +class Session(object): + + """A CherryPy dict-like Session object (one per request).""" + + _id = None + + id_observers = None + "A list of callbacks to which to pass new id's." + + @property + def id(self): + """Return the current session id.""" + return self._id + + @id.setter + def id(self, value): + self._id = value + for o in self.id_observers: + o(value) + + timeout = 60 + 'Number of minutes after which to delete session data.' + + locked = False + """ + If True, this session instance has exclusive read/write access + to session data.""" + + loaded = False + """ + If True, data has been retrieved from storage. This should happen + automatically on the first attempt to access session data.""" + + clean_thread = None + 'Class-level Monitor which calls self.clean_up.' + + clean_freq = 5 + 'The poll rate for expired session cleanup in minutes.' + + originalid = None + 'The session id passed by the client. May be missing or unsafe.' + + missing = False + 'True if the session requested by the client did not exist.' + + regenerated = False + """ + True if the application called session.regenerate(). This is not set by + internal calls to regenerate the session id.""" + + debug = False + 'If True, log debug information.' + + # --------------------- Session management methods --------------------- # + + def __init__(self, id=None, **kwargs): + self.id_observers = [] + self._data = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + self.originalid = id + self.missing = False + if id is None: + if self.debug: + cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') + self._regenerate() + else: + self.id = id + if self._exists(): + if self.debug: + cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS') + else: + if self.debug: + cherrypy.log('Expired or malicious session %r; ' + 'making a new one' % id, 'TOOLS.SESSIONS') + # Expired or malicious session. Make a new one. + # See https://github.com/cherrypy/cherrypy/issues/709. + self.id = None + self.missing = True + self._regenerate() + + def now(self): + """Generate the session specific concept of 'now'. + + Other session providers can override this to use alternative, + possibly timezone aware, versions of 'now'. + """ + return datetime.datetime.now() + + def regenerate(self): + """Replace the current session (with a new id).""" + self.regenerated = True + self._regenerate() + + def _regenerate(self): + if self.id is not None: + if self.debug: + cherrypy.log( + 'Deleting the existing session %r before ' + 'regeneration.' % self.id, + 'TOOLS.SESSIONS') + self.delete() + + old_session_was_locked = self.locked + if old_session_was_locked: + self.release_lock() + if self.debug: + cherrypy.log('Old lock released.', 'TOOLS.SESSIONS') + + self.id = None + while self.id is None: + self.id = self.generate_id() + # Assert that the generated id is not already stored. + if self._exists(): + self.id = None + if self.debug: + cherrypy.log('Set id to generated %s.' % self.id, + 'TOOLS.SESSIONS') + + if old_session_was_locked: + self.acquire_lock() + if self.debug: + cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS') + + def clean_up(self): + """Clean up expired sessions.""" + pass + + def generate_id(self): + """Return a new session id.""" + return binascii.hexlify(os.urandom(20)).decode('ascii') + + def save(self): + """Save session data.""" + try: + # If session data has never been loaded then it's never been + # accessed: no need to save it + if self.loaded: + t = datetime.timedelta(seconds=self.timeout * 60) + expiration_time = self.now() + t + if self.debug: + cherrypy.log('Saving session %r with expiry %s' % + (self.id, expiration_time), + 'TOOLS.SESSIONS') + self._save(expiration_time) + else: + if self.debug: + cherrypy.log( + 'Skipping save of session %r (no session loaded).' % + self.id, 'TOOLS.SESSIONS') + finally: + if self.locked: + # Always release the lock if the user didn't release it + self.release_lock() + if self.debug: + cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS') + + def load(self): + """Copy stored session data into this session instance.""" + data = self._load() + # data is either None or a tuple (session_data, expiration_time) + if data is None or data[1] < self.now(): + if self.debug: + cherrypy.log('Expired session %r, flushing data.' % self.id, + 'TOOLS.SESSIONS') + self._data = {} + else: + if self.debug: + cherrypy.log('Data loaded for session %r.' % self.id, + 'TOOLS.SESSIONS') + self._data = data[0] + self.loaded = True + + # Stick the clean_thread in the class, not the instance. + # The instances are created and destroyed per-request. + cls = self.__class__ + if self.clean_freq and not cls.clean_thread: + # clean_up is an instancemethod and not a classmethod, + # so that tool config can be accessed inside the method. + t = cherrypy.process.plugins.Monitor( + cherrypy.engine, self.clean_up, self.clean_freq * 60, + name='Session cleanup') + t.subscribe() + cls.clean_thread = t + t.start() + if self.debug: + cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS') + + def delete(self): + """Delete stored session data.""" + self._delete() + if self.debug: + cherrypy.log('Deleted session %s.' % self.id, + 'TOOLS.SESSIONS') + + # -------------------- Application accessor methods -------------------- # + + def __getitem__(self, key): + if not self.loaded: + self.load() + return self._data[key] + + def __setitem__(self, key, value): + if not self.loaded: + self.load() + self._data[key] = value + + def __delitem__(self, key): + if not self.loaded: + self.load() + del self._data[key] + + def pop(self, key, default=missing): + """Remove the specified key and return the corresponding value. + If key is not found, default is returned if given, + otherwise KeyError is raised. + """ + if not self.loaded: + self.load() + if default is missing: + return self._data.pop(key) + else: + return self._data.pop(key, default) + + def __contains__(self, key): + if not self.loaded: + self.load() + return key in self._data + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + if not self.loaded: + self.load() + return self._data.get(key, default) + + def update(self, d): + """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" + if not self.loaded: + self.load() + self._data.update(d) + + def setdefault(self, key, default=None): + """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" + if not self.loaded: + self.load() + return self._data.setdefault(key, default) + + def clear(self): + """D.clear() -> None. Remove all items from D.""" + if not self.loaded: + self.load() + self._data.clear() + + def keys(self): + """D.keys() -> list of D's keys.""" + if not self.loaded: + self.load() + return self._data.keys() + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" + if not self.loaded: + self.load() + return self._data.items() + + def values(self): + """D.values() -> list of D's values.""" + if not self.loaded: + self.load() + return self._data.values() + + +class RamSession(Session): + + # Class-level objects. Don't rebind these! + cache = {} + locks = {} + + def clean_up(self): + """Clean up expired sessions.""" + + now = self.now() + for _id, (data, expiration_time) in list(six.iteritems(self.cache)): + if expiration_time <= now: + try: + del self.cache[_id] + except KeyError: + pass + try: + if self.locks[_id].acquire(blocking=False): + lock = self.locks.pop(_id) + lock.release() + except KeyError: + pass + + # added to remove obsolete lock objects + for _id in list(self.locks): + locked = ( + _id not in self.cache + and self.locks[_id].acquire(blocking=False) + ) + if locked: + lock = self.locks.pop(_id) + lock.release() + + def _exists(self): + return self.id in self.cache + + def _load(self): + return self.cache.get(self.id) + + def _save(self, expiration_time): + self.cache[self.id] = (self._data, expiration_time) + + def _delete(self): + self.cache.pop(self.id, None) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + return len(self.cache) + + +class FileSession(Session): + + """Implementation of the File backend for sessions + + storage_path + The folder where session data will be saved. Each session + will be saved as pickle.dump(data, expiration_time) in its own file; + the filename will be self.SESSION_PREFIX + self.id. + + lock_timeout + A timedelta or numeric seconds indicating how long + to block acquiring a lock. If None (default), acquiring a lock + will block indefinitely. + """ + + SESSION_PREFIX = 'session-' + LOCK_SUFFIX = '.lock' + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + kwargs.setdefault('lock_timeout', None) + + Session.__init__(self, id=id, **kwargs) + + # validate self.lock_timeout + if isinstance(self.lock_timeout, (int, float)): + self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) + if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): + raise ValueError( + 'Lock timeout must be numeric seconds or a timedelta instance.' + ) + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for file-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + + for k, v in kwargs.items(): + setattr(cls, k, v) + + def _get_file_path(self): + f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) + if not os.path.abspath(f).startswith(self.storage_path): + raise cherrypy.HTTPError(400, 'Invalid session id in cookie.') + return f + + def _exists(self): + path = self._get_file_path() + return os.path.exists(path) + + def _load(self, path=None): + assert self.locked, ('The session load without being locked. ' + "Check your tools' priority levels.") + if path is None: + path = self._get_file_path() + try: + f = open(path, 'rb') + try: + return pickle.load(f) + finally: + f.close() + except (IOError, EOFError): + e = sys.exc_info()[1] + if self.debug: + cherrypy.log('Error loading the session pickle: %s' % + e, 'TOOLS.SESSIONS') + return 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: + 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. ' + "Check your tools' priority levels.") + try: + os.unlink(self._get_file_path()) + except OSError: + pass + + def acquire_lock(self, path=None): + """Acquire an exclusive lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + path += self.LOCK_SUFFIX + checker = locking.LockChecker(self.id, self.lock_timeout) + while not checker.expired(): + try: + self.lock = zc.lockfile.LockFile(path) + except zc.lockfile.LockError: + time.sleep(0.1) + else: + break + self.locked = True + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self, path=None): + """Release the lock on the currently-loaded session data.""" + self.lock.close() + with contextlib2.suppress(FileNotFoundError): + os.remove(self.lock._path) + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + # Iterate over all session files in self.storage_path + for fname in os.listdir(self.storage_path): + have_session = ( + fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX) + ) + if have_session: + # We have a session file: lock and load it and check + # if it's expired. If it fails, nevermind. + path = os.path.join(self.storage_path, fname) + self.acquire_lock(path) + if self.debug: + # This is a bit of a hack, since we're calling clean_up + # on the first instance rather than the entire class, + # so depending on whether you have "debug" set on the + # path of the first session called, this may not run. + cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS') + + try: + contents = self._load(path) + # _load returns None on IOError + if contents is not None: + data, expiration_time = contents + if expiration_time < now: + # Session expired: deleting it + os.unlink(path) + finally: + self.release_lock(path) + + def __len__(self): + """Return the number of active sessions.""" + return len([fname for fname in os.listdir(self.storage_path) + if (fname.startswith(self.SESSION_PREFIX) and + not fname.endswith(self.LOCK_SUFFIX))]) + + +class MemcachedSession(Session): + + # The most popular memcached client for Python isn't thread-safe. + # Wrap all .get and .set operations in a single lock. + mc_lock = threading.RLock() + + # This is a separate set of locks per session id. + locks = {} + + servers = ['127.0.0.1:11211'] + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for memcached-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + import memcache + cls.cache = memcache.Client(cls.servers) + + def _exists(self): + self.mc_lock.acquire() + try: + return bool(self.cache.get(self.id)) + finally: + self.mc_lock.release() + + def _load(self): + self.mc_lock.acquire() + try: + return self.cache.get(self.id) + finally: + self.mc_lock.release() + + def _save(self, expiration_time): + # Send the expiration time as "Unix time" (seconds since 1/1/1970) + td = int(time.mktime(expiration_time.timetuple())) + self.mc_lock.acquire() + try: + if not self.cache.set(self.id, (self._data, expiration_time), td): + raise AssertionError( + 'Session data for id %r not set.' % self.id) + finally: + self.mc_lock.release() + + def _delete(self): + self.cache.delete(self.id) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + raise NotImplementedError + + +# Hook functions (for CherryPy tools) + +def save(): + """Save any changed session data.""" + + if not hasattr(cherrypy.serving, 'session'): + return + request = cherrypy.serving.request + response = cherrypy.serving.response + + # Guard against running twice + if hasattr(request, '_sessionsaved'): + return + request._sessionsaved = True + + if response.stream: + # If the body is being streamed, we have to save the data + # *after* the response has been written out + request.hooks.attach('on_end_request', cherrypy.session.save) + else: + # If the body is not being streamed, we save the data now + # (so we can release the lock). + if is_iterator(response.body): + response.collapse_body() + cherrypy.session.save() + + +save.failsafe = True + + +def close(): + """Close the session object for this request.""" + sess = getattr(cherrypy.serving, 'session', None) + if getattr(sess, 'locked', False): + # If the session is still locked we release the lock + sess.release_lock() + if sess.debug: + cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') + + +close.failsafe = True +close.priority = 90 + + +def init(storage_type=None, path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, clean_freq=5, + persistent=True, httponly=False, debug=False, + # Py27 compat + # *, storage_class=RamSession, + **kwargs): + """Initialize session object (using cookies). + + storage_class + The Session subclass to use. Defaults to RamSession. + + storage_type + (deprecated) + One of 'ram', 'file', memcached'. This will be + used to look up the corresponding class in cherrypy.lib.sessions + globals. For example, 'file' will use the FileSession class. + + path + The 'path' value to stick in the response cookie metadata. + + path_header + If 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + The name of the cookie. + + timeout + The expiration timeout (in minutes) for the stored session data. + If 'persistent' is True (the default), this is also the timeout + for the cookie. + + domain + The cookie domain. + + secure + If False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + clean_freq (minutes) + The poll rate for expired session cleanup. + + persistent + If True (the default), the 'timeout' argument will be used + to expire the cookie. If False, the cookie will not have an expiry, + and the cookie will be a "session cookie" which expires when the + browser is closed. + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + Any additional kwargs will be bound to the new Session instance, + and may be specific to the storage type. See the subclass of Session + you're using for more information. + """ + + # Py27 compat + storage_class = kwargs.pop('storage_class', RamSession) + + request = cherrypy.serving.request + + # Guard against running twice + if hasattr(request, '_session_init_flag'): + return + request._session_init_flag = True + + # Check if request came with a session ID + id = None + if name in request.cookie: + id = request.cookie[name].value + if debug: + cherrypy.log('ID obtained from request.cookie: %r' % id, + 'TOOLS.SESSIONS') + + first_time = not hasattr(cherrypy, 'session') + + if storage_type: + if first_time: + msg = 'storage_type is deprecated. Supply storage_class instead' + cherrypy.log(msg) + storage_class = storage_type.title() + 'Session' + storage_class = globals()[storage_class] + + # call setup first time only + if first_time: + if hasattr(storage_class, 'setup'): + storage_class.setup(**kwargs) + + # Create and attach a new Session instance to cherrypy.serving. + # It will possess a reference to (and lock, and lazily load) + # the requested session data. + kwargs['timeout'] = timeout + kwargs['clean_freq'] = clean_freq + cherrypy.serving.session = sess = storage_class(id, **kwargs) + sess.debug = debug + + def update_cookie(id): + """Update the cookie every time the session id changes.""" + cherrypy.serving.response.cookie[name] = id + sess.id_observers.append(update_cookie) + + # Create cherrypy.session which will proxy to cherrypy.serving.session + if not hasattr(cherrypy, 'session'): + cherrypy.session = cherrypy._ThreadLocalProxy('session') + + if persistent: + cookie_timeout = timeout + else: + # See http://support.microsoft.com/kb/223799/EN-US/ + # and http://support.mozilla.com/en-US/kb/Cookies + cookie_timeout = None + set_response_cookie(path=path, path_header=path_header, name=name, + timeout=cookie_timeout, domain=domain, secure=secure, + httponly=httponly) + + +def set_response_cookie(path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, httponly=False): + """Set a response cookie for the client. + + path + the 'path' value to stick in the response cookie metadata. + + path_header + if 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + the name of the cookie. + + timeout + the expiration timeout for the cookie. If 0 or other boolean + False, no 'expires' param will be set, and the cookie will be a + "session cookie" which expires when the browser is closed. + + domain + the cookie domain. + + secure + if False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + """ + # Set response cookie + cookie = cherrypy.serving.response.cookie + cookie[name] = cherrypy.serving.session.id + cookie[name]['path'] = ( + path or + cherrypy.serving.request.headers.get(path_header) or + '/' + ) + + if timeout: + cookie[name]['max-age'] = timeout * 60 + _add_MSIE_max_age_workaround(cookie[name], timeout) + if domain is not None: + cookie[name]['domain'] = domain + if secure: + cookie[name]['secure'] = 1 + if httponly: + if not cookie[name].isReservedKey('httponly'): + raise ValueError('The httponly cookie token is not supported.') + cookie[name]['httponly'] = 1 + + +def _add_MSIE_max_age_workaround(cookie, timeout): + """ + We'd like to use the "max-age" param as indicated in + http://www.faqs.org/rfcs/rfc2109.html but IE doesn't + save it to disk and the session is lost if people close + the browser. So we have to use the old "expires" ... sigh ... + """ + expires = time.time() + timeout * 60 + cookie['expires'] = httputil.HTTPDate(expires) + + +def expire(): + """Expire the current session cookie.""" + name = cherrypy.serving.request.config.get( + 'tools.sessions.name', 'session_id') + one_year = 60 * 60 * 24 * 365 + e = time.time() - one_year + cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) + cherrypy.serving.response.cookie[name].pop('max-age', None) diff --git a/libraries/cherrypy/lib/static.py b/libraries/cherrypy/lib/static.py new file mode 100644 index 00000000..da9d9373 --- /dev/null +++ b/libraries/cherrypy/lib/static.py @@ -0,0 +1,390 @@ +"""Module with helpers for serving static files.""" + +import os +import platform +import re +import stat +import mimetypes + +from email.generator import _make_boundary as make_boundary +from io import UnsupportedOperation + +from six.moves import urllib + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import cptools, httputil, file_generator_limited + + +def _setup_mimetypes(): + """Pre-initialize global mimetype map.""" + if not mimetypes.inited: + mimetypes.init() + mimetypes.types_map['.dwg'] = 'image/x-dwg' + mimetypes.types_map['.ico'] = 'image/x-icon' + mimetypes.types_map['.bz2'] = 'application/x-bzip2' + mimetypes.types_map['.gz'] = 'application/x-gzip' + + +_setup_mimetypes() + + +def serve_file(path, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "<disposition>; filename=<name>". If name is None, it will be set + to the basename of path. If disposition is None, no Content-Disposition + header will be written. + """ + response = cherrypy.serving.response + + # If path is relative, users should fix it by making path absolute. + # That is, CherryPy should not guess where the application root is. + # It certainly should *not* use cwd (since CP may be invoked from a + # variety of paths). If using tools.staticdir, you can make your relative + # paths become absolute by supplying a value for "tools.staticdir.root". + if not os.path.isabs(path): + msg = "'%s' is not an absolute path." % path + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + + try: + st = os.stat(path) + except (OSError, TypeError, ValueError): + # OSError when file fails to stat + # TypeError on Python 2 when there's a null byte + # ValueError on Python 3 when there's a null byte + if debug: + cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Check if path is a directory. + if stat.S_ISDIR(st.st_mode): + # Let the caller deal with it as they like. + if debug: + cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + + if content_type is None: + # Set content-type based on filename extension + ext = '' + i = path.rfind('.') + if i != -1: + ext = path[i:].lower() + content_type = mimetypes.types_map.get(ext, None) + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + name = os.path.basename(path) + cd = '%s; filename="%s"' % (disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + content_length = st.st_size + fileobj = open(path, 'rb') + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given file object. + + The Content-Type header will be set to the content_type arg, if provided. + + If disposition is not None, the Content-Disposition header will be set + to "<disposition>; filename=<name>". If name is None, 'filename' will + not be set. If disposition is None, no Content-Disposition header will + be written. + + CAUTION: If the request contains a 'Range' header, one or more seek()s will + be performed on the file object. This may cause undesired behavior if + the file object is not seekable. It could also produce undesired results + if the caller set the read position of the file object prior to calling + serve_fileobj(), expecting that the data would be served starting from that + position. + """ + response = cherrypy.serving.response + + try: + st = os.fstat(fileobj.fileno()) + except AttributeError: + if debug: + cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') + content_length = None + except UnsupportedOperation: + content_length = None + else: + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + content_length = st.st_size + + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + cd = disposition + else: + cd = '%s; filename="%s"' % (disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def _serve_fileobj(fileobj, content_type, content_length, debug=False): + """Internal. Set response.body to the given file object, perhaps ranged.""" + response = cherrypy.serving.response + + # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code + request = cherrypy.serving.request + if request.protocol >= (1, 1): + response.headers['Accept-Ranges'] = 'bytes' + r = httputil.get_ranges(request.headers.get('Range'), content_length) + if r == []: + response.headers['Content-Range'] = 'bytes */%s' % content_length + message = ('Invalid Range (first-byte-pos greater than ' + 'Content-Length)') + if debug: + cherrypy.log(message, 'TOOLS.STATIC') + raise cherrypy.HTTPError(416, message) + + if r: + if len(r) == 1: + # Return a single-part response. + start, stop = r[0] + if stop > content_length: + stop = content_length + r_len = stop - start + if debug: + cherrypy.log( + 'Single part; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + response.status = '206 Partial Content' + response.headers['Content-Range'] = ( + 'bytes %s-%s/%s' % (start, stop - 1, content_length)) + response.headers['Content-Length'] = r_len + fileobj.seek(start) + response.body = file_generator_limited(fileobj, r_len) + else: + # Return a multipart/byteranges response. + response.status = '206 Partial Content' + boundary = make_boundary() + ct = 'multipart/byteranges; boundary=%s' % boundary + response.headers['Content-Type'] = ct + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + def file_ranges(): + # Apache compatibility: + yield b'\r\n' + + for start, stop in r: + if debug: + cherrypy.log( + 'Multipart; start: %r, stop: %r' % ( + start, stop), + 'TOOLS.STATIC') + yield ntob('--' + boundary, 'ascii') + yield ntob('\r\nContent-type: %s' % content_type, + 'ascii') + yield ntob( + '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % ( + start, stop - 1, content_length), + 'ascii') + fileobj.seek(start) + gen = file_generator_limited(fileobj, stop - start) + for chunk in gen: + yield chunk + yield b'\r\n' + # Final boundary + yield ntob('--' + boundary + '--', 'ascii') + + # Apache compatibility: + yield b'\r\n' + response.body = file_ranges() + return response.body + else: + if debug: + cherrypy.log('No byteranges requested', 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + response.headers['Content-Length'] = content_length + response.body = fileobj + return response.body + + +def serve_download(path, name=None): + """Serve 'path' as an application/x-download attachment.""" + # This is such a common idiom I felt it deserved its own wrapper. + return serve_file(path, 'application/x-download', 'attachment', name) + + +def _attempt(filename, content_types, debug=False): + if debug: + cherrypy.log('Attempting %r (content_types %r)' % + (filename, content_types), 'TOOLS.STATICDIR') + try: + # you can set the content types for a + # complete directory per extension + content_type = None + if content_types: + r, ext = os.path.splitext(filename) + content_type = content_types.get(ext[1:], None) + serve_file(filename, content_type=content_type, debug=debug) + return True + except cherrypy.NotFound: + # If we didn't find the static file, continue handling the + # request. We might find a dynamic handler instead. + if debug: + cherrypy.log('NotFound', 'TOOLS.STATICFILE') + return False + + +def staticdir(section, dir, root='', match='', content_types=None, index='', + debug=False): + """Serve a static resource from the given (root +) dir. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + index + If provided, it should be the (relative) name of a file to + serve for directory requests. For example, if the dir argument is + '/home/me', the Request-URI is 'myapp', and the index arg is + 'index.html', the file '/home/me/myapp/index.html' will be sought. + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICDIR') + return False + + # Allow the use of '~' to refer to a user's home directory. + dir = os.path.expanduser(dir) + + # If dir is relative, make absolute using "root". + if not os.path.isabs(dir): + if not root: + msg = 'Static dir requires an absolute dir (or root).' + if debug: + cherrypy.log(msg, 'TOOLS.STATICDIR') + raise ValueError(msg) + dir = os.path.join(root, dir) + + # Determine where we are in the object tree relative to 'section' + # (where the static tool was defined). + if section == 'global': + section = '/' + section = section.rstrip(r'\/') + branch = request.path_info[len(section) + 1:] + branch = urllib.parse.unquote(branch.lstrip(r'\/')) + + # Requesting a file in sub-dir of the staticdir results + # in mixing of delimiter styles, e.g. C:\static\js/script.js. + # Windows accepts this form except not when the path is + # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js. + # http://bit.ly/1vdioCX + if platform.system() == 'Windows': + branch = branch.replace('/', '\\') + + # If branch is "", filename will end in a slash + filename = os.path.join(dir, branch) + if debug: + cherrypy.log('Checking file %r to fulfill %r' % + (filename, request.path_info), 'TOOLS.STATICDIR') + + # There's a chance that the branch pulled from the URL might + # have ".." or similar uplevel attacks in it. Check that the final + # filename is a child of dir. + if not os.path.normpath(filename).startswith(os.path.normpath(dir)): + raise cherrypy.HTTPError(403) # Forbidden + + handled = _attempt(filename, content_types) + if not handled: + # Check for an index file if a folder was requested. + if index: + handled = _attempt(os.path.join(filename, index), content_types) + if handled: + request.is_index = filename[-1] in (r'\/') + return handled + + +def staticfile(filename, root=None, match='', content_types=None, debug=False): + """Serve a static resource from the given (root +) filename. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICFILE') + return False + + # If filename is relative, make absolute using "root". + if not os.path.isabs(filename): + if not root: + msg = "Static tool requires an absolute filename (got '%s')." % ( + filename,) + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + filename = os.path.join(root, filename) + + return _attempt(filename, content_types, debug=debug) diff --git a/libraries/cherrypy/lib/xmlrpcutil.py b/libraries/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 00000000..ddaac86a --- /dev/null +++ b/libraries/cherrypy/lib/xmlrpcutil.py @@ -0,0 +1,61 @@ +"""XML-RPC tool helpers.""" +import sys + +from six.moves.xmlrpc_client import ( + loads as xmlrpc_loads, dumps as xmlrpc_dumps, + Fault as XMLRPCFault +) + +import cherrypy +from cherrypy._cpcompat import ntob + + +def process_body(): + """Return (params, method) from request body.""" + try: + return xmlrpc_loads(cherrypy.request.body.read()) + except Exception: + return ('ERROR PARAMS', ), 'ERRORMETHOD' + + +def patched_path(path): + """Return 'path', doctored for RPC.""" + if not path.endswith('/'): + path += '/' + if path.startswith('/RPC2/'): + # strip the first /rpc2 + path = path[5:] + return path + + +def _set_response(body): + """Set up HTTP status, headers and body within CherryPy.""" + # The XML-RPC spec (http://www.xmlrpc.com/spec) says: + # "Unless there's a lower-level error, always return 200 OK." + # Since Python's xmlrpc_client interprets a non-200 response + # as a "Protocol Error", we'll just return 200 every time. + response = cherrypy.response + response.status = '200 OK' + response.body = ntob(body, 'utf-8') + response.headers['Content-Type'] = 'text/xml' + response.headers['Content-Length'] = len(body) + + +def respond(body, encoding='utf-8', allow_none=0): + """Construct HTTP response body.""" + if not isinstance(body, XMLRPCFault): + body = (body,) + + _set_response( + xmlrpc_dumps( + body, methodresponse=1, + encoding=encoding, + allow_none=allow_none + ) + ) + + +def on_error(*args, **kwargs): + """Construct HTTP response body for an error response.""" + body = str(sys.exc_info()[1]) + _set_response(xmlrpc_dumps(XMLRPCFault(1, body))) diff --git a/libraries/cherrypy/process/__init__.py b/libraries/cherrypy/process/__init__.py new file mode 100644 index 00000000..f242d226 --- /dev/null +++ b/libraries/cherrypy/process/__init__.py @@ -0,0 +1,17 @@ +"""Site container for an HTTP server. + +A Web Site Process Bus object is used to connect applications, servers, +and frameworks with site-wide services such as daemonization, process +reload, signal handling, drop privileges, PID file management, logging +for all of these, and many more. + +The 'plugins' module defines a few abstract and concrete services for +use with the bus. Some use tool-specific channels; see the documentation +for each class. +""" + +from .wspbus import bus +from . import plugins, servers + + +__all__ = ('bus', 'plugins', 'servers') diff --git a/libraries/cherrypy/process/plugins.py b/libraries/cherrypy/process/plugins.py new file mode 100644 index 00000000..8c246c81 --- /dev/null +++ b/libraries/cherrypy/process/plugins.py @@ -0,0 +1,752 @@ +"""Site services for use with a Web Site Process Bus.""" + +import os +import re +import signal as _signal +import sys +import time +import threading + +from six.moves import _thread + +from cherrypy._cpcompat import text_or_bytes +from cherrypy._cpcompat import ntob, Timer + +# _module__file__base is used by Autoreload to make +# absolute any filenames retrieved from sys.modules which are not +# already absolute paths. This is to work around Python's quirk +# of importing the startup script and using a relative filename +# for it in sys.modules. +# +# Autoreload examines sys.modules afresh every time it runs. If an application +# changes the current directory by executing os.chdir(), then the next time +# Autoreload runs, it will not be able to find any filenames which are +# not absolute paths, because the current directory is not the same as when the +# module was first imported. Autoreload will then wrongly conclude the file +# has "changed", and initiate the shutdown/re-exec sequence. +# See ticket #917. +# For this workaround to have a decent probability of success, this module +# needs to be imported as early as possible, before the app has much chance +# to change the working directory. +_module__file__base = os.getcwd() + + +class SimplePlugin(object): + + """Plugin base class which auto-subscribes methods for known channels.""" + + bus = None + """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Register this object as a (multi-channel) listener on the bus.""" + for channel in self.bus.listeners: + # Subscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.subscribe(channel, method) + + def unsubscribe(self): + """Unregister this object as a listener on the bus.""" + for channel in self.bus.listeners: + # Unsubscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.unsubscribe(channel, method) + + +class SignalHandler(object): + + """Register bus channels (and listeners) for system signals. + + You can modify what signals your application listens for, and what it does + when it receives signals, by modifying :attr:`SignalHandler.handlers`, + a dict of {signal name: callback} pairs. The default set is:: + + handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + The :func:`SignalHandler.handle_SIGHUP`` method calls + :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>` + if the process is daemonized, but + :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>` + if the process is attached to a TTY. This is because Unix window + managers tend to send SIGHUP to terminal windows when the user closes them. + + Feel free to add signals which are not available on every platform. + The :class:`SignalHandler` will ignore errors raised from attempting + to register handlers for unknown signals. + """ + + handlers = {} + """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" + + signals = {} + """A map from signal numbers to names.""" + + for k, v in vars(_signal).items(): + if k.startswith('SIG') and not k.startswith('SIG_'): + signals[v] = k + del k, v + + def __init__(self, bus): + self.bus = bus + # Set default handlers + self.handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + if sys.platform[:4] == 'java': + del self.handlers['SIGUSR1'] + self.handlers['SIGUSR2'] = self.bus.graceful + self.bus.log('SIGUSR1 cannot be set on the JVM platform. ' + 'Using SIGUSR2 instead.') + self.handlers['SIGINT'] = self._jython_SIGINT_handler + + self._previous_handlers = {} + # used to determine is the process is a daemon in `self._is_daemonized` + self._original_pid = os.getpid() + + def _jython_SIGINT_handler(self, signum=None, frame=None): + # See http://bugs.jython.org/issue1313 + self.bus.log('Keyboard Interrupt: shutting down bus') + self.bus.exit() + + def _is_daemonized(self): + """Return boolean indicating if the current process is + running as a daemon. + + The criteria to determine the `daemon` condition is to verify + if the current pid is not the same as the one that got used on + the initial construction of the plugin *and* the stdin is not + connected to a terminal. + + The sole validation of the tty is not enough when the plugin + is executing inside other process like in a CI tool + (Buildbot, Jenkins). + """ + return ( + self._original_pid != os.getpid() and + not os.isatty(sys.stdin.fileno()) + ) + + def subscribe(self): + """Subscribe self.handlers to signals.""" + for sig, func in self.handlers.items(): + try: + self.set_handler(sig, func) + except ValueError: + pass + + def unsubscribe(self): + """Unsubscribe self.handlers from signals.""" + for signum, handler in self._previous_handlers.items(): + signame = self.signals[signum] + + if handler is None: + self.bus.log('Restoring %s handler to SIG_DFL.' % signame) + handler = _signal.SIG_DFL + else: + self.bus.log('Restoring %s handler %r.' % (signame, handler)) + + try: + our_handler = _signal.signal(signum, handler) + if our_handler is None: + self.bus.log('Restored old %s handler %r, but our ' + 'handler was not registered.' % + (signame, handler), level=30) + except ValueError: + self.bus.log('Unable to restore %s handler %r.' % + (signame, handler), level=40, traceback=True) + + def set_handler(self, signal, listener=None): + """Subscribe a handler for the given signal (number or name). + + If the optional 'listener' argument is provided, it will be + subscribed as a listener for the given signal's channel. + + If the given signal name or number is not available on the current + platform, ValueError is raised. + """ + if isinstance(signal, text_or_bytes): + signum = getattr(_signal, signal, None) + if signum is None: + raise ValueError('No such signal: %r' % signal) + signame = signal + else: + try: + signame = self.signals[signal] + except KeyError: + raise ValueError('No such signal: %r' % signal) + signum = signal + + prev = _signal.signal(signum, self._handle_signal) + self._previous_handlers[signum] = prev + + if listener is not None: + self.bus.log('Listening for %s.' % signame) + self.bus.subscribe(signame, listener) + + def _handle_signal(self, signum=None, frame=None): + """Python signal handler (self.set_handler subscribes it for you).""" + signame = self.signals[signum] + self.bus.log('Caught signal %s.' % signame) + self.bus.publish(signame) + + def handle_SIGHUP(self): + """Restart if daemonized, else exit.""" + if self._is_daemonized(): + self.bus.log('SIGHUP caught while daemonized. Restarting.') + self.bus.restart() + else: + # not daemonized (may be foreground or background) + self.bus.log('SIGHUP caught but not daemonized. Exiting.') + self.bus.exit() + + +try: + import pwd + import grp +except ImportError: + pwd, grp = None, None + + +class DropPrivileges(SimplePlugin): + + """Drop privileges. uid/gid arguments not available on Windows. + + Special thanks to `Gavin Baker + <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_ + """ + + def __init__(self, bus, umask=None, uid=None, gid=None): + SimplePlugin.__init__(self, bus) + self.finalized = False + self.uid = uid + self.gid = gid + self.umask = umask + + @property + def uid(self): + """The uid under which to run. Availability: Unix.""" + return self._uid + + @uid.setter + def uid(self, val): + if val is not None: + if pwd is None: + self.bus.log('pwd module not available; ignoring uid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = pwd.getpwnam(val)[2] + self._uid = val + + @property + def gid(self): + """The gid under which to run. Availability: Unix.""" + return self._gid + + @gid.setter + def gid(self, val): + if val is not None: + if grp is None: + self.bus.log('grp module not available; ignoring gid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = grp.getgrnam(val)[2] + self._gid = val + + @property + def umask(self): + """The default permission mode for newly created files and directories. + + Usually expressed in octal format, for example, ``0644``. + Availability: Unix, Windows. + """ + return self._umask + + @umask.setter + def umask(self, val): + if val is not None: + try: + os.umask + except AttributeError: + self.bus.log('umask function not available; ignoring umask.', + level=30) + val = None + self._umask = val + + def start(self): + # uid/gid + def current_ids(): + """Return the current (uid, gid) if available.""" + name, group = None, None + if pwd: + name = pwd.getpwuid(os.getuid())[0] + if grp: + group = grp.getgrgid(os.getgid())[0] + return name, group + + if self.finalized: + if not (self.uid is None and self.gid is None): + self.bus.log('Already running as uid: %r gid: %r' % + current_ids()) + else: + if self.uid is None and self.gid is None: + if pwd or grp: + self.bus.log('uid/gid not set', level=30) + else: + self.bus.log('Started as uid: %r gid: %r' % current_ids()) + if self.gid is not None: + os.setgid(self.gid) + os.setgroups([]) + if self.uid is not None: + os.setuid(self.uid) + self.bus.log('Running as uid: %r gid: %r' % current_ids()) + + # umask + if self.finalized: + if self.umask is not None: + self.bus.log('umask already set to: %03o' % self.umask) + else: + if self.umask is None: + self.bus.log('umask not set', level=30) + else: + old_umask = os.umask(self.umask) + self.bus.log('umask old: %03o, new: %03o' % + (old_umask, self.umask)) + + self.finalized = True + # This is slightly higher than the priority for server.start + # in order to facilitate the most common use: starting on a low + # port (which requires root) and then dropping to another user. + start.priority = 77 + + +class Daemonizer(SimplePlugin): + + """Daemonize the running script. + + Use this with a Web Site Process Bus via:: + + Daemonizer(bus).subscribe() + + When this component finishes, the process is completely decoupled from + the parent environment. Please note that when this component is used, + the return code from the parent process will still be 0 if a startup + error occurs in the forked children. Errors in the initial daemonizing + process still return proper exit codes. Therefore, if you use this + plugin to daemonize, don't use the return code as an accurate indicator + of whether the process fully started. In fact, that return code only + indicates if the process successfully finished the first fork. + """ + + def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', + stderr='/dev/null'): + SimplePlugin.__init__(self, bus) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.finalized = False + + def start(self): + if self.finalized: + self.bus.log('Already deamonized.') + + # forking has issues with threads: + # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html + # "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: + self.bus.log('There are %r active threads. ' + 'Daemonizing now may cause strange failures.' % + threading.enumerate(), level=30) + + self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log) + + self.finalized = True + start.priority = 65 + + @staticmethod + def daemonize( + stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', + logger=lambda msg: None): + # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) + # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 + + # Finish up with the current stdout/stderr + sys.stdout.flush() + sys.stderr.flush() + + error_tmpl = ( + '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n' + ) + + for fork in range(2): + msg = ['Forking once.', 'Forking twice.'][fork] + try: + pid = os.fork() + if pid > 0: + # This is the parent; exit. + logger(msg) + os._exit(0) + except OSError as exc: + # Python raises OSError rather than returning negative numbers. + sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1)) + if fork == 0: + os.setsid() + + os.umask(0) + + si = open(stdin, 'r') + so = open(stdout, 'a+') + se = open(stderr, 'a+') + + # os.dup2(fd, fd2) will close fd2 if necessary, + # so we don't explicitly close stdin/out/err. + # See http://docs.python.org/lib/os-fd-ops.html + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + logger('Daemonized to PID: %s' % os.getpid()) + + +class PIDFile(SimplePlugin): + + """Maintain a PID file via a WSPBus.""" + + def __init__(self, bus, pidfile): + SimplePlugin.__init__(self, bus) + self.pidfile = pidfile + self.finalized = False + + def start(self): + pid = os.getpid() + 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')) + self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) + self.finalized = True + start.priority = 70 + + def exit(self): + try: + os.remove(self.pidfile) + self.bus.log('PID file removed: %r.' % self.pidfile) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + pass + + +class PerpetualTimer(Timer): + + """A responsive subclass of threading.Timer whose run() method repeats. + + Use this timer only when you really need a very interruptible timer; + this checks its 'finished' condition up to 20 times a second, which can + results in pretty high CPU usage + """ + + def __init__(self, *args, **kwargs): + "Override parent constructor to allow 'bus' to be provided." + self.bus = kwargs.pop('bus', None) + super(PerpetualTimer, self).__init__(*args, **kwargs) + + def run(self): + while True: + self.finished.wait(self.interval) + if self.finished.isSet(): + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log( + 'Error in perpetual timer thread function %r.' % + self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class BackgroundTask(threading.Thread): + + """A subclass of threading.Thread whose run() method repeats. + + Use this class for most repeating tasks. It uses time.sleep() to wait + for each interval, which isn't very responsive; that is, even if you call + self.cancel(), you'll have to wait until the sleep() call finishes before + the thread stops. To compensate, it defaults to being daemonic, which means + it won't delay stopping the whole process. + """ + + def __init__(self, interval, function, args=[], kwargs={}, bus=None): + super(BackgroundTask, self).__init__() + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.running = False + self.bus = bus + + # default to daemonic + self.daemon = True + + def cancel(self): + self.running = False + + def run(self): + self.running = True + while self.running: + time.sleep(self.interval) + if not self.running: + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log('Error in background task thread function %r.' + % self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class Monitor(SimplePlugin): + + """WSPBus listener to periodically run a callback in its own thread.""" + + callback = None + """The function to call at intervals.""" + + frequency = 60 + """The time in seconds between callback runs.""" + + thread = None + """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` + thread. + """ + + def __init__(self, bus, callback, frequency=60, name=None): + SimplePlugin.__init__(self, bus) + self.callback = callback + self.frequency = frequency + self.thread = None + self.name = name + + def start(self): + """Start our callback in its own background thread.""" + if self.frequency > 0: + threadname = self.name or self.__class__.__name__ + if self.thread is None: + self.thread = BackgroundTask(self.frequency, self.callback, + bus=self.bus) + self.thread.setName(threadname) + self.thread.start() + self.bus.log('Started monitor thread %r.' % threadname) + else: + self.bus.log('Monitor thread %r already started.' % threadname) + start.priority = 70 + + def stop(self): + """Stop our callback's background task thread.""" + if self.thread is None: + 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() + self.thread.cancel() + if not self.thread.daemon: + self.bus.log('Joining %r' % name) + self.thread.join() + self.bus.log('Stopped thread %r.' % name) + self.thread = None + + def graceful(self): + """Stop the callback's background task thread and restart it.""" + self.stop() + self.start() + + +class Autoreloader(Monitor): + + """Monitor which re-executes the process when files change. + + This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`) + if any of the files it monitors change (or is deleted). By default, the + autoreloader monitors all imported modules; you can add to the + set by adding to ``autoreload.files``:: + + cherrypy.engine.autoreload.files.add(myFile) + + If there are imported files you do *not* wish to monitor, you can + adjust the ``match`` attribute, a regular expression. For example, + to stop monitoring cherrypy itself:: + + cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' + + Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins, + the autoreload plugin takes a ``frequency`` argument. The default is + 1 second; that is, the autoreloader will examine files once each second. + """ + + files = None + """The set of files to poll for modifications.""" + + frequency = 1 + """The interval in seconds at which to poll for modified files.""" + + match = '.*' + """A regular expression by which to match filenames.""" + + def __init__(self, bus, frequency=1, match='.*'): + self.mtimes = {} + self.files = set() + self.match = match + Monitor.__init__(self, bus, self.run, frequency) + + def start(self): + """Start our own background task thread for self.run.""" + if self.thread is None: + self.mtimes = {} + Monitor.start(self) + start.priority = 70 + + def sysfiles(self): + """Return a Set of sys.modules filenames to monitor.""" + search_mod_names = filter(re.compile(self.match).match, sys.modules) + mods = map(sys.modules.get, search_mod_names) + return set(filter(None, map(self._file_for_module, mods))) + + @classmethod + def _file_for_module(cls, module): + """Return the relevant file for the module.""" + return ( + cls._archive_for_zip_module(module) + or cls._file_for_file_module(module) + ) + + @staticmethod + def _archive_for_zip_module(module): + """Return the archive filename for the module if relevant.""" + try: + return module.__loader__.archive + except AttributeError: + pass + + @classmethod + def _file_for_file_module(cls, module): + """Return the file for the module.""" + try: + return module.__file__ and cls._make_absolute(module.__file__) + except AttributeError: + pass + + @staticmethod + def _make_absolute(filename): + """Ensure filename is absolute to avoid effect of os.chdir.""" + return filename if os.path.isabs(filename) else ( + os.path.normpath(os.path.join(_module__file__base, filename)) + ) + + def run(self): + """Reload the process if registered files have been modified.""" + for filename in self.sysfiles() | self.files: + if filename: + if filename.endswith('.pyc'): + filename = filename[:-1] + + oldtime = self.mtimes.get(filename, 0) + if oldtime is None: + # Module with no .py file. Skip it. + continue + + try: + mtime = os.stat(filename).st_mtime + except OSError: + # Either a module with no .py file, or it's been deleted. + mtime = None + + if filename not in self.mtimes: + # If a module has no .py file, this will be None. + self.mtimes[filename] = mtime + else: + if mtime is None or mtime > oldtime: + # The file has been deleted or modified. + self.bus.log('Restarting because %s changed.' % + filename) + self.thread.cancel() + self.bus.log('Stopped thread %r.' % + self.thread.getName()) + self.bus.restart() + return + + +class ThreadManager(SimplePlugin): + + """Manager for HTTP request threads. + + If you have control over thread creation and destruction, publish to + the 'acquire_thread' and 'release_thread' channels (for each thread). + This will register/unregister the current thread and publish to + 'start_thread' and 'stop_thread' listeners in the bus as needed. + + If threads are created and destroyed by code you do not control + (e.g., Apache), then, at the beginning of every HTTP request, + publish to 'acquire_thread' only. You should not publish to + 'release_thread' in this case, since you do not know whether + the thread will be re-used or not. The bus will call + 'stop_thread' listeners for you when it stops. + """ + + threads = None + """A map of {thread ident: index number} pairs.""" + + def __init__(self, bus): + self.threads = {} + SimplePlugin.__init__(self, bus) + self.bus.listeners.setdefault('acquire_thread', set()) + self.bus.listeners.setdefault('start_thread', set()) + self.bus.listeners.setdefault('release_thread', set()) + self.bus.listeners.setdefault('stop_thread', set()) + + def acquire_thread(self): + """Run 'start_thread' listeners for the current thread. + + If the current thread has already been seen, any 'start_thread' + listeners will not be run again. + """ + thread_ident = _thread.get_ident() + if thread_ident not in self.threads: + # We can't just use get_ident as the thread ID + # because some platforms reuse thread ID's. + i = len(self.threads) + 1 + self.threads[thread_ident] = i + self.bus.publish('start_thread', i) + + def release_thread(self): + """Release the current thread and run 'stop_thread' listeners.""" + thread_ident = _thread.get_ident() + i = self.threads.pop(thread_ident, None) + if i is not None: + self.bus.publish('stop_thread', i) + + def stop(self): + """Release all threads and run all 'stop_thread' listeners.""" + for thread_ident, i in self.threads.items(): + self.bus.publish('stop_thread', i) + self.threads.clear() + graceful = stop diff --git a/libraries/cherrypy/process/servers.py b/libraries/cherrypy/process/servers.py new file mode 100644 index 00000000..dcb34de6 --- /dev/null +++ b/libraries/cherrypy/process/servers.py @@ -0,0 +1,416 @@ +r""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin<plugins>`. It's an instance of +:class:`cherrypy._cpserver.Server`, which is a subclass of +:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class +is designed to control other servers, as well. + +Multiple servers/ports +====================== + +If you need to start more than one HTTP server (to serve on multiple ports, or +protocols, etc.), you can manually register each one and then start them all +with engine.start:: + + s1 = ServerAdapter( + cherrypy.engine, + MyWSGIServer(host='0.0.0.0', port=80) + ) + s2 = ServerAdapter( + cherrypy.engine, + another.HTTPServer(host='127.0.0.1', SSL=True) + ) + s1.subscribe() + s2.subscribe() + cherrypy.engine.start() + +.. index:: SCGI + +FastCGI/SCGI +============ + +There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in +:mod:`cherrypy.process.servers`. To start an fcgi server, for example, +wrap an instance of it in a ServerAdapter:: + + addr = ('0.0.0.0', 4000) + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) + s.subscribe() + +The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for +you via its `-f` flag. +Note that you need to download and install `flup <http://trac.saddi.com/flup>`_ +yourself, whether you use ``cherryd`` or not. + +.. _fastcgi: +.. index:: FastCGI + +FastCGI +------- + +A very simple setup lets your cherry run with FastCGI. +You just need the flup library, +plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. + +CherryPy code +^^^^^^^^^^^^^ + +hello.py:: + + #!/usr/bin/python + import cherrypy + + class HelloWorld: + '''Sample request handler class.''' + @cherrypy.expose + def index(self): + return "Hello world!" + + cherrypy.tree.mount(HelloWorld()) + # CherryPy autoreload must be disabled for the flup server to work + cherrypy.config.update({'engine.autoreload.on':False}) + +Then run :doc:`/deployguide/cherryd` with the '-f' arg:: + + cherryd -c <myconfig> -d -f -i hello.py + +Apache +^^^^^^ + +At the top level in httpd.conf:: + + FastCgiIpcDir /tmp + FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 + +And inside the relevant VirtualHost section:: + + # FastCGI config + AddHandler fastcgi-script .fcgi + ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 + +Lighttpd +^^^^^^^^ + +For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these +instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is +active within ``server.modules``. Then, within your ``$HTTP["host"]`` +directive, configure your fastcgi script like the following:: + + $HTTP["url"] =~ "" { + fastcgi.server = ( + "/" => ( + "script.fcgi" => ( + "bin-path" => "/path/to/your/script.fcgi", + "socket" => "/tmp/script.sock", + "check-local" => "disable", + "disable-time" => 1, + "min-procs" => 1, + "max-procs" => 1, # adjust as needed + ), + ), + ) + } # end of $HTTP["url"] =~ "^/" + +Please see `Lighttpd FastCGI Docs +<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for +an explanation of the possible configuration options. +""" + +import os +import sys +import time +import warnings +import contextlib + +import portend + + +class Timeouts: + occupied = 5 + free = 1 + + +class ServerAdapter(object): + + """Adapter for an HTTP server. + + If you need to start more than one HTTP server (to serve on multiple + ports, or protocols, etc.), you can manually register each one and then + start them all with bus.start:: + + s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + bus.start() + """ + + def __init__(self, bus, httpserver=None, bind_addr=None): + self.bus = bus + self.httpserver = httpserver + self.bind_addr = bind_addr + self.interrupt = None + self.running = False + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + + def unsubscribe(self): + self.bus.unsubscribe('start', self.start) + self.bus.unsubscribe('stop', self.stop) + + def start(self): + """Start the HTTP server.""" + if self.running: + self.bus.log('Already serving on %s' % self.description) + return + + self.interrupt = None + if not self.httpserver: + raise ValueError('No HTTP server has been created.') + + if not os.environ.get('LISTEN_PID', None): + # Start the httpserver in a new thread. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bind_addr, timeout=Timeouts.free) + + import threading + t = threading.Thread(target=self._start_http_thread) + t.setName('HTTPServer ' + t.getName()) + t.start() + + self.wait() + self.running = True + self.bus.log('Serving on %s' % self.description) + start.priority = 75 + + @property + def description(self): + """ + A description about where this server is bound. + """ + if self.bind_addr is None: + on_what = 'unknown interface (dynamic?)' + elif isinstance(self.bind_addr, tuple): + on_what = self._get_base() + else: + on_what = 'socket file: %s' % self.bind_addr + return on_what + + def _get_base(self): + if not self.httpserver: + return '' + host, port = self.bound_addr + if getattr(self.httpserver, 'ssl_adapter', None): + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) + + def _start_http_thread(self): + """HTTP servers MUST be running in new threads, so that the + main thread persists to receive KeyboardInterrupt's. If an + exception is raised in the httpserver's thread then it's + trapped here, and the bus (and therefore our httpserver) + are shut down. + """ + try: + self.httpserver.start() + except KeyboardInterrupt: + self.bus.log('<Ctrl-C> hit: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + except SystemExit: + self.bus.log('SystemExit raised: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + raise + except Exception: + self.interrupt = sys.exc_info()[1] + self.bus.log('Error in HTTP server: shutting down', + traceback=True, level=40) + self.bus.exit() + raise + + def wait(self): + """Wait until the HTTP server is ready to receive requests.""" + while not getattr(self.httpserver, 'ready', False): + if self.interrupt: + raise self.interrupt + time.sleep(.1) + + # bypass check when LISTEN_PID is set + if os.environ.get('LISTEN_PID', None): + return + + # bypass check when running via socket-activation + # (for socket-activation the port will be managed by systemd) + if not isinstance(self.bind_addr, tuple): + return + + # wait for port to be occupied + with _safe_wait(*self.bound_addr): + portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) + + @property + def bound_addr(self): + """ + The bind address, or if it's an ephemeral port and the + socket has been bound, return the actual port bound. + """ + host, port = self.bind_addr + if port == 0 and self.httpserver.socket: + # Bound to ephemeral port. Get the actual port allocated. + port = self.httpserver.socket.getsockname()[1] + return host, port + + def stop(self): + """Stop the HTTP server.""" + if self.running: + # stop() MUST block until the server is *truly* stopped. + self.httpserver.stop() + # Wait for the socket to be truly freed. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bound_addr, timeout=Timeouts.free) + self.running = False + self.bus.log('HTTP Server %s shut down' % self.httpserver) + else: + self.bus.log('HTTP Server %s already shut down' % self.httpserver) + stop.priority = 25 + + def restart(self): + """Restart the HTTP server.""" + self.stop() + self.start() + + +class FlupCGIServer(object): + + """Adapter for a flup.server.cgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the CGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.cgi import WSGIServer + + self.cgiserver = WSGIServer(*self.args, **self.kwargs) + self.ready = True + self.cgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + + +class FlupFCGIServer(object): + + """Adapter for a flup.server.fcgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + if kwargs.get('bindAddress', None) is None: + import socket + if not hasattr(socket, 'fromfd'): + raise ValueError( + 'Dynamic FCGI server not available on this platform. ' + 'You must use a static or external one by providing a ' + 'legal bindAddress.') + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the FCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.fcgi import WSGIServer + self.fcgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.fcgiserver._installSignalHandlers = lambda: None + self.fcgiserver._oldSIGs = [] + self.ready = True + self.fcgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + # Forcibly stop the fcgi server main event loop. + self.fcgiserver._keepGoing = False + # Force all worker threads to die off. + self.fcgiserver._threadPool.maxSpare = ( + self.fcgiserver._threadPool._idleCount) + self.ready = False + + +class FlupSCGIServer(object): + + """Adapter for a flup.server.scgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the SCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.scgi import WSGIServer + self.scgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.scgiserver._installSignalHandlers = lambda: None + self.scgiserver._oldSIGs = [] + self.ready = True + self.scgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + # Forcibly stop the scgi server main event loop. + self.scgiserver._keepGoing = False + # Force all worker threads to die off. + self.scgiserver._threadPool.maxSpare = 0 + + +@contextlib.contextmanager +def _safe_wait(host, port): + """ + On systems where a loopback interface is not available and the + server is bound to all interfaces, it's difficult to determine + whether the server is in fact occupying the port. In this case, + just issue a warning and move on. See issue #1100. + """ + try: + yield + except portend.Timeout: + if host == portend.client_host(host): + raise + msg = 'Unable to verify that the server is bound on %r' % port + warnings.warn(msg) diff --git a/libraries/cherrypy/process/win32.py b/libraries/cherrypy/process/win32.py new file mode 100644 index 00000000..096b0278 --- /dev/null +++ b/libraries/cherrypy/process/win32.py @@ -0,0 +1,183 @@ +"""Windows service. Requires pywin32.""" + +import os +import win32api +import win32con +import win32event +import win32service +import win32serviceutil + +from cherrypy.process import wspbus, plugins + + +class ConsoleCtrlHandler(plugins.SimplePlugin): + + """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" + + def __init__(self, bus): + self.is_set = False + plugins.SimplePlugin.__init__(self, bus) + + def start(self): + if self.is_set: + self.bus.log('Handler for console events already set.', level=40) + return + + result = win32api.SetConsoleCtrlHandler(self.handle, 1) + if result == 0: + self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Set handler for console events.', level=40) + self.is_set = True + + def stop(self): + if not self.is_set: + self.bus.log('Handler for console events already off.', level=40) + return + + try: + result = win32api.SetConsoleCtrlHandler(self.handle, 0) + except ValueError: + # "ValueError: The object has not been registered" + result = 1 + + if result == 0: + self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Removed handler for console events.', level=40) + self.is_set = False + + def handle(self, event): + """Handle console control events (like Ctrl-C).""" + if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT): + self.bus.log('Console event %s: shutting down bus' % event) + + # Remove self immediately so repeated Ctrl-C doesn't re-call it. + try: + self.stop() + except ValueError: + pass + + self.bus.exit() + # 'First to return True stops the calls' + return 1 + return 0 + + +class Win32Bus(wspbus.Bus): + + """A Web Site Process Bus implementation for Win32. + + Instead of time.sleep, this bus blocks using native win32event objects. + """ + + def __init__(self): + self.events = {} + wspbus.Bus.__init__(self) + + def _get_state_event(self, state): + """Return a win32event for the given state (creating it if needed).""" + try: + return self.events[state] + except KeyError: + event = win32event.CreateEvent(None, 0, 0, + 'WSPBus %s Event (pid=%r)' % + (state.name, os.getpid())) + self.events[state] = event + return event + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + self._state = value + event = self._get_state_event(value) + win32event.PulseEvent(event) + + def wait(self, state, interval=0.1, channel=None): + """Wait for the given state(s), KeyboardInterrupt or SystemExit. + + Since this class uses native win32event objects, the interval + argument is ignored. + """ + if isinstance(state, (tuple, list)): + # Don't wait for an event that beat us to the punch ;) + if self.state not in state: + events = tuple([self._get_state_event(s) for s in state]) + win32event.WaitForMultipleObjects( + events, 0, win32event.INFINITE) + else: + # Don't wait for an event that beat us to the punch ;) + if self.state != state: + event = self._get_state_event(state) + win32event.WaitForSingleObject(event, win32event.INFINITE) + + +class _ControlCodes(dict): + + """Control codes used to "signal" a service via ControlService. + + User-defined control codes are in the range 128-255. We generally use + the standard Python value for the Linux signal and add 128. Example: + + >>> signal.SIGUSR1 + 10 + control_codes['graceful'] = 128 + 10 + """ + + def key_for(self, obj): + """For the given value, return its corresponding key.""" + for key, val in self.items(): + if val is obj: + return key + raise ValueError('The given object could not be found: %r' % obj) + + +control_codes = _ControlCodes({'graceful': 138}) + + +def signal_child(service, command): + if command == 'stop': + win32serviceutil.StopService(service) + elif command == 'restart': + win32serviceutil.RestartService(service) + else: + win32serviceutil.ControlService(service, control_codes[command]) + + +class PyWebService(win32serviceutil.ServiceFramework): + + """Python Web Service.""" + + _svc_name_ = 'Python Web Service' + _svc_display_name_ = 'Python Web Service' + _svc_deps_ = None # sequence of service names on which this depends + _exe_name_ = 'pywebsvc' + _exe_args_ = None # Default to no arguments + + # Only exists on Windows 2000 or later, ignored on windows NT + _svc_description_ = 'Python Web Service' + + def SvcDoRun(self): + from cherrypy import process + process.bus.start() + process.bus.block() + + def SvcStop(self): + from cherrypy import process + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + process.bus.exit() + + def SvcOther(self, control): + from cherrypy import process + process.bus.publish(control_codes.key_for(control)) + + +if __name__ == '__main__': + win32serviceutil.HandleCommandLine(PyWebService) diff --git a/libraries/cherrypy/process/wspbus.py b/libraries/cherrypy/process/wspbus.py new file mode 100644 index 00000000..39ac45bf --- /dev/null +++ b/libraries/cherrypy/process/wspbus.py @@ -0,0 +1,590 @@ +r"""An implementation of the Web Site Process Bus. + +This module is completely standalone, depending only on the stdlib. + +Web Site Process Bus +-------------------- + +A Bus object is used to contain and manage site-wide behavior: +daemonization, HTTP server start/stop, process reload, signal handling, +drop privileges, PID file management, logging for all of these, +and many more. + +In addition, a Bus object provides a place for each web framework +to register code that runs in response to site-wide events (like +process start and stop), or which controls or otherwise interacts with +the site-wide components mentioned above. For example, a framework which +uses file-based templates would add known template filenames to an +autoreload component. + +Ideally, a Bus object will be flexible enough to be useful in a variety +of invocation scenarios: + + 1. The deployer starts a site from the command line via a + framework-neutral deployment script; applications from multiple frameworks + are mixed in a single site. Command-line arguments and configuration + files are used to define site-wide components such as the HTTP server, + WSGI component graph, autoreload behavior, signal handling, etc. + 2. The deployer starts a site via some other process, such as Apache; + applications from multiple frameworks are mixed in a single site. + Autoreload and signal handling (from Python at least) are disabled. + 3. The deployer starts a site via a framework-specific mechanism; + for example, when running tests, exploring tutorials, or deploying + single applications from a single framework. The framework controls + which site-wide components are enabled as it sees fit. + +The Bus object in this package uses topic-based publish-subscribe +messaging to accomplish all this. A few topic channels are built in +('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and +site containers are free to define their own. If a message is sent to a +channel that has not been defined or has no listeners, there is no effect. + +In general, there should only ever be a single Bus object per process. +Frameworks and site containers share a single Bus object by publishing +messages and subscribing listeners. + +The Bus object works as a finite state machine which models the current +state of the process. Bus methods move it from one state to another; +those methods then publish to subscribed listeners on the channel for +the new state.:: + + O + | + V + STOPPING --> STOPPED --> EXITING -> X + A A | + | \___ | + | \ | + | V V + STARTED <-- STARTING + +""" + +import atexit + +try: + import ctypes +except (ImportError, MemoryError): + """Google AppEngine is shipped without ctypes + + :seealso: http://stackoverflow.com/a/6523777/70170 + """ + ctypes = None + +import operator +import os +import sys +import threading +import time +import traceback as _traceback +import warnings +import subprocess +import functools + +import six + + +# Here I save the value of os.getcwd(), which, if I am imported early enough, +# will be the directory from which the startup script was run. This is needed +# by _do_execv(), to change back to the original directory before execv()ing a +# new process. This is a defense against the application having changed the +# current working directory (which could make sys.executable "not found" if +# sys.executable is a relative-path, and/or cause other problems). +_startup_cwd = os.getcwd() + + +class ChannelFailures(Exception): + """Exception raised during errors on Bus.publish().""" + + delimiter = '\n' + + def __init__(self, *args, **kwargs): + """Initialize ChannelFailures errors wrapper.""" + super(ChannelFailures, self).__init__(*args, **kwargs) + self._exceptions = list() + + def handle_exception(self): + """Append the current exception to self.""" + self._exceptions.append(sys.exc_info()[1]) + + def get_instances(self): + """Return a list of seen exception instances.""" + return self._exceptions[:] + + def __str__(self): + """Render the list of errors, which happened in channel.""" + exception_strings = map(repr, self.get_instances()) + return self.delimiter.join(exception_strings) + + __repr__ = __str__ + + def __bool__(self): + """Determine whether any error happened in channel.""" + return bool(self._exceptions) + __nonzero__ = __bool__ + +# Use a flag to indicate the state of the bus. + + +class _StateEnum(object): + + class State(object): + name = None + + def __repr__(self): + return 'states.%s' % self.name + + def __setattr__(self, key, value): + if isinstance(value, self.State): + value.name = key + object.__setattr__(self, key, value) + + +states = _StateEnum() +states.STOPPED = states.State() +states.STARTING = states.State() +states.STARTED = states.State() +states.STOPPING = states.State() +states.EXITING = states.State() + + +try: + import fcntl +except ImportError: + max_files = 0 +else: + try: + max_files = os.sysconf('SC_OPEN_MAX') + except AttributeError: + max_files = 1024 + + +class Bus(object): + """Process state-machine and messenger for HTTP site deployment. + + All listeners for a given channel are guaranteed to be called even + if others at the same channel fail. Each failure is logged, but + execution proceeds on to the next listener. The only way to stop all + processing from inside a listener is to raise SystemExit and stop the + whole server. + """ + + states = states + state = states.STOPPED + execv = False + max_cloexec_files = max_files + + def __init__(self): + """Initialize pub/sub bus.""" + self.execv = False + self.state = states.STOPPED + channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main' + self.listeners = dict( + (channel, set()) + for channel in channels + ) + self._priorities = {} + + def subscribe(self, channel, callback=None, priority=None): + """Add the given callback at the given channel (if not present). + + If callback is None, return a partial suitable for decorating + the callback. + """ + if callback is None: + return functools.partial( + self.subscribe, + channel, + priority=priority, + ) + + ch_listeners = self.listeners.setdefault(channel, set()) + ch_listeners.add(callback) + + if priority is None: + priority = getattr(callback, 'priority', 50) + self._priorities[(channel, callback)] = priority + + def unsubscribe(self, channel, callback): + """Discard the given callback (if present).""" + listeners = self.listeners.get(channel) + if listeners and callback in listeners: + listeners.discard(callback) + del self._priorities[(channel, callback)] + + def publish(self, channel, *args, **kwargs): + """Return output of all subscribers for the given channel.""" + if channel not in self.listeners: + return [] + + exc = ChannelFailures() + output = [] + + raw_items = ( + (self._priorities[(channel, listener)], listener) + for listener in self.listeners[channel] + ) + items = sorted(raw_items, key=operator.itemgetter(0)) + for priority, listener in items: + try: + output.append(listener(*args, **kwargs)) + except KeyboardInterrupt: + raise + except SystemExit: + e = sys.exc_info()[1] + # If we have previous errors ensure the exit code is non-zero + if exc and e.code == 0: + e.code = 1 + raise + except Exception: + exc.handle_exception() + if channel == 'log': + # Assume any further messages to 'log' will fail. + pass + else: + self.log('Error in %r listener %r' % (channel, listener), + level=40, traceback=True) + if exc: + raise exc + return output + + def _clean_exit(self): + """Assert that the Bus is not running in atexit handler callback.""" + if self.state != states.EXITING: + warnings.warn( + 'The main thread is exiting, but the Bus is in the %r state; ' + 'shutting it down automatically now. You must either call ' + 'bus.block() after start(), or call bus.exit() before the ' + 'main thread exits.' % self.state, RuntimeWarning) + self.exit() + + def start(self): + """Start all services.""" + atexit.register(self._clean_exit) + + self.state = states.STARTING + self.log('Bus STARTING') + try: + self.publish('start') + self.state = states.STARTED + self.log('Bus STARTED') + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.log('Shutting down due to error in start listener:', + level=40, traceback=True) + e_info = sys.exc_info()[1] + try: + self.exit() + except Exception: + # Any stop/exit errors will be logged inside publish(). + pass + # Re-raise the original error + raise e_info + + def exit(self): + """Stop all services and prepare to exit the process.""" + exitstate = self.state + EX_SOFTWARE = 70 + try: + self.stop() + + self.state = states.EXITING + self.log('Bus EXITING') + self.publish('exit') + # This isn't strictly necessary, but it's better than seeing + # "Waiting for child threads to terminate..." and then nothing. + self.log('Bus EXITED') + except Exception: + # This method is often called asynchronously (whether thread, + # signal handler, console handler, or atexit handler), so we + # can't just let exceptions propagate out unhandled. + # Assume it's been logged and just die. + os._exit(EX_SOFTWARE) + + if exitstate == states.STARTING: + # exit() was called before start() finished, possibly due to + # Ctrl-C because a start listener got stuck. In this case, + # we could get stuck in a loop where Ctrl-C never exits the + # process, so we just call os.exit here. + os._exit(EX_SOFTWARE) + + def restart(self): + """Restart the process (may close connections). + + This method does not restart the process from the calling thread; + instead, it stops the bus and asks the main thread to call execv. + """ + self.execv = True + self.exit() + + def graceful(self): + """Advise all services to reload.""" + self.log('Bus graceful') + self.publish('graceful') + + def block(self, interval=0.1): + """Wait for the EXITING state, KeyboardInterrupt or SystemExit. + + This function is intended to be called only by the main thread. + After waiting for the EXITING state, it also waits for all threads + to terminate, and then calls os.execv if self.execv is True. This + design allows another thread to call bus.restart, yet have the main + thread perform the actual execv call (required on some platforms). + """ + try: + self.wait(states.EXITING, interval=interval, channel='main') + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.log('Keyboard Interrupt: shutting down bus') + self.exit() + except SystemExit: + self.log('SystemExit raised: shutting down bus') + self.exit() + raise + + # Waiting for ALL child threads to finish is necessary on OS X. + # See https://github.com/cherrypy/cherrypy/issues/581. + # It's also good to let them all shut down before allowing + # the main thread to call atexit handlers. + # See https://github.com/cherrypy/cherrypy/issues/751. + self.log('Waiting for child threads to terminate...') + for t in threading.enumerate(): + # Validate the we're not trying to join the MainThread + # that will cause a deadlock and the case exist when + # implemented as a windows service and in any other case + # that another thread executes cherrypy.engine.exit() + if ( + t != threading.currentThread() 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()) + t.join() + + if self.execv: + self._do_execv() + + def wait(self, state, interval=0.1, channel=None): + """Poll for the given state(s) at intervals; publish to channel.""" + if isinstance(state, (tuple, list)): + states = state + else: + states = [state] + + while self.state not in states: + time.sleep(interval) + self.publish(channel) + + def _do_execv(self): + """Re-execute the current process. + + This must be called from the main thread, because certain platforms + (OS X) don't allow execv to be called in a child thread very well. + """ + try: + args = self._get_true_argv() + except NotImplementedError: + """It's probably win32 or GAE""" + args = [sys.executable] + self._get_interpreter_argv() + sys.argv + + self.log('Re-spawning %s' % ' '.join(args)) + + self._extend_pythonpath(os.environ) + + if sys.platform[:4] == 'java': + from _systemrestart import SystemRestart + raise SystemRestart + else: + if sys.platform == 'win32': + args = ['"%s"' % arg for arg in args] + + os.chdir(_startup_cwd) + if self.max_cloexec_files: + self._set_cloexec() + os.execv(sys.executable, args) + + @staticmethod + def _get_interpreter_argv(): + """Retrieve current Python interpreter's arguments. + + Returns empty tuple in case of frozen mode, uses built-in arguments + reproduction function otherwise. + + Frozen mode is possible for the app has been packaged into a binary + executable using py2exe. In this case the interpreter's arguments are + already built-in into that executable. + + :seealso: https://github.com/cherrypy/cherrypy/issues/1526 + Ref: https://pythonhosted.org/PyInstaller/runtime-information.html + """ + return ([] + if getattr(sys, 'frozen', False) + else subprocess._args_from_interpreter_flags()) + + @staticmethod + def _get_true_argv(): + """Retrieve all real arguments of the python interpreter. + + ...even those not listed in ``sys.argv`` + + :seealso: http://stackoverflow.com/a/28338254/595220 + :seealso: http://stackoverflow.com/a/6683222/595220 + :seealso: http://stackoverflow.com/a/28414807/595220 + """ + try: + char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p + + argv = ctypes.POINTER(char_p)() + argc = ctypes.c_int() + + ctypes.pythonapi.Py_GetArgcArgv( + ctypes.byref(argc), + ctypes.byref(argv), + ) + + _argv = argv[:argc.value] + + # The code below is trying to correctly handle special cases. + # `-c`'s argument interpreted by Python itself becomes `-c` as + # well. Same applies to `-m`. This snippet is trying to survive + # at least the case with `-m` + # Ref: https://github.com/cherrypy/cherrypy/issues/1545 + # Ref: python/cpython@418baf9 + argv_len, is_command, is_module = len(_argv), False, False + + try: + m_ind = _argv.index('-m') + if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'): + """ + In some older Python versions `-m`'s argument may be + substituted with `-c`, not `-m` + """ + is_module = True + except (IndexError, ValueError): + m_ind = None + + try: + c_ind = _argv.index('-c') + if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c': + is_command = True + except (IndexError, ValueError): + c_ind = None + + if is_module: + """It's containing `-m -m` sequence of arguments""" + if is_command and c_ind < m_ind: + """There's `-c -c` before `-m`""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. Ref: " + 'https://github.com/cherrypy/cherrypy/issues/1545') + # Survive module argument here + original_module = sys.argv[0] + if not os.access(original_module, os.R_OK): + """There's no such module exist""" + raise AttributeError( + "{} doesn't seem to be a module " + 'accessible by current user'.format(original_module)) + del _argv[m_ind:m_ind + 2] # remove `-m -m` + # ... and substitute it with the original module path: + _argv.insert(m_ind, original_module) + elif is_command: + """It's containing just `-c -c` sequence of arguments""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. " + 'Ref: https://github.com/cherrypy/cherrypy/issues/1545') + except AttributeError: + """It looks Py_GetArgcArgv is completely absent in some environments + + It is known, that there's no Py_GetArgcArgv in MS Windows and + ``ctypes`` module is completely absent in Google AppEngine + + :seealso: https://github.com/cherrypy/cherrypy/issues/1506 + :seealso: https://github.com/cherrypy/cherrypy/issues/1512 + :ref: http://bit.ly/2gK6bXK + """ + raise NotImplementedError + else: + return _argv + + @staticmethod + def _extend_pythonpath(env): + """Prepend current working dir to PATH environment variable if needed. + + If sys.path[0] is an empty string, the interpreter was likely + invoked with -m and the effective path is about to change on + re-exec. Add the current directory to $PYTHONPATH to ensure + that the new process sees the same path. + + This issue cannot be addressed in the general case because + Python cannot reliably reconstruct the + original command line (http://bugs.python.org/issue14208). + + (This idea filched from tornado.autoreload) + """ + path_prefix = '.' + os.pathsep + existing_path = env.get('PYTHONPATH', '') + needs_patch = ( + sys.path[0] == '' and + not existing_path.startswith(path_prefix) + ) + + if needs_patch: + env['PYTHONPATH'] = path_prefix + existing_path + + def _set_cloexec(self): + """Set the CLOEXEC flag on all open files (except stdin/out/err). + + If self.max_cloexec_files is an integer (the default), then on + platforms which support it, it represents the max open files setting + for the operating system. This function will be called just before + the process is restarted via os.execv() to prevent open files + from persisting into the new process. + + Set self.max_cloexec_files to 0 to disable this behavior. + """ + for fd in range(3, self.max_cloexec_files): # skip stdin/out/err + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + except IOError: + continue + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + def stop(self): + """Stop all services.""" + self.state = states.STOPPING + self.log('Bus STOPPING') + self.publish('stop') + self.state = states.STOPPED + self.log('Bus STOPPED') + + def start_with_callback(self, func, args=None, kwargs=None): + """Start 'func' in a new thread T, then start self (and return T).""" + if args is None: + args = () + if kwargs is None: + kwargs = {} + args = (func,) + args + + 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.start() + + self.start() + + return t + + def log(self, msg='', level=20, traceback=False): + """Log the given message. Append the last traceback if requested.""" + if traceback: + msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) + self.publish('log', msg, level) + + +bus = Bus() diff --git a/libraries/cherrypy/scaffold/__init__.py b/libraries/cherrypy/scaffold/__init__.py new file mode 100644 index 00000000..bcddba2d --- /dev/null +++ b/libraries/cherrypy/scaffold/__init__.py @@ -0,0 +1,63 @@ +"""<MyProject>, a CherryPy application. + +Use this as a base for creating new CherryPy applications. When you want +to make a new app, copy and paste this folder to some other location +(maybe site-packages) and rename it to the name of your project, +then tweak as desired. + +Even before any tweaking, this should serve a few demonstration pages. +Change to this directory and run: + + cherryd -c site.conf + +""" + +import cherrypy +from cherrypy import tools, url + +import os +local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +@cherrypy.config(**{'tools.log_tracebacks.on': True}) +class Root: + """Declaration of the CherryPy app URI structure.""" + + @cherrypy.expose + def index(self): + """Render HTML-template at the root path of the web-app.""" + return """<html> +<body>Try some <a href='%s?a=7'>other</a> path, +or a <a href='%s?n=14'>default</a> path.<br /> +Or, just look at the pretty picture:<br /> +<img src='%s' /> +</body></html>""" % (url('other'), url('else'), + url('files/made_with_cherrypy_small.png')) + + @cherrypy.expose + def default(self, *args, **kwargs): + """Render catch-all args and kwargs.""" + return 'args: %s kwargs: %s' % (args, kwargs) + + @cherrypy.expose + def other(self, a=2, b='bananas', c=None): + """Render number of fruits based on third argument.""" + cherrypy.response.headers['Content-Type'] = 'text/plain' + if c is None: + return 'Have %d %s.' % (int(a), b) + else: + return 'Have %d %s, %s.' % (int(a), b, c) + + files = tools.staticdir.handler( + section='/files', + dir=os.path.join(local_dir, 'static'), + # Ignore .php files, etc. + match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', + ) + + +root = Root() + +# Uncomment the following to use your own favicon instead of CP's default. +# favicon_path = os.path.join(local_dir, "favicon.ico") +# root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/libraries/cherrypy/scaffold/apache-fcgi.conf b/libraries/cherrypy/scaffold/apache-fcgi.conf new file mode 100644 index 00000000..6e4f144c --- /dev/null +++ b/libraries/cherrypy/scaffold/apache-fcgi.conf @@ -0,0 +1,22 @@ +# Apache2 server conf file for using CherryPy with mod_fcgid. + +# This doesn't have to be "C:/", but it has to be a directory somewhere, and +# MUST match the directory used in the FastCgiExternalServer directive, below. +DocumentRoot "C:/" + +ServerName 127.0.0.1 +Listen 80 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +# Send requests for any URI to our fastcgi handler. +RewriteRule ^(.*)$ /fastcgi.pyc [L] + +# The FastCgiExternalServer directive defines filename as an external FastCGI application. +# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. +# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this +# filename will be handled by this external FastCGI application. +FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 diff --git a/libraries/cherrypy/scaffold/example.conf b/libraries/cherrypy/scaffold/example.conf new file mode 100644 index 00000000..63250fe3 --- /dev/null +++ b/libraries/cherrypy/scaffold/example.conf @@ -0,0 +1,3 @@ +[/] +log.error_file: "error.log" +log.access_file: "access.log" diff --git a/libraries/cherrypy/scaffold/site.conf b/libraries/cherrypy/scaffold/site.conf new file mode 100644 index 00000000..6ed38983 --- /dev/null +++ b/libraries/cherrypy/scaffold/site.conf @@ -0,0 +1,14 @@ +[global] +# Uncomment this when you're done developing +#environment: "production" + +server.socket_host: "0.0.0.0" +server.socket_port: 8088 + +# Uncomment the following lines to run on HTTPS at the same time +#server.2.socket_host: "0.0.0.0" +#server.2.socket_port: 8433 +#server.2.ssl_certificate: '../test/test.pem' +#server.2.ssl_private_key: '../test/test.pem' + +tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png new file mode 100644 index 0000000000000000000000000000000000000000..724f9d72d9ca5aede0b788fc1216286aa967e212 GIT binary patch literal 6347 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 N002ovPDHLkV1nY?RmA`R literal 0 HcmV?d00001 diff --git a/libraries/cherrypy/test/__init__.py b/libraries/cherrypy/test/__init__.py new file mode 100644 index 00000000..068382be --- /dev/null +++ b/libraries/cherrypy/test/__init__.py @@ -0,0 +1,24 @@ +""" +Regression test suite for CherryPy. +""" + +import os +import sys + + +def newexit(): + os._exit(1) + + +def setup(): + # We want to monkey patch sys.exit so that we can get some + # information about where exit is being called. + newexit._old = sys.exit + sys.exit = newexit + + +def teardown(): + try: + sys.exit = sys.exit._old + except AttributeError: + sys.exit = sys._exit diff --git a/libraries/cherrypy/test/_test_decorators.py b/libraries/cherrypy/test/_test_decorators.py new file mode 100644 index 00000000..74832e40 --- /dev/null +++ b/libraries/cherrypy/test/_test_decorators.py @@ -0,0 +1,39 @@ +"""Test module for the @-decorator syntax, which is version-specific""" + +import cherrypy +from cherrypy import expose, tools + + +class ExposeExamples(object): + + @expose + def no_call(self): + return 'Mr E. R. Bradshaw' + + @expose() + def call_empty(self): + return 'Mrs. B.J. Smegma' + + @expose('call_alias') + def nesbitt(self): + return 'Mr Nesbitt' + + @expose(['alias1', 'alias2']) + def andrews(self): + return 'Mr Ken Andrews' + + @expose(alias='alias3') + def watson(self): + return 'Mr. and Mrs. Watson' + + +class ToolExamples(object): + + @expose + # This is here to demonstrate that using the config decorator + # does not overwrite other config attributes added by the Tool + # decorator (in this case response_headers). + @cherrypy.config(**{'response.stream': True}) + @tools.response_headers(headers=[('Content-Type', 'application/data')]) + def blah(self): + yield b'blah' diff --git a/libraries/cherrypy/test/_test_states_demo.py b/libraries/cherrypy/test/_test_states_demo.py new file mode 100644 index 00000000..a49407ba --- /dev/null +++ b/libraries/cherrypy/test/_test_states_demo.py @@ -0,0 +1,69 @@ +import os +import sys +import time + +import cherrypy + +starttime = time.time() + + +class Root: + + @cherrypy.expose + def index(self): + return 'Hello World' + + @cherrypy.expose + def mtimes(self): + return repr(cherrypy.engine.publish('Autoreloader', 'mtimes')) + + @cherrypy.expose + def pid(self): + return str(os.getpid()) + + @cherrypy.expose + def start(self): + return repr(starttime) + + @cherrypy.expose + def exit(self): + # This handler might be called before the engine is STARTED if an + # HTTP worker thread handles it before the HTTP server returns + # control to engine.start. We avoid that race condition here + # by waiting for the Bus to be STARTED. + cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) + cherrypy.engine.exit() + + +@cherrypy.engine.subscribe('start', priority=100) +def unsub_sig(): + cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False)) + if cherrypy.config.get('unsubsig', False): + cherrypy.log('Unsubscribing the default cherrypy signal handler') + cherrypy.engine.signal_handler.unsubscribe() + try: + from signal import signal, SIGTERM + except ImportError: + pass + else: + def old_term_handler(signum=None, frame=None): + cherrypy.log('I am an old SIGTERM handler.') + sys.exit(0) + cherrypy.log('Subscribing the new one.') + signal(SIGTERM, old_term_handler) + + +@cherrypy.engine.subscribe('start', priority=6) +def starterror(): + if cherrypy.config.get('starterror', False): + 1 / 0 + + +@cherrypy.engine.subscribe('start', priority=6) +def log_test_case_name(): + if cherrypy.config.get('test_case_name', False): + cherrypy.log('STARTED FROM: %s' % + cherrypy.config.get('test_case_name')) + + +cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/libraries/cherrypy/test/benchmark.py b/libraries/cherrypy/test/benchmark.py new file mode 100644 index 00000000..44dfeff1 --- /dev/null +++ b/libraries/cherrypy/test/benchmark.py @@ -0,0 +1,425 @@ +"""CherryPy Benchmark Tool + + Usage: + benchmark.py [options] + + --null: use a null Request object (to bench the HTTP server only) + --notests: start the server but do not run the tests; this allows + you to check the tested pages with a browser + --help: show this help message + --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) + --modpython: run tests via apache on 54583 (with modpython_gateway) + --ab=path: Use the ab script/executable at 'path' (see below) + --apache=path: Use the apache script/exe at 'path' (see below) + + To run the benchmarks, the Apache Benchmark tool "ab" must either be on + your system path, or specified via the --ab=path option. + + To run the modpython tests, the "apache" executable or script must be + on your system path, or provided via the --apache=path option. On some + platforms, "apache" may be called "apachectl" or "apache2ctl"--create + a symlink to them if needed. +""" + +import getopt +import os +import re +import sys +import time + +import cherrypy +from cherrypy import _cperror, _cpmodpy +from cherrypy.lib import httputil + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +AB_PATH = '' +APACHE_PATH = 'apache' +SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' + +__all__ = ['ABSession', 'Root', 'print_report', + 'run_standard_benchmarks', 'safe_threads', + 'size_report', 'thread_report', + ] + +size_cache = {} + + +class Root: + + @cherrypy.expose + def index(self): + return """<html> +<head> + <title>CherryPy Benchmark</title> +</head> +<body> + <ul> + <li><a href="hello">Hello, world! (14 byte dynamic)</a></li> + <li><a href="static/index.html">Static file (14 bytes static)</a></li> + <li><form action="sizer">Response of length: + <input type='text' name='size' value='10' /></form> + </li> + </ul> +</body> +</html>""" + + @cherrypy.expose + def hello(self): + return 'Hello, world\r\n' + + @cherrypy.expose + def sizer(self, size): + resp = size_cache.get(size, None) + if resp is None: + size_cache[size] = resp = 'X' * int(size) + return resp + + +def init(): + + cherrypy.config.update({ + 'log.error.file': '', + 'environment': 'production', + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 54583, + 'server.max_request_header_size': 0, + 'server.max_request_body_size': 0, + }) + + # Cheat mode on ;) + del cherrypy.config['tools.log_tracebacks.on'] + del cherrypy.config['tools.log_headers.on'] + del cherrypy.config['tools.trailing_slash.on'] + + appconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + } + globals().update( + app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), + ) + + +class NullRequest: + + """A null HTTP request class, returning 200 and an empty body.""" + + def __init__(self, local, remote, scheme='http'): + pass + + def close(self): + pass + + def run(self, method, path, query_string, protocol, headers, rfile): + cherrypy.response.status = '200 OK' + cherrypy.response.header_list = [('Content-Type', 'text/html'), + ('Server', 'Null CherryPy'), + ('Date', httputil.HTTPDate()), + ('Content-Length', '0'), + ] + cherrypy.response.body = [''] + return cherrypy.response + + +class NullResponse: + pass + + +class ABSession: + + """A session of 'ab', the Apache HTTP server benchmarking tool. + +Example output from ab: + +This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 +Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100 requests +Completed 200 requests +Completed 300 requests +Completed 400 requests +Completed 500 requests +Completed 600 requests +Completed 700 requests +Completed 800 requests +Completed 900 requests + + +Server Software: CherryPy/3.1beta +Server Hostname: 127.0.0.1 +Server Port: 54583 + +Document Path: /static/index.html +Document Length: 14 bytes + +Concurrency Level: 10 +Time taken for tests: 9.643867 seconds +Complete requests: 1000 +Failed requests: 0 +Write errors: 0 +Total transferred: 189000 bytes +HTML transferred: 14000 bytes +Requests per second: 103.69 [#/sec] (mean) +Time per request: 96.439 [ms] (mean) +Time per request: 9.644 [ms] (mean, across all concurrent requests) +Transfer rate: 19.08 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 2.9 0 10 +Processing: 20 94 7.3 90 130 +Waiting: 0 43 28.1 40 100 +Total: 20 95 7.3 100 130 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 100 + 75% 100 + 80% 100 + 90% 100 + 95% 100 + 98% 100 + 99% 110 + 100% 130 (longest request) +Finished 1000 requests +""" + + parse_patterns = [ + ('complete_requests', 'Completed', + br'^Complete requests:\s*(\d+)'), + ('failed_requests', 'Failed', + br'^Failed requests:\s*(\d+)'), + ('requests_per_second', 'req/sec', + br'^Requests per second:\s*([0-9.]+)'), + ('time_per_request_concurrent', 'msec/req', + br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), + ('transfer_rate', 'KB/sec', + br'^Transfer rate:\s*([0-9.]+)') + ] + + def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, + concurrency=10): + self.path = path + self.requests = requests + self.concurrency = concurrency + + def args(self): + port = cherrypy.server.socket_port + assert self.concurrency > 0 + assert self.requests > 0 + # Don't use "localhost". + # Cf + # http://mail.python.org/pipermail/python-win32/2008-March/007050.html + return ('-k -n %s -c %s http://127.0.0.1:%s%s' % + (self.requests, self.concurrency, port, self.path)) + + def run(self): + # Parse output of ab, setting attributes on self + try: + self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) + except Exception: + print(_cperror.format_exc()) + raise + + for attr, name, pattern in self.parse_patterns: + val = re.search(pattern, self.output, re.MULTILINE) + if val: + val = val.group(1) + setattr(self, attr, val) + else: + setattr(self, attr, None) + + +safe_threads = (25, 50, 100, 200, 400) +if sys.platform in ('win32',): + # For some reason, ab crashes with > 50 threads on my Win2k laptop. + safe_threads = (10, 20, 30, 40, 50) + + +def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): + sess = ABSession(path) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + avg = dict.fromkeys(attrs, 0.0) + + yield ('threads',) + names + for c in concurrency: + sess.concurrency = c + sess.run() + row = [c] + for attr in attrs: + val = getattr(sess, attr) + if val is None: + print(sess.output) + row = None + break + val = float(val) + avg[attr] += float(val) + row.append(val) + if row: + yield row + + # Add a row of averages. + yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] + + +def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), + concurrency=50): + sess = ABSession(concurrency=concurrency) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + yield ('bytes',) + names + for sz in sizes: + sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) + sess.run() + yield [sz] + [getattr(sess, attr) for attr in attrs] + + +def print_report(rows): + for row in rows: + print('') + for val in row: + sys.stdout.write(str(val).rjust(10) + ' | ') + print('') + + +def run_standard_benchmarks(): + print('') + print('Client Thread Report (1000 requests, 14 byte response body, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report()) + + print('') + print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) + + print('') + print('Size Report (1000 requests, 50 client threads, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(size_report()) + + +# modpython and other WSGI # + +def startup_modpython(req=None): + """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). + """ + if cherrypy.engine.state == cherrypy._cpengine.STOPPED: + if req: + if 'nullreq' in req.get_options(): + cherrypy.engine.request_class = NullRequest + cherrypy.engine.response_class = NullResponse + ab_opt = req.get_options().get('ab', '') + if ab_opt: + global AB_PATH + AB_PATH = ab_opt + cherrypy.engine.start() + if cherrypy.engine.state == cherrypy._cpengine.STARTING: + cherrypy.engine.wait() + return 0 # apache.OK + + +def run_modpython(use_wsgi=False): + print('Starting mod_python...') + pyopts = [] + + # Pass the null and ab=path options through Apache + if '--null' in opts: + pyopts.append(('nullreq', '')) + + if '--ab' in opts: + pyopts.append(('ab', opts['--ab'])) + + s = _cpmodpy.ModPythonServer + if use_wsgi: + pyopts.append(('wsgi.application', 'cherrypy::tree')) + pyopts.append( + ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) + handler = 'modpython_gateway::handler' + s = s(port=54583, opts=pyopts, + apache_path=APACHE_PATH, handler=handler) + else: + pyopts.append( + ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) + + try: + s.start() + run() + finally: + s.stop() + + +if __name__ == '__main__': + init() + + longopts = ['cpmodpy', 'modpython', 'null', 'notests', + 'help', 'ab=', 'apache='] + try: + switches, args = getopt.getopt(sys.argv[1:], '', longopts) + opts = dict(switches) + except getopt.GetoptError: + print(__doc__) + sys.exit(2) + + if '--help' in opts: + print(__doc__) + sys.exit(0) + + if '--ab' in opts: + AB_PATH = opts['--ab'] + + if '--notests' in opts: + # Return without stopping the server, so that the pages + # can be tested from a standard web browser. + def run(): + port = cherrypy.server.socket_port + print('You may now open http://127.0.0.1:%s%s/' % + (port, SCRIPT_NAME)) + + if '--null' in opts: + print('Using null Request object') + else: + def run(): + end = time.time() - start + print('Started in %s seconds' % end) + if '--null' in opts: + print('\nUsing null Request object') + try: + try: + run_standard_benchmarks() + except Exception: + print(_cperror.format_exc()) + raise + finally: + cherrypy.engine.exit() + + print('Starting CherryPy app server...') + + class NullWriter(object): + + """Suppresses the printing of socket errors.""" + + def write(self, data): + pass + sys.stderr = NullWriter() + + start = time.time() + + if '--cpmodpy' in opts: + run_modpython() + elif '--modpython' in opts: + run_modpython(use_wsgi=True) + else: + if '--null' in opts: + cherrypy.server.request_class = NullRequest + cherrypy.server.response_class = NullResponse + + cherrypy.engine.start_with_callback(run) + cherrypy.engine.block() diff --git a/libraries/cherrypy/test/checkerdemo.py b/libraries/cherrypy/test/checkerdemo.py new file mode 100644 index 00000000..3438bd0c --- /dev/null +++ b/libraries/cherrypy/test/checkerdemo.py @@ -0,0 +1,49 @@ +"""Demonstration app for cherrypy.checker. + +This application is intentionally broken and badly designed. +To demonstrate the output of the CherryPy Checker, simply execute +this module. +""" + +import os +import cherrypy +thisdir = os.path.dirname(os.path.abspath(__file__)) + + +class Root: + pass + + +if __name__ == '__main__': + conf = {'/base': {'tools.staticdir.root': thisdir, + # Obsolete key. + 'throw_errors': True, + }, + # This entry should be OK. + '/base/static': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on missing folder. + '/base/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'js'}, + # Warn on dir with an abs path even though we provide root. + '/base/static2': {'tools.staticdir.on': True, + 'tools.staticdir.dir': '/static'}, + # Warn on dir with a relative path with no root. + '/static3': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on unknown namespace + '/unknown': {'toobles.gzip.on': True}, + # Warn special on cherrypy.<known ns>.* + '/cpknown': {'cherrypy.tools.encode.on': True}, + # Warn on mismatched types + '/conftype': {'request.show_tracebacks': 14}, + # Warn on unknown tool. + '/web': {'tools.unknown.on': True}, + # Warn on server.* in app config. + '/app1': {'server.socket_host': '0.0.0.0'}, + # Warn on 'localhost' + 'global': {'server.socket_host': 'localhost'}, + # Warn on '[name]' + '[/extra_brackets]': {}, + } + cherrypy.quickstart(Root(), config=conf) diff --git a/libraries/cherrypy/test/fastcgi.conf b/libraries/cherrypy/test/fastcgi.conf new file mode 100644 index 00000000..e5c5163c --- /dev/null +++ b/libraries/cherrypy/test/fastcgi.conf @@ -0,0 +1,18 @@ + +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log + +DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/libraries/cherrypy/test/fcgi.conf b/libraries/cherrypy/test/fcgi.conf new file mode 100644 index 00000000..3062eb35 --- /dev/null +++ b/libraries/cherrypy/test/fcgi.conf @@ -0,0 +1,14 @@ + +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/libraries/cherrypy/test/helper.py b/libraries/cherrypy/test/helper.py new file mode 100644 index 00000000..01c5a0c0 --- /dev/null +++ b/libraries/cherrypy/test/helper.py @@ -0,0 +1,542 @@ +"""A library of helper functions for the CherryPy test suite.""" + +import datetime +import io +import logging +import os +import re +import subprocess +import sys +import time +import unittest +import warnings + +import portend +import pytest +import six + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob +from cherrypy.lib import httputil +from cherrypy.lib import gctools + +log = logging.getLogger(__name__) +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + + +class Supervisor(object): + + """Base class for modeling and controlling servers during testing.""" + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if k == 'port': + setattr(self, k, int(v)) + setattr(self, k, v) + + +def log_to_stderr(msg, level): + return sys.stderr.write(msg + os.linesep) + + +class LocalSupervisor(Supervisor): + + """Base class for modeling/controlling servers which run in the same + process. + + When the server side runs in a different process, start/stop can dump all + state between each test module easily. When the server side runs in the + same process as the client, however, we have to do a bit more work to + ensure config and mounted apps are reset between tests. + """ + + using_apache = False + using_wsgi = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + cherrypy.server.httpserver = self.httpserver_class + + # This is perhaps the wrong place for this call but this is the only + # place that i've found so far that I KNOW is early enough to set this. + cherrypy.config.update({'log.screen': False}) + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + def start(self, modulename=None): + """Load and start the HTTP server.""" + if modulename: + # Unhook httpserver so cherrypy.server.start() creates a new + # one (with config from setup_server, if declared). + cherrypy.server.httpserver = None + + cherrypy.engine.start() + + self.sync_apps() + + def sync_apps(self): + """Tell the server about any apps which the setup functions mounted.""" + pass + + def stop(self): + td = getattr(self, 'teardown', None) + if td: + td() + + cherrypy.engine.exit() + + servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {}))) + for name, server in servers_copy: + server.unsubscribe() + del cherrypy.servers[name] + + +class NativeServerSupervisor(LocalSupervisor): + + """Server supervisor for the builtin HTTP server.""" + + httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' + using_apache = False + using_wsgi = False + + def __str__(self): + return 'Builtin HTTP Server on %s:%s' % (self.host, self.port) + + +class LocalWSGISupervisor(LocalSupervisor): + + """Server supervisor for the builtin WSGI server.""" + + httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' + using_apache = False + using_wsgi = True + + def __str__(self): + return 'Builtin WSGI Server on %s:%s' % (self.host, self.port) + + def sync_apps(self): + """Hook a new WSGI app into the origin server.""" + cherrypy.server.httpserver.wsgi_app = self.get_app() + + def get_app(self, app=None): + """Obtain a new (decorated) WSGI app to hook into the origin server.""" + if app is None: + app = cherrypy.tree + + if self.validate: + try: + from wsgiref import validate + except ImportError: + warnings.warn( + 'Error importing wsgiref. The validator will not run.') + else: + # wraps the app in the validator + app = validate.validator(app) + + return app + + +def get_cpmodpy_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_cpmodpy + return sup + + +def get_modpygw_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_modpython_gateway + sup.using_wsgi = True + return sup + + +def get_modwsgi_supervisor(**options): + from cherrypy.test import modwsgi + return modwsgi.ModWSGISupervisor(**options) + + +def get_modfcgid_supervisor(**options): + from cherrypy.test import modfcgid + return modfcgid.ModFCGISupervisor(**options) + + +def get_modfastcgi_supervisor(**options): + from cherrypy.test import modfastcgi + return modfastcgi.ModFCGISupervisor(**options) + + +def get_wsgi_u_supervisor(**options): + cherrypy.server.wsgi_version = ('u', 0) + return LocalWSGISupervisor(**options) + + +class CPWebCase(webtest.WebCase): + + script_name = '' + scheme = 'http' + + available_servers = {'wsgi': LocalWSGISupervisor, + 'wsgi_u': get_wsgi_u_supervisor, + 'native': NativeServerSupervisor, + 'cpmodpy': get_cpmodpy_supervisor, + 'modpygw': get_modpygw_supervisor, + 'modwsgi': get_modwsgi_supervisor, + 'modfcgid': get_modfcgid_supervisor, + 'modfastcgi': get_modfastcgi_supervisor, + } + default_server = 'wsgi' + + @classmethod + def _setup_server(cls, supervisor, conf): + v = sys.version.split()[0] + log.info('Python version used to run this test script: %s' % v) + log.info('CherryPy version: %s' % cherrypy.__version__) + if supervisor.scheme == 'https': + ssl = ' (ssl)' + else: + ssl = '' + log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl)) + log.info('PID: %s' % os.getpid()) + + cherrypy.server.using_apache = supervisor.using_apache + cherrypy.server.using_wsgi = supervisor.using_wsgi + + if sys.platform[:4] == 'java': + cherrypy.config.update({'server.nodelay': False}) + + if isinstance(conf, text_or_bytes): + parser = cherrypy.lib.reprconf.Parser() + conf = parser.dict_from_file(conf).get('global', {}) + else: + conf = conf or {} + baseconf = conf.copy() + baseconf.update({'server.socket_host': supervisor.host, + 'server.socket_port': supervisor.port, + 'server.protocol_version': supervisor.protocol, + 'environment': 'test_suite', + }) + if supervisor.scheme == 'https': + # baseconf['server.ssl_module'] = 'builtin' + baseconf['server.ssl_certificate'] = serverpem + baseconf['server.ssl_private_key'] = serverpem + + # helper must be imported lazily so the coverage tool + # can run against module-level statements within cherrypy. + # Also, we have to do "from cherrypy.test import helper", + # exactly like each test module does, because a relative import + # would stick a second instance of webtest in sys.modules, + # and we wouldn't be able to globally override the port anymore. + if supervisor.scheme == 'https': + webtest.WebCase.HTTP_CONN = HTTPSConnection + return baseconf + + @classmethod + def setup_class(cls): + '' + # Creates a server + conf = { + 'scheme': 'http', + 'protocol': 'HTTP/1.1', + 'port': 54583, + 'host': '127.0.0.1', + 'validate': False, + 'server': 'wsgi', + } + supervisor_factory = cls.available_servers.get( + conf.get('server', 'wsgi')) + if supervisor_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + supervisor = supervisor_factory(**conf) + + # Copied from "run_test_suite" + cherrypy.config.reset() + baseconf = cls._setup_server(supervisor, conf) + cherrypy.config.update(baseconf) + setup_client() + + if hasattr(cls, 'setup_server'): + # Clear the cherrypy tree and clear the wsgi server so that + # it can be updated with the new root + cherrypy.tree = cherrypy._cptree.Tree() + cherrypy.server.httpserver = None + cls.setup_server() + # Add a resource for verifying there are no refleaks + # to *every* test class. + cherrypy.tree.mount(gctools.GCRoot(), '/gc') + cls.do_gc_test = True + supervisor.start(cls.__module__) + + cls.supervisor = supervisor + + @classmethod + def teardown_class(cls): + '' + if hasattr(cls, 'setup_server'): + cls.supervisor.stop() + + do_gc_test = False + + def test_gc(self): + if not self.do_gc_test: + return + + self.getPage('/gc/stats') + try: + self.assertBody('Statistics:') + except Exception: + 'Failures occur intermittently. See #1420' + + def prefix(self): + return self.script_name.rstrip('/') + + def base(self): + if ((self.scheme == 'http' and self.PORT == 80) or + (self.scheme == 'https' and self.PORT == 443)): + port = '' + else: + port = ':%s' % self.PORT + + return '%s://%s%s%s' % (self.scheme, self.HOST, port, + self.script_name.rstrip('/')) + + def exit(self): + sys.exit() + + def getPage(self, url, headers=None, method='GET', body=None, + protocol=None, raise_subcls=None): + """Open the url. Return status, headers, body. + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + if self.script_name: + url = httputil.urljoin(self.script_name, url) + return webtest.WebCase.getPage(self, url, headers, method, body, + protocol, raise_subcls) + + def skip(self, msg='skipped '): + pytest.skip(msg) + + def assertErrorPage(self, status, message=None, pattern=''): + """Compare the response body with a built in error page. + + The function will optionally look for the regexp pattern, + within the exception embedded in the error page.""" + + # This will never contain a traceback + page = cherrypy._cperror.get_error_page(status, message=message) + + # First, test the response body without checking the traceback. + # Stick a match-all group (.*) in to grab the traceback. + def esc(text): + return re.escape(ntob(text)) + epage = re.escape(page) + epage = epage.replace( + esc('<pre id="traceback"></pre>'), + esc('<pre id="traceback">') + b'(.*)' + esc('</pre>')) + m = re.match(epage, self.body, re.DOTALL) + if not m: + self._handlewebError( + 'Error page does not match; expected:\n' + page) + return + + # Now test the pattern against the traceback + if pattern is None: + # Special-case None to mean that there should be *no* traceback. + if m and m.group(1): + self._handlewebError('Error page contains traceback') + else: + if (m is None) or ( + not re.search(ntob(re.escape(pattern), self.encoding), + m.group(1))): + msg = 'Error page does not contain %s in traceback' + self._handlewebError(msg % repr(pattern)) + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +def _test_method_sorter(_, x, y): + """Monkeypatch the test sorter to always run test_gc last in each suite.""" + if x == 'test_gc': + return 1 + if y == 'test_gc': + return -1 + if x > y: + return 1 + if x < y: + return -1 + return 0 + + +unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter + + +def setup_client(): + """Set up the WebCase classes to match the server's socket settings.""" + webtest.WebCase.PORT = cherrypy.server.socket_port + webtest.WebCase.HOST = cherrypy.server.socket_host + if cherrypy.server.ssl_certificate: + CPWebCase.scheme = 'https' + +# --------------------------- Spawning helpers --------------------------- # + + +class CPProcess(object): + + pid_file = os.path.join(thisdir, 'test.pid') + config_file = os.path.join(thisdir, 'test.conf') + config_template = """[global] +server.socket_host: '%(host)s' +server.socket_port: %(port)s +checker.on: False +log.screen: False +log.error_file: r'%(error_log)s' +log.access_file: r'%(access_log)s' +%(ssl)s +%(extra)s +""" + error_log = os.path.join(thisdir, 'test.error.log') + access_log = os.path.join(thisdir, 'test.access.log') + + def __init__(self, wait=False, daemonize=False, ssl=False, + socket_host=None, socket_port=None): + self.wait = wait + self.daemonize = daemonize + self.ssl = ssl + self.host = socket_host or cherrypy.server.socket_host + self.port = socket_port or cherrypy.server.socket_port + + def write_conf(self, extra=''): + if self.ssl: + serverpem = os.path.join(thisdir, 'test.pem') + ssl = """ +server.ssl_certificate: r'%s' +server.ssl_private_key: r'%s' +""" % (serverpem, serverpem) + else: + ssl = '' + + conf = self.config_template % { + 'host': self.host, + 'port': self.port, + 'error_log': self.error_log, + 'access_log': self.access_log, + 'ssl': ssl, + 'extra': extra, + } + with io.open(self.config_file, 'w', encoding='utf-8') as f: + f.write(six.text_type(conf)) + + def start(self, imports=None): + """Start cherryd in a subprocess.""" + portend.free(self.host, self.port, timeout=1) + + args = [ + '-m', + 'cherrypy', + '-c', self.config_file, + '-p', self.pid_file, + ] + r""" + Command for running cherryd server with autoreload enabled + + Using + + ``` + ['-c', + "__requires__ = 'CherryPy'; \ + import pkg_resources, re, sys; \ + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ + sys.exit(\ + pkg_resources.load_entry_point(\ + 'CherryPy', 'console_scripts', 'cherryd')())"] + ``` + + doesn't work as it's impossible to reconstruct the `-c`'s contents. + Ref: https://github.com/cherrypy/cherrypy/issues/1545 + """ + + if not isinstance(imports, (list, tuple)): + imports = [imports] + for i in imports: + if i: + args.append('-i') + args.append(i) + + if self.daemonize: + args.append('-d') + + env = os.environ.copy() + # Make sure we import the cherrypy package in which this module is + # defined. + grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) + if env.get('PYTHONPATH', ''): + env['PYTHONPATH'] = os.pathsep.join( + (grandparentdir, env['PYTHONPATH'])) + else: + env['PYTHONPATH'] = grandparentdir + self._proc = subprocess.Popen([sys.executable] + args, env=env) + if self.wait: + self.exit_code = self._proc.wait() + else: + portend.occupied(self.host, self.port, timeout=5) + + # Give the engine a wee bit more time to finish STARTING + if self.daemonize: + time.sleep(2) + else: + time.sleep(1) + + def get_pid(self): + if self.daemonize: + return int(open(self.pid_file, 'rb').read()) + return self._proc.pid + + def join(self): + """Wait for the process to exit.""" + if self.daemonize: + return self._join_daemon() + self._proc.wait() + + def _join_daemon(self): + try: + try: + # Mac, UNIX + os.wait() + except AttributeError: + # Windows + try: + pid = self.get_pid() + except IOError: + # Assume the subprocess deleted the pidfile on shutdown. + pass + else: + os.waitpid(pid, 0) + except OSError: + x = sys.exc_info()[1] + if x.args != (10, 'No child processes'): + raise diff --git a/libraries/cherrypy/test/logtest.py b/libraries/cherrypy/test/logtest.py new file mode 100644 index 00000000..ed8f1540 --- /dev/null +++ b/libraries/cherrypy/test/logtest.py @@ -0,0 +1,228 @@ +"""logtest, a unittest.TestCase helper for testing log output.""" + +import sys +import time +from uuid import UUID + +import six + +from cherrypy._cpcompat import text_or_bytes, ntob + + +try: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class LogCase(object): + + """unittest.TestCase mixin for testing log messages. + + logfile: a filename for the desired log. Yes, I know modes are evil, + but it makes the test functions so much cleaner to set this once. + + lastmarker: the last marker in the log. This can be used to search for + messages since the last marker. + + markerPrefix: a string with which to prefix log markers. This should be + unique enough from normal log output to use for marker identification. + """ + + logfile = None + lastmarker = None + markerPrefix = b'test suite marker: ' + + def _handleLogError(self, msg, data, marker, pattern): + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = (' Show: ' + '[L]og [M]arker [P]attern; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ') + sys.stdout.write(p + ' ') + # ARGH + sys.stdout.flush() + while True: + i = getchar().upper() + if i not in 'MPLIRX': + continue + print(i.upper()) # Also prints new line + if i == 'L': + for x, line in enumerate(data): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r ') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r ') + if m == 'q': + break + print(line.rstrip()) + elif i == 'M': + print(repr(marker or self.lastmarker)) + elif i == 'P': + print(repr(pattern)) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise self.failureException(msg) + elif i == 'X': + self.exit() + sys.stdout.write(p + ' ') + + def exit(self): + sys.exit() + + def emptyLog(self): + """Overwrite self.logfile with 0 bytes.""" + open(self.logfile, 'wb').write('') + + def markLog(self, key=None): + """Insert a marker line into the log and set self.lastmarker.""" + if key is None: + key = str(time.time()) + self.lastmarker = key + + open(self.logfile, 'ab+').write( + ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8')) + + def _read_marked_region(self, marker=None): + """Return lines from self.logfile in the marked region. + + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be returned. + """ +# Give the logger time to finish writing? +# time.sleep(0.5) + + logfile = self.logfile + marker = marker or self.lastmarker + if marker is None: + return open(logfile, 'rb').readlines() + + if isinstance(marker, six.text_type): + 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 + return data + + def assertInLog(self, line, marker=None): + """Fail if the given (partial) line is not in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + return + msg = '%r not found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertNotInLog(self, line, marker=None): + """Fail if the given (partial) line is in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + msg = '%r found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertValidUUIDv4(self, marker=None): + """Fail if the given UUIDv4 is not valid. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + data = [ + chunk.decode('utf-8').rstrip('\n').rstrip('\r') + for chunk in data + ] + for log_chunk in data: + try: + uuid_log = data[-1] + uuid_obj = UUID(uuid_log, version=4) + except (TypeError, ValueError): + pass # it might be in other chunk + else: + if str(uuid_obj) == uuid_log: + return + msg = '%r is not a valid UUIDv4' % uuid_log + self._handleLogError(msg, data, marker, log_chunk) + + msg = 'UUIDv4 not found in log' + self._handleLogError(msg, data, marker, log_chunk) + + def assertLog(self, sliceargs, lines, marker=None): + """Fail if log.readlines()[sliceargs] is not contained in 'lines'. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + if isinstance(sliceargs, int): + # Single arg. Use __getitem__ and allow lines to be str or list. + if isinstance(lines, (tuple, list)): + lines = lines[0] + if isinstance(lines, six.text_type): + lines = lines.encode('utf-8') + if lines not in data[sliceargs]: + msg = '%r not found on log line %r' % (lines, sliceargs) + self._handleLogError( + msg, + [data[sliceargs], '--EXTRA CONTEXT--'] + data[ + sliceargs + 1:sliceargs + 6], + marker, + 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.") + + start, stop = sliceargs + for line, logline in zip(lines, data[start:stop]): + if isinstance(line, six.text_type): + line = line.encode('utf-8') + if line not in logline: + msg = '%r not found in log' % line + self._handleLogError(msg, data[start:stop], marker, line) diff --git a/libraries/cherrypy/test/modfastcgi.py b/libraries/cherrypy/test/modfastcgi.py new file mode 100644 index 00000000..79ec3d18 --- /dev/null +++ b/libraries/cherrypy/test/modfastcgi.py @@ -0,0 +1,136 @@ +"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. + +To autostart fastcgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +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. +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. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(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) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'apache2ctl' +CONF_PATH = 'fastcgi.conf' + +conf_fastcgi = """ +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog %(root)s/mod_fastcgi.error.log + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +def erase_script_name(environ, start_response): + environ['SCRIPT_NAME'] = '' + return cherrypy.tree(environ, start_response) + + +class ModFCGISupervisor(helper.LocalWSGISupervisor): + + httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' + using_apache = True + using_wsgi = True + template = conf_fastcgi + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + 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) + cherrypy.server.socket_port = 4000 + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + cherrypy.engine.start() + self.sync_apps() + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + 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: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalWSGISupervisor.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app( + erase_script_name) diff --git a/libraries/cherrypy/test/modfcgid.py b/libraries/cherrypy/test/modfcgid.py new file mode 100644 index 00000000..d101bd67 --- /dev/null +++ b/libraries/cherrypy/test/modfcgid.py @@ -0,0 +1,124 @@ +"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. + +To autostart fcgid, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +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. +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. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(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) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'fcgi.conf' + +conf_fcgid = """ +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +class ModFCGISupervisor(helper.LocalSupervisor): + + using_apache = True + using_wsgi = True + template = conf_fcgid + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + 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 + helper.LocalServer.start(self, modulename) + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + 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: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalServer.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app() diff --git a/libraries/cherrypy/test/modpy.py b/libraries/cherrypy/test/modpy.py new file mode 100644 index 00000000..7c288d2c --- /dev/null +++ b/libraries/cherrypy/test/modpy.py @@ -0,0 +1,164 @@ +"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. + +To autostart modpython, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + +If you wish to test the WSGI interface instead of our _cpmodpy interface, +you also need the 'modpython_gateway' module at: +http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +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. +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. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(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) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'test_mp.conf' + +conf_modpython_gateway = """ +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::wsgisetup +PythonOption testmod %(modulename)s +PythonHandler modpython_gateway::handler +PythonOption wsgi.application cherrypy::tree +PythonOption socket_host %(host)s +PythonDebug On +""" + +conf_cpmodpy = """ +# Apache2 server conf file for testing CherryPy with _cpmodpy. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::cpmodpysetup +PythonHandler cherrypy._cpmodpy::handler +PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server +PythonOption socket_host %(host)s +PythonDebug On +""" + + +class ModPythonSupervisor(helper.Supervisor): + + using_apache = True + using_wsgi = False + template = None + + def __str__(self): + return 'ModPython Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + 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: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def wsgisetup(req): + global loaded + if not loaded: + 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'], + }) + + 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 + + +def cpmodpysetup(req): + global loaded + if not loaded: + 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'], + }) + from mod_python import apache + return apache.OK diff --git a/libraries/cherrypy/test/modwsgi.py b/libraries/cherrypy/test/modwsgi.py new file mode 100644 index 00000000..f558e223 --- /dev/null +++ b/libraries/cherrypy/test/modwsgi.py @@ -0,0 +1,154 @@ +"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. + +To autostart modwsgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + + +KNOWN BUGS +========== + +##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. +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. +##4. Apache replaces status "reason phrases" automatically. For example, +## CherryPy may set "304 Not modified" but Apache will write out +## "304 Not Modified" (capital "M"). +##5. Apache does not allow custom error codes as per the spec. +##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the +## Request-URI too early. +7. mod_wsgi will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. When responding with 204 No Content, mod_wsgi adds a Content-Length + header for you. +9. When an error is raised, mod_wsgi has no facility for printing a + traceback as the response content (it's sent to the Apache log instead). +10. Startup and shutdown of Apache when running mod_wsgi seems slow. +""" + +import os +import re +import sys +import time + +import portend + +from cheroot.test import webtest + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.abspath(os.path.dirname(__file__)) + + +def read_process(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) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +if sys.platform == 'win32': + APACHE_PATH = 'httpd' +else: + APACHE_PATH = 'apache' + +CONF_PATH = 'test_mw.conf' + +conf_modwsgi = r""" +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s + +AllowEncodedSlashes On +LoadModule rewrite_module modules/mod_rewrite.so +RewriteEngine on +RewriteMap escaping int:escape + +LoadModule log_config_module modules/mod_log_config.so +LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined +CustomLog "%(curdir)s/apache.access.log" combined +ErrorLog "%(curdir)s/apache.error.log" +LogLevel debug + +LoadModule wsgi_module modules/mod_wsgi.so +LoadModule env_module modules/mod_env.so + +WSGIScriptAlias / "%(curdir)s/modwsgi.py" +SetEnv testmod %(testmod)s +""" # noqa E501 + + +class ModWSGISupervisor(helper.Supervisor): + + """Server Controller for ModWSGI and CherryPy.""" + + using_apache = True + using_wsgi = True + template = conf_modwsgi + + def __str__(self): + return 'ModWSGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + 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: + 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) + time.sleep(1) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def application(environ, start_response): + global loaded + if not loaded: + loaded = True + 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, + }) + return cherrypy.tree(environ, start_response) diff --git a/libraries/cherrypy/test/sessiondemo.py b/libraries/cherrypy/test/sessiondemo.py new file mode 100644 index 00000000..8226c1b9 --- /dev/null +++ b/libraries/cherrypy/test/sessiondemo.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +"""A session demonstration app.""" + +import calendar +from datetime import datetime +import sys + +import six + +import cherrypy +from cherrypy.lib import sessions + + +page = """ +<html> +<head> +<style type='text/css'> +table { border-collapse: collapse; border: 1px solid #663333; } +th { text-align: right; background-color: #663333; color: white; padding: 0.5em; } +td { white-space: pre-wrap; font-family: monospace; padding: 0.5em; + border: 1px solid #663333; } +.warn { font-family: serif; color: #990000; } +</style> +<script type="text/javascript"> +<!-- +function twodigit(d) { return d < 10 ? "0" + d : d; } +function formattime(t) { + var month = t.getUTCMonth() + 1; + var day = t.getUTCDate(); + var year = t.getUTCFullYear(); + var hours = t.getUTCHours(); + var minutes = t.getUTCMinutes(); + return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " + + hours + ":" + twodigit(minutes) + " UTC"); +} + +function interval(s) { + // Return the given interval (in seconds) as an English phrase + var seconds = s %% 60; + s = Math.floor(s / 60); + var minutes = s %% 60; + s = Math.floor(s / 60); + var hours = s %% 24; + var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds); + var days = Math.floor(s / 24); + if (days != 0) v = days + ' days, ' + v; + return v; +} + +var fudge_seconds = 5; + +function init() { + // Set the content of the 'btime' cell. + var currentTime = new Date(); + var bunixtime = Math.floor(currentTime.getTime() / 1000); + + var v = formattime(currentTime); + v += " (Unix time: " + bunixtime + ")"; + + var diff = Math.abs(%(serverunixtime)s - bunixtime); + if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>"; + + document.getElementById('btime').innerHTML = v; + + // Warn if response cookie expires is not close to one hour in the future. + // Yes, we want this to happen when wit hit the 'Expire' link, too. + var expires = Date.parse("%(expires)s") / 1000; + var onehour = (60 * 60); + if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) { + diff = Math.floor(expires - bunixtime); + if (expires > (bunixtime + onehour)) { + var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future."; + } else { + var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past."; + } + document.getElementById('respcookiewarn').innerHTML = msg; + } +} +//--> +</script> +</head> + +<body onload='init()'> +<h2>Session Demo</h2> +<p>Reload this page. The session ID should not change from one reload to the next</p> +<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p> +<table> + <tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr> + <tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr> + <tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr> + <tr><th>Session Data</th><td>%(sessiondata)s</td></tr> + <tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr> + <tr><th>Browser Time</th><td id='btime'> </td></tr> + <tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr> + <tr><th>Python Version:</th><td>%(pyversion)s</td></tr> +</table> +</body></html> +""" # noqa E501 + + +class Root(object): + + 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.') + if cherrypy.session.missing: + changemsg.append( + 'Created new session due to missing ' + '(expired or malicious) session.') + if cherrypy.session.regenerated: + changemsg.append('Application generated a new session.') + + try: + expires = cherrypy.response.cookie['session_id']['expires'] + except KeyError: + expires = '' + + return page % { + 'sessionid': cherrypy.session.id, + 'changemsg': '<br>'.join(changemsg), + 'respcookie': cherrypy.response.cookie.output(), + 'reqcookie': cherrypy.request.cookie.output(), + 'sessiondata': list(six.iteritems(cherrypy.session)), + 'servertime': ( + datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' + ), + 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), + 'cpversion': cherrypy.__version__, + 'pyversion': sys.version, + 'expires': expires, + } + + @cherrypy.expose + def index(self): + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'green' + return self.page() + + @cherrypy.expose + def expire(self): + sessions.expire() + return self.page() + + @cherrypy.expose + def regen(self): + cherrypy.session.regenerate() + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'yellow' + return self.page() + + +if __name__ == '__main__': + cherrypy.config.update({ + # 'environment': 'production', + 'log.screen': True, + 'tools.sessions.on': True, + }) + cherrypy.quickstart(Root()) diff --git a/libraries/cherrypy/test/static/404.html b/libraries/cherrypy/test/static/404.html new file mode 100644 index 00000000..01b17b09 --- /dev/null +++ b/libraries/cherrypy/test/static/404.html @@ -0,0 +1,5 @@ +<html> + <body> + <h1>I couldn't find that thing you were looking for!</h1> + </body> +</html> diff --git a/libraries/cherrypy/test/static/dirback.jpg b/libraries/cherrypy/test/static/dirback.jpg new file mode 100644 index 0000000000000000000000000000000000000000..80403dc227c19b9192158420f5288121e7f2669a GIT binary patch literal 16585 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 Sse&CiQ+RTS)_Y=q`u_k!0j`Vy literal 0 HcmV?d00001 diff --git a/libraries/cherrypy/test/static/index.html b/libraries/cherrypy/test/static/index.html new file mode 100644 index 00000000..a5c19667 --- /dev/null +++ b/libraries/cherrypy/test/static/index.html @@ -0,0 +1 @@ +Hello, world diff --git a/libraries/cherrypy/test/style.css b/libraries/cherrypy/test/style.css new file mode 100644 index 00000000..b266e93d --- /dev/null +++ b/libraries/cherrypy/test/style.css @@ -0,0 +1 @@ +Dummy stylesheet diff --git a/libraries/cherrypy/test/test.pem b/libraries/cherrypy/test/test.pem new file mode 100644 index 00000000..47a47042 --- /dev/null +++ b/libraries/cherrypy/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/libraries/cherrypy/test/test_auth_basic.py b/libraries/cherrypy/test/test_auth_basic.py new file mode 100644 index 00000000..d7e69a9b --- /dev/null +++ b/libraries/cherrypy/test/test_auth_basic.py @@ -0,0 +1,135 @@ +# This file is part of CherryPy <http://www.cherrypy.org/> +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +from hashlib import md5 + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import auth_basic +from cherrypy.test import helper + + +class BasicAuthTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'This is public.' + + class BasicProtected: + + @cherrypy.expose + def index(self): + 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) + + class BasicProtected2_u: + + @cherrypy.expose + def index(self): + 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()} + + def checkpasshash(realm, user, password): + p = userhashdict.get(user) + return p and p == md5(ntob(password)).hexdigest() or False + + def checkpasshash_u(realm, user, password): + p = userhashdict_u.get(user) + 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 + }, + '/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', + }, + } + + root = Root() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + root.basic2_u = BasicProtected2_u() + 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.') + + def testBasic(self): + self.getPage('/basic/') + self.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + 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.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic2/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + 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.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) + self.assertStatus(401) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) + self.assertStatus('200 OK') + self.assertBody("Hello xюзер, you've been authorized.") diff --git a/libraries/cherrypy/test/test_auth_digest.py b/libraries/cherrypy/test/test_auth_digest.py new file mode 100644 index 00000000..512e39a5 --- /dev/null +++ b/libraries/cherrypy/test/test_auth_digest.py @@ -0,0 +1,134 @@ +# This file is part of CherryPy <http://www.cherrypy.org/> +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +import six + + +import cherrypy +from cherrypy.lib import auth_digest +from cherrypy._cpcompat import ntob + +from cherrypy.test import helper + + +def _fetch_users(): + 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.' + + 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'}} + + 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.' + + def _test_parametric_digest(self, username, realm): + 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, + )) + assert len(www_auth_digest) == 1, msg + + items = www_auth_digest[0][-1][7:].split(', ') + tokens = {} + for item in items: + 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"' + + 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"') + + encoded_user = username + if six.PY3: + 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', + ) + 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', + ) + 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') + assert self.status_code == 401 + + def test_ascii_user(self): + 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' + assert self.body == ntob( + "Hello ☃йюзер, you've been authorized.", 'utf-8', + ) + + def test_wrong_scheme(self): + basic_auth = { + 'Authorization': 'Basic foo:bar', + } + self.getPage('/digest/', headers=list(basic_auth.items())) + assert self.status_code == 401 diff --git a/libraries/cherrypy/test/test_bus.py b/libraries/cherrypy/test/test_bus.py new file mode 100644 index 00000000..6026b47e --- /dev/null +++ b/libraries/cherrypy/test/test_bus.py @@ -0,0 +1,274 @@ +import threading +import time +import unittest + +from cherrypy.process import wspbus + + +msg = 'Listener %d on channel %s: %s.' + + +class PublishSubscribeTests(unittest.TestCase): + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_builtin_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + for channel in b.listeners: + for index, priority in enumerate([100, 50, 0, 51]): + b.subscribe(channel, + self.get_listener(channel, index), priority) + + for channel in b.listeners: + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) + b.publish(channel, arg=79347) + expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) + + self.assertEqual(self.responses, expected) + + def test_custom_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + custom_listeners = ('hugh', 'louis', 'dewey') + for channel in custom_listeners: + for index, priority in enumerate([None, 10, 60, 40]): + b.subscribe(channel, + self.get_listener(channel, index), priority) + + for channel in custom_listeners: + b.publish(channel, 'ah so') + expected.extend([msg % (i, channel, 'ah so') + for i in (1, 3, 0, 2)]) + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) + + self.assertEqual(self.responses, expected) + + def test_listener_errors(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + channels = [c for c in b.listeners if c != 'log'] + + for channel in channels: + b.subscribe(channel, self.get_listener(channel, 1)) + # This will break since the lambda takes no args. + b.subscribe(channel, lambda: None, priority=20) + + for channel in channels: + self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) + expected.append(msg % (1, channel, 123)) + + self.assertEqual(self.responses, expected) + + +class BusMethodTests(unittest.TestCase): + + def log(self, bus): + self._log_entries = [] + + def logit(msg, level): + self._log_entries.append(msg) + bus.subscribe('log', logit) + + def assertLog(self, entries): + self.assertEqual(self._log_entries, entries) + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_start(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('start', self.get_listener('start', index)) + + b.start() + try: + # The start method MUST call all 'start' listeners. + self.assertEqual( + set(self.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) + self.assertEqual(b.state, b.states.STARTED) + # The start method MUST log its states. + self.assertLog(['Bus STARTING', 'Bus STARTED']) + finally: + # Exit so the atexit handler doesn't complain. + b.exit() + + def test_stop(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + + b.stop() + + # The stop method MUST call all 'stop' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)])) + # The stop method MUST move the state to STOPPED + self.assertEqual(b.state, b.states.STOPPED) + # The stop method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED']) + + def test_graceful(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('graceful', self.get_listener('graceful', index)) + + b.graceful() + + # The graceful method MUST call all 'graceful' listeners. + self.assertEqual( + set(self.responses), + set([msg % (i, 'graceful', None) for i in range(num)])) + # The graceful method MUST log its states. + self.assertLog(['Bus graceful']) + + def test_exit(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + b.subscribe('exit', self.get_listener('exit', index)) + + b.exit() + + # The exit method MUST call all 'stop' listeners, + # and then all 'exit' listeners. + self.assertEqual(set(self.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 + self.assertEqual(b.state, b.states.EXITING) + # The exit method MUST log its states. + self.assertLog( + ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + + def test_wait(self): + b = wspbus.Bus() + + def f(method): + time.sleep(0.2) + getattr(b, method)() + + for method, states in [('start', [b.states.STARTED]), + ('stop', [b.states.STOPPED]), + ('start', + [b.states.STARTING, b.states.STARTED]), + ('exit', [b.states.EXITING]), + ]: + threading.Thread(target=f, args=(method,)).start() + b.wait(states) + + # The wait method MUST wait for the given state(s). + if b.state not in states: + self.fail('State %r not in %r' % (b.state, states)) + + def test_block(self): + b = wspbus.Bus() + self.log(b) + + def f(): + time.sleep(0.2) + b.exit() + + def g(): + time.sleep(0.4) + threading.Thread(target=f).start() + threading.Thread(target=g).start() + threads = [t for t in threading.enumerate() if not t.daemon] + self.assertEqual(len(threads), 3) + + b.block() + + # The block method MUST wait for the EXITING state. + self.assertEqual(b.state, b.states.EXITING) + # The block method MUST wait for ALL non-main, non-daemon threads to + # finish. + threads = [t for t in threading.enumerate() if not t.daemon] + self.assertEqual(len(threads), 1) + # The last message will mention an indeterminable thread name; ignore + # it + self.assertEqual(self._log_entries[:-1], + ['Bus STOPPING', 'Bus STOPPED', + 'Bus EXITING', 'Bus EXITED', + 'Waiting for child threads to terminate...']) + + def test_start_with_callback(self): + b = wspbus.Bus() + self.log(b) + try: + events = [] + + def f(*args, **kwargs): + events.append(('f', args, kwargs)) + + def g(): + events.append('g') + b.subscribe('start', g) + b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) + # Give wait() time to run f() + time.sleep(0.2) + + # The callback method MUST wait for the STARTED state. + self.assertEqual(b.state, b.states.STARTED) + # The callback method MUST run after all start methods. + self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})]) + finally: + b.exit() + + def test_log(self): + b = wspbus.Bus() + self.log(b) + self.assertLog([]) + + # Try a normal message. + expected = [] + for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: + b.log(msg) + expected.append(msg) + self.assertLog(expected) + + # Try an error message + try: + foo + except NameError: + b.log('You are lost and gone forever', traceback=True) + lastmsg = self._log_entries[-1] + if 'Traceback' not in lastmsg or 'NameError' not in lastmsg: + self.fail('Last log message %r did not contain ' + 'the expected traceback.' % lastmsg) + else: + self.fail('NameError was not raised as expected.') + + +if __name__ == '__main__': + unittest.main() diff --git a/libraries/cherrypy/test/test_caching.py b/libraries/cherrypy/test/test_caching.py new file mode 100644 index 00000000..1a6ed4f2 --- /dev/null +++ b/libraries/cherrypy/test/test_caching.py @@ -0,0 +1,392 @@ +import datetime +from itertools import count +import os +import threading +import time + +from six.moves import range +from six.moves import urllib + +import pytest + +import cherrypy +from cherrypy.lib import httputil + +from cherrypy.test import helper + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +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;' +) + + +class CacheTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + @cherrypy.config(**{'tools.caching.on': True}) + class Root: + + def __init__(self): + self.counter = 0 + self.control_counter = 0 + self.longlock = threading.Lock() + + @cherrypy.expose + def index(self): + self.counter += 1 + msg = 'visit #%s' % self.counter + return msg + + @cherrypy.expose + def control(self): + self.control_counter += 1 + return 'visit #%s' % self.control_counter + + @cherrypy.expose + def a_gif(self): + cherrypy.response.headers[ + 'Last-Modified'] = httputil.HTTPDate() + return gif_bytes + + @cherrypy.expose + def long_process(self, seconds='1'): + try: + self.longlock.acquire() + time.sleep(float(seconds)) + finally: + self.longlock.release() + 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') + ], + }) + 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, + }) + class UnCached(object): + + @cherrypy.expose + @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.expose + def dynamic(self): + 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' + return "Hi, I'm cacheable." + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': 86400}) + def specific(self): + 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()}) + 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 + }) + 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}) + + def testCaching(self): + elapsed = 0.0 + for trial in range(10): + self.getPage('/') + # The response should be the same every time, + # except for the Age response header. + self.assertBody('visit #1') + if trial != 0: + age = int(self.assertHeader('Age')) + self.assert_(age >= elapsed) + elapsed = age + + # POST, PUT, DELETE should not be cached. + 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') + # The previous request should have invalidated the cache, + # so this request will recalc the response. + 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') + + # 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') + + # 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') + + # 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') + + def testVaryHeader(self): + 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/') + 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') + + # test exceptions for bad time values + self.getPage('/expires/wrongtype') + self.assertStatus(500) + 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') + + # 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!') + # the Cache-Control header should be untouched + 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') + # 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') + + # 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') + + # 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!') + # 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') + + 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')] + content_len = None + + for _ in range(3): + 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') + + 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') + # non-gzipped version should have a different content length + 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') + + 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.assertStatus(200) + self.assertBody(gif_bytes) + lm1 = self.assertHeader('Last-Modified') + + # this request should get the cached copy. + self.getPage('/a.gif') + self.assertStatus(200) + self.assertBody(gif_bytes) + 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.assertStatus(304) + self.assertNoHeader('Last-Modified') + if not getattr(cherrypy.server, 'using_apache', False): + self.assertHeader('Age') + + @pytest.mark.xfail(reason='#1536') + def test_antistampede(self): + SECONDS = 4 + 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.assertStatus(200) + + start = datetime.datetime.now() + + def run(): + self.getPage(slow_url) + # The response should be the same every time + self.assertBody('success!') + ts = [threading.Thread(target=run) for i in range(100)] + for t in ts: + t.start() + for t in ts: + t.join() + finish = datetime.datetime.now() + # Allow for overhead, two seconds for slow hosts + allowance = SECONDS + 2 + 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', 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') + + time.sleep(1) + self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) + self.assertBody('visit #4') + self.getPage('/control') + self.assertBody('visit #4') diff --git a/libraries/cherrypy/test/test_compat.py b/libraries/cherrypy/test/test_compat.py new file mode 100644 index 00000000..44a9fa31 --- /dev/null +++ b/libraries/cherrypy/test/test_compat.py @@ -0,0 +1,34 @@ +"""Test Python 2/3 compatibility module.""" +from __future__ import unicode_literals + +import unittest + +import pytest +import six + +from cherrypy import _cpcompat as compat + + +class StringTester(unittest.TestCase): + """Tests for string conversion.""" + + @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2') + def test_ntob_non_native(self): + """ntob should raise an Exception on unicode. + + (Python 2 only) + + See #1132 for discussion. + """ + self.assertRaises(TypeError, compat.ntob, 'fight') + + +class EscapeTester(unittest.TestCase): + """Class to test escape_html function from _cpcompat.""" + + def test_escape_quote(self): + """test_escape_quote - Verify the output for &<>"' chars.""" + self.assertEqual( + """xx&<>"aa'""", + compat.escape_html("""xx&<>"aa'"""), + ) diff --git a/libraries/cherrypy/test/test_config.py b/libraries/cherrypy/test/test_config.py new file mode 100644 index 00000000..be17df90 --- /dev/null +++ b/libraries/cherrypy/test/test_config.py @@ -0,0 +1,303 @@ +"""Tests for the CherryPy configuration system.""" + +import io +import os +import sys +import unittest + +import six + +import cherrypy + +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def StringIOFromNative(x): + return io.StringIO(six.text_type(x)) + + +def setup_server(): + + @cherrypy.config(foo='this', bar='that') + class Root: + + def __init__(self): + cherrypy.config.namespaces['db'] = self.db_namespace + + def db_namespace(self, k, v): + if k == 'scheme': + self.db = v + + @cherrypy.expose(alias=('global_', 'xyz')) + def index(self, key): + return cherrypy.request.config.get(key, 'None') + + @cherrypy.expose + def repr(self, key): + return repr(cherrypy.request.config.get(key, None)) + + @cherrypy.expose + def dbscheme(self): + return self.db + + @cherrypy.expose + @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')) + + @cherrypy.config(foo='this2', baz='that2') + class Foo: + + @cherrypy.expose + def index(self, key): + return cherrypy.request.config.get(key, 'None') + nex = index + + @cherrypy.expose + @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) + def silly(self): + return 'Hello world' + + # Test the expose and config decorators + @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')) + + def raw_namespace(key, value): + if key == 'input.map': + handler = cherrypy.request.handler + + def wrapper(): + params = cherrypy.request.params + for name, coercer in list(value.items()): + try: + params[name] = coercer(params[name]) + except KeyError: + pass + return handler() + cherrypy.request.handler = wrapper + 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}) + class Raw: + + @cherrypy.expose + @cherrypy.config(**{'raw.input.map': {'num': int}}) + def incr(self, num): + return num + 1 + + if not six.PY3: + thing3 = "thing3: unicode('test', errors='ignore')" + else: + thing3 = '' + + ioconf = StringIOFromNative(""" +[/] +neg: -1234 +filename: os.path.join(sys.prefix, "hello.py") +thing1: cherrypy.lib.httputil.response_codes[404] +thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 +%s +complex: 3+2j +mul: 6*3 +ones: "11" +twos: "22" +stradd: %%(ones)s + %%(twos)s + "33" + +[/favicon.ico] +tools.staticfile.filename = %r +""" % (thing3, 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 + + cherrypy.tree.mount(Another(), '/another') + cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', + 'db.scheme': r'sqlite///memory', + }) + + +# Client-side code # + + +class ConfigTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + 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'), + # If 'foo' == 'this', then the mount point '/another' leaks into + # '/'. + ('/another/', 'foo', 'None'), + ] + for path, key, expected in tests: + 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, + # From global config + 'luxuryyacht': 'throatwobblermangrove', + # From Root._cp_config + 'bar': 'that', + # From Foo._cp_config + 'baz': 'that2', + # From Foo.bar._cp_config + 'foo': 'this3', + 'bax': 'this4', + } + for key, expected in expectedconf.items(): + 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=filename') + self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) + + self.getPage('/repr?key=thing1') + self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) + + 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') + from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) + + if not six.PY3: + self.getPage('/repr?key=thing3') + self.assertBody(repr(six.text_type('test'))) + + self.getPage('/repr?key=complex') + self.assertBody('(3+2j)') + + self.getPage('/repr?key=mul') + self.assertBody('18') + + 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') + + def testCustomNamespaces(self): + self.getPage('/raw/incr?num=12') + self.assertBody('13') + + 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') + self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'), + 'rb').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') + + +class VariableSubstitutionTests(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_config(self): + from textwrap import dedent + + # variable substitution with [DEFAULT] + conf = dedent(""" + [DEFAULT] + dir = "/some/dir" + my.dir = %(dir)s + "/sub" + + [my] + my.dir = %(dir)s + "/my/dir" + my.dir2 = %(my.dir)s + '/dir2' + + """) + + 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') + + +class CallablesInConfigTest(unittest.TestCase): + setup_server = staticmethod(setup_server) + + 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'}) + + 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' + } + 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) + del cherrypy._test_dict diff --git a/libraries/cherrypy/test/test_config_server.py b/libraries/cherrypy/test/test_config_server.py new file mode 100644 index 00000000..7b183530 --- /dev/null +++ b/libraries/cherrypy/test/test_config_server.py @@ -0,0 +1,126 @@ +"""Tests for the CherryPy configuration system.""" + +import os + +import cherrypy +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +# Client-side code # + + +class ServerConfigTests(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class Root: + + @cherrypy.expose + def index(self): + return cherrypy.request.wsgi_environ['SERVER_PORT'] + + @cherrypy.expose + def upload(self, file): + return 'Size: %s' % len(file.file.read()) + + @cherrypy.expose + @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 <servername> + # Also test default server.instance = builtin server + 'server.yetanother.socket_port': 9878, + }) + + PORT = 9876 + + def testBasicConfig(self): + self.getPage('/') + self.assertBody(str(self.PORT)) + + def testAdditionalServers(self): + if self.scheme == 'https': + return self.skip('not available under ssl') + self.PORT = 9877 + self.getPage('/') + self.assertBody(str(self.PORT)) + self.PORT = 9878 + 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) + self.assertStatus(200) + self.assertBody('x' * 100) + + 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... ') + + for size in (500, 5000, 50000): + 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)]) + + # Test upload + 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) + self.assertStatus(413) diff --git a/libraries/cherrypy/test/test_conn.py b/libraries/cherrypy/test/test_conn.py new file mode 100644 index 00000000..7d60c6fb --- /dev/null +++ b/libraries/cherrypy/test/test_conn.py @@ -0,0 +1,873 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import errno +import socket +import sys +import time + +import six +from six.moves import urllib +from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected + +import pytest + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import HTTPSConnection, ntob, tonative +from cherrypy.test import helper + + +timeout = 1 +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!' + + @cherrypy.expose + def timeout(self, t): + return str(cherrypy.server.httpserver.timeout) + + @cherrypy.expose + @cherrypy.config(**{'response.stream': True}) + def stream(self, set_cl=False): + if set_cl: + cherrypy.response.headers['Content-Length'] = 10 + + def content(): + for x in range(10): + yield str(x) + + return content() + + @cherrypy.expose + def error(self, code=500): + raise cherrypy.HTTPError(code) + + @cherrypy.expose + def upload(self): + 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 + + @cherrypy.expose + @cherrypy.config(**{'hooks.on_start_resource': raise500}) + def err_before_read(self): + return 'ok' + + @cherrypy.expose + def one_megabyte_of_a(self): + 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}) + def custom_cl(self, body, cl): + cherrypy.response.headers['Content-Length'] = cl + if not isinstance(body, list): + body = [body] + newbody = [] + for chunk in body: + if isinstance(chunk, six.text_type): + 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, + }) + + +class ConnectionCloseTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + 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.assertBody(pov) + self.assertNoHeader('Connection') + + # Make another request on the same connection. + self.getPage('/page1') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Test client-side close. + self.getPage('/page2', headers=[('Connection', 'close')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, '/') + + def test_Streaming_no_len(self): + try: + self._streaming(set_cl=False) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def test_Streaming_with_len(self): + try: + self._streaming(set_cl=True) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def _streaming(self, set_cl): + 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.assertBody(pov) + 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.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') + + chunked_response = False + for k, v in self.headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + self.assertNoHeader('Connection', 'close') + else: + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should + # error. + 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') + else: + 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.assertBody(pov) + 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') + 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.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, '/') + + def test_HTTP10_KeepAlive(self): + 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.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 + # self.assertNoHeader("Connection") + + # Test a keep-alive HTTP/1.0 request. + self.persistent = True + + self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'Keep-Alive') + + # Remove the keep-alive header again. + 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") + + +class PipelineTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + 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': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Connect but send nothing. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + # Connect but send half the headers only. + self.persistent = True + 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')) + + # 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.begin() + self.assertEqual(response.status, 408) + conn.close() + + 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': + return self.skip() + + 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.endheaders() + 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._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + 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._send_output() + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % + response.read()) + + conn.close() + + # 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.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + + # Make another request on the same socket, + # but timeout on the headers + 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') + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % sys.exc_info()[1]) + else: + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % + response.read()) + + conn.close() + + # 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.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + conn.close() + + def test_HTTP11_pipelining(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + 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.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._send_output() + + # Retrieve previous response + 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 + if six.PY3: + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + conn.close() + + def test_100_Continue(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + conn = self.HTTP_CONN + + # Try a page without an Expect request header first. + # 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.endheaders() + conn.send(ntob("d'oh")) + response = conn.response_class(conn.sock, method='POST') + version, status, reason = response._read_status() + self.assertNotEqual(status, 100) + finally: + conn.close() + + # 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.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + line = response.fp.readline().strip() + if line: + self.fail( + '100 Continue should not output any headers. Got %r' % + line) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + finally: + conn.close() + + +class ConnectionTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_readall_or_close(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + if self.scheme == 'https': + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a max of 0 (the default) and then reset to what it was above. + old_max = cherrypy.server.max_request_body_size + for new_max in (0, old_max): + cherrypy.server.max_request_body_size = new_max + + self.persistent = True + 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.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + conn.send(ntob('x' * 1000)) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + 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._send_output() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + def test_No_Message_Body(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + 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.assertBody(pov) + self.assertNoHeader('Connection') + + # Make a 204 request on the same connection. + self.getPage('/custom/204') + self.assertStatus(204) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + # Make a 304 request on the same connection. + self.getPage('/custom/304') + self.assertStatus(304) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + def test_Chunked_Encoding(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + 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' + + # 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') + # 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.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') + + # 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') + # Chunked requests don't need a content-length + # # conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + conn.close() + + def test_Content_Length_in(self): + # Try a non-chunked request where Content-Length exceeds + # 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.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.') + conn.close() + + def test_Content_Length_out_preheaders(self): + # Try a non-chunked response where Content-Length is less than + # 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.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.') + conn.close() + + def test_Content_Length_out_postheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + 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) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody('I too') + conn.close() + + def test_598(self): + tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' + url = tmpl.format( + scheme=self.scheme, + host=self.HOST, + port=self.PORT, + ) + remote_data_conn = urllib.request.urlopen(url) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + else: + buf += data + remaining -= len(data) + + self.assertEqual(len(buf), 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) + 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, + }) + + +reset_names = 'ECONNRESET', 'WSAECONNRESET' +socket_reset_errors = [ + getattr(errno, name) + for name in reset_names + if hasattr(errno, name) +] +'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', +] + + +class LimitedRequestQueueTests(helper.CPWebCase): + setup_server = staticmethod(setup_upload_server) + + @pytest.mark.xfail(reason='#1535') + def test_queue_full(self): + conns = [] + overflow_conn = None + + try: + # Make 15 initial requests and leave them open, which should use + # 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.endheaders() + conns.append(conn) + + # Now try a 16th conn, which should be closed by the + # 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): + 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.endheaders() + response = overflow_conn.response_class( + overflow_conn.sock, + method='GET', + ) + try: + response.begin() + except socket.error as exc: + if exc.args[0] in socket_reset_errors: + pass # Expected. + else: + 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' + else: + 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') + response.begin() + self.body = response.read() + self.assertBody("thanks for 'done'") + self.assertEqual(response.status, 200) + conn.close() + if overflow_conn: + overflow_conn.close() + + +class BadRequestTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + 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') + response.begin() + self.body = response.read() + 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') + response.begin() + self.body = response.read() + self.assertBody('HTTP requires CRLF terminators') + conn.close() diff --git a/libraries/cherrypy/test/test_core.py b/libraries/cherrypy/test/test_core.py new file mode 100644 index 00000000..9834c1f3 --- /dev/null +++ b/libraries/cherrypy/test/test_core.py @@ -0,0 +1,823 @@ +# coding: utf-8 + +"""Basic tests for the CherryPy core: request handling.""" + +import os +import sys +import types + +import six + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cptools, tools +from cherrypy.lib import httputil, static + +from cherrypy.test._test_decorators import ExposeExamples +from cherrypy.test import helper + + +localDir = os.path.dirname(__file__) +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' + + 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)]}) + + @cherrypy.expose + def baseurl(self, path_info, relative=None): + return cherrypy.url(path_info, relative=bool(relative)) + + root = Root() + root.expose_dec = ExposeExamples() + + class TestType(type): + + """Metaclass which automatically exposes all functions in each + 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 six.itervalues(dct): + 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): + + def index(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def leaf(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def qs(self, qs): + return cherrypy.url(qs=qs) + + def log_status(): + Status.statuses.append(cherrypy.response.status) + cherrypy.tools.log_status = cherrypy.Tool( + 'on_end_resource', log_status) + + class Status(Test): + + def index(self): + return 'normal' + + def blank(self): + cherrypy.response.status = '' + + # According to RFC 2616, new status codes are OK as long as they + # are between 100 and 599. + + # Here is an illegal code... + def illegal(self): + cherrypy.response.status = 781 + return 'oops' + + # ...and here is an unknown but legal code. + def unknown(self): + cherrypy.response.status = '431 My custom error' + return 'funky' + + # Non-numeric code + def bad(self): + cherrypy.response.status = 'error' + return 'bad news' + + statuses = [] + + @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, + }) + class Error: + @cherrypy.expose + def index(self): + raise NameError('redirect_test') + + error = Error() + + def index(self): + return 'child' + + def custom(self, url, code): + raise cherrypy.HTTPRedirect(url, code) + + @cherrypy.config(**{'tools.trailing_slash.extra': True}) + def by_code(self, code): + raise cherrypy.HTTPRedirect('somewhere%20else', code) + + def nomodify(self): + raise cherrypy.HTTPRedirect('', 304) + + def proxy(self): + raise cherrypy.HTTPRedirect('proxy', 305) + + def stringify(self): + return str(cherrypy.HTTPRedirect('/')) + + def fragment(self, 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( + "/some<script>alert(1);</script>url/that'we/want") + + def url_with_unicode(self): + 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) + + def redir_custom(): + raise cherrypy.InternalRedirect('/internalredirect/custom_err') + + class InternalRedirect(Test): + + def index(self): + raise cherrypy.InternalRedirect('/') + + @cherrypy.expose + @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') + + def cousin(self, t): + assert cherrypy.request.prev.closed + return cherrypy.request.prev.query_string + + def petshop(self, user_id): + if user_id == 'parrot': + # Trade it for a slug when redirecting + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=slug') + elif user_id == 'terrier': + # Trade it for a fish when redirecting + raise cherrypy.InternalRedirect( + '/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)) + + # 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: + # tools.login_redir()(secure) + + def login(self): + return 'Please log in' + + def custom_err(self): + return 'Something went horribly wrong.' + + @cherrypy.config(**{'hooks.before_request_body': redir_custom}) + 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' + + def as_list(self): + return ['con', 'tent'] + + def as_yield(self): + yield b'content' + + @cherrypy.config(**{'tools.flatten.on': True}) + def as_dblyield(self): + yield self.as_yield() + + def as_refyield(self): + for chunk in self.as_yield(): + 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')) + + class Cookies(Test): + + def single(self, name): + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def multiple(self, names): + list(map(self.single, names)) + + def append_headers(header_list, debug=False): + if debug: + cherrypy.log( + '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): + + 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) + + def commas(self): + cherrypy.response.headers[ + 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + + cherrypy.tree.mount(root) + + def testStatus(self): + self.getPage('/status/') + self.assertBody('normal') + self.assertStatus(200) + + self.getPage('/status/blank') + self.assertBody('') + self.assertStatus(200) + + self.getPage('/status/illegal') + self.assertStatus(500) + 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') + self.assertStatus(431) + + 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'])) + + 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.assertStatus(301) + self.assertMatchesBody( + '<a href=([\'"])%s/redirect/[?]id=3\\1>' + '%s/redirect/[?]id=3</a>' % (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.assertStatus(301) + self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" % + (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.assertStatus(301) + self.assertMatchesBody( + "<a href=(['\"])%s/redirect/by_code[?]code=307\\1>" + '%s/redirect/by_code[?]code=307</a>' + % (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()) + + def testRedirect(self): + self.getPage('/redirect/') + self.assertBody('child') + self.assertStatus(200) + + self.getPage('/redirect/by_code?code=300') + self.assertMatchesBody( + r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") + self.assertStatus(300) + + self.getPage('/redirect/by_code?code=301') + self.assertMatchesBody( + r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") + self.assertStatus(301) + + self.getPage('/redirect/by_code?code=302') + self.assertMatchesBody( + r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") + self.assertStatus(302) + + self.getPage('/redirect/by_code?code=303') + self.assertMatchesBody( + r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") + self.assertStatus(303) + + self.getPage('/redirect/by_code?code=307') + self.assertMatchesBody( + r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") + self.assertStatus(307) + + self.getPage('/redirect/nomodify') + self.assertBody('') + self.assertStatus(304) + + 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') + + # Make sure str(HTTPRedirect()) works. + 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') + 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) + self.assertMatchesBody( + r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % ( + 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') + self.assertStatus(303) + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') + + def assertValidXHTML(): + from xml.etree import ElementTree + try: + ElementTree.fromstring( + '<html><body>%s</body></html>' % self.body, + ) + except ElementTree.ParseError: + self._handlewebError( + '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.assertStatus(303) + assertValidXHTML() + + # do the same with a url containing quote characters. + 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.assertStatus(303) + assert b'<script>' not in self.body + assert b'<script>' in self.body + + def test_redirect_with_unicode(self): + """ + A redirect to a URL with Unicode should return a Location + header containing that Unicode URL. + """ + # test disabled due to #1440 + return + self.getPage('/redirect/url_with_unicode') + self.assertStatus(303) + loc = self.assertHeader('Location') + assert ntou('тест', encoding='utf-8') in loc + + def test_InternalRedirect(self): + # InternalRedirect + self.getPage('/internalredirect/') + self.assertBody('hello') + self.assertStatus(200) + + # Test passthrough + self.getPage( + '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film') + self.assertBody('0 images for Sir-not-appearing-in-this-film') + self.assertStatus(200) + + # Test args + self.getPage('/internalredirect/petshop?user_id=parrot') + self.assertBody('0 images for slug') + self.assertStatus(200) + + # Test POST + 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.assertBody('Something went horribly wrong.') + self.assertStatus(200) + + self.getPage('/internalredirect/secure') + self.assertBody('Please log in') + self.assertStatus(200) + + # Relative path in InternalRedirect. + # Also tests request.prev. + self.getPage('/internalredirect/relative?a=3&b=5') + self.assertBody('a=3&b=5') + self.assertStatus(200) + + # InternalRedirect on error + self.getPage('/internalredirect/choke') + self.assertStatus(200) + 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']: + self.getPage(url) + self.assertBody('content') + + def testRanges(self): + self.getPage('/ranges/get_ranges?bytes=3-6') + self.assertBody('[(3, 7)]') + + # Test multiple ranges and a suffix-byte-range-spec, for good measure. + self.getPage('/ranges/get_ranges?bytes=2-4,-1') + self.assertBody('[(2, 5), (7, 8)]') + + # Test a suffix-byte-range longer than the content + # length. Note that in this test, the content length + # is 8 bytes. + self.getPage('/ranges/get_ranges?bytes=-100') + self.assertBody('[(0, 8)]') + + # Get a partial file. + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')]) + self.assertStatus(206) + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertHeader('Content-Range', 'bytes 2-5/14') + self.assertBody('llo,') + + # What happens with overlapping ranges (and out of order, too)? + self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')]) + self.assertStatus(206) + ct = self.assertHeader('Content-Type') + expected_type = 'multipart/byteranges; boundary=' + self.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)) + self.assertBody(expected_body) + self.assertHeader('Content-Length') + + # Test "416 Requested Range Not Satisfiable" + self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')]) + self.assertStatus(416) + # "When this status code is returned for a byte-range request, + # the response SHOULD include a Content-Range entity-header + # field specifying the current length of the selected resource" + self.assertHeader('Content-Range', 'bytes */14') + elif cherrypy.server.protocol_version == 'HTTP/1.0': + # Test Range behavior with HTTP/1.0 request + self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')]) + self.assertStatus(200) + self.assertBody('Hello, world\r\n') + + 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() + + self.getPage('/favicon.ico') + self.assertBody(data) + + def skip_if_bad_cookies(self): + """ + cookies module fails to reject invalid cookies + https://github.com/cherrypy/cherrypy/issues/1405 + """ + cookies = sys.modules.get('http.cookies') + _is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False) + if not _is_legal_key(','): + return + issue = 'http://bugs.python.org/issue26302' + tmpl = 'Broken cookies module ({issue})' + self.skip(tmpl.format(**locals())) + + def testCookies(self): + self.skip_if_bad_cookies() + + 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.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.assertStatus(400) + + def testDefaultContentType(self): + self.getPage('/') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.getPage('/defct/plain') + self.getPage('/') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.getPage('/defct/html') + + 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"'), + ]) + self.getPage('/multiheader/commas') + self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') + + def test_cherrypy_url(self): + # Input relative to current + 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()) + # Other host header + host = 'www.mydomain.example' + 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 + self.getPage('/url/leaf?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + + # Single dots + self.getPage('/url/leaf?path_info=./page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/./page1') + self.assertBody('%s/url/other/page1' % self.base()) + self.getPage('/url/?path_info=/other/./page1') + self.assertBody('%s/other/page1' % self.base()) + self.getPage('/url/?path_info=/other/././././page1') + self.assertBody('%s/other/page1' % self.base()) + + # Double dots + self.getPage('/url/leaf?path_info=../page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/../page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=/other/../page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/leaf?path_info=/other/../../../page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/leaf?path_info=/other/../../../../../page1') + self.assertBody('%s/page1' % self.base()) + + # qs param is not normalized as a path + self.getPage('/url/qs?qs=/other') + self.assertBody('%s/url/qs?/other' % self.base()) + self.getPage('/url/qs?qs=/other/../page1') + self.assertBody('%s/url/qs?/other/../page1' % self.base()) + self.getPage('/url/qs?qs=../page1') + self.assertBody('%s/url/qs?../page1' % self.base()) + self.getPage('/url/qs?qs=../../page1') + self.assertBody('%s/url/qs?../../page1' % self.base()) + + # Output relative to current path or script_name + self.getPage('/url/?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=/page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/leaf?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=leaf/page1&relative=True') + self.assertBody('leaf/page1') + self.getPage('/url/leaf?path_info=../page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/?path_info=other/../page1&relative=True') + self.assertBody('page1') + + # Output relative to / + self.getPage('/baseurl?path_info=ab&relative=True') + self.assertBody('ab') + # Output relative to / + self.getPage('/baseurl?path_info=/ab&relative=True') + self.assertBody('ab') + + # absolute-path references ("server-relative") + # Input relative to current + self.getPage('/url/leaf?path_info=page1&relative=server') + self.assertBody('/url/page1') + self.getPage('/url/?path_info=page1&relative=server') + self.assertBody('/url/page1') + # Input is 'absolute'; that is, relative to script_name + self.getPage('/url/leaf?path_info=/page1&relative=server') + self.assertBody('/page1') + self.getPage('/url/?path_info=/page1&relative=server') + self.assertBody('/page1') + + def test_expose_decorator(self): + # Test @expose + self.getPage('/expose_dec/no_call') + self.assertStatus(200) + self.assertBody('Mr E. R. Bradshaw') + + # Test @expose() + self.getPage('/expose_dec/call_empty') + self.assertStatus(200) + self.assertBody('Mrs. B.J. Smegma') + + # Test @expose("alias") + self.getPage('/expose_dec/call_alias') + self.assertStatus(200) + self.assertBody('Mr Nesbitt') + # Does the original name work? + self.getPage('/expose_dec/nesbitt') + self.assertStatus(200) + self.assertBody('Mr Nesbitt') + + # Test @expose(["alias1", "alias2"]) + self.getPage('/expose_dec/alias1') + self.assertStatus(200) + self.assertBody('Mr Ken Andrews') + self.getPage('/expose_dec/alias2') + self.assertStatus(200) + self.assertBody('Mr Ken Andrews') + # Does the original name work? + self.getPage('/expose_dec/andrews') + self.assertStatus(200) + self.assertBody('Mr Ken Andrews') + + # Test @expose(alias="alias") + self.getPage('/expose_dec/alias3') + self.assertStatus(200) + self.assertBody('Mr. and Mrs. Watson') + + +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) + + class Root: + + @cherrypy.expose + def index(self): + return 'hello' + + @cherrypy.config(**{'tools.break_header.on': True}) + def start_response_error(self): + return 'salud!' + + @cherrypy.expose + def stat(self, path): + with cherrypy.HTTPError.handle(OSError, 404): + os.stat(path) + + root = Root() + + cherrypy.tree.mount(root) + + 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.') + + 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 + ) + + +class TestBinding: + def test_bind_ephemeral_port(self): + """ + A server configured to bind to port 0 will bind to an ephemeral + port and indicate that port number on startup. + """ + cherrypy.config.reset() + bind_ephemeral_conf = { + 'server.socket_port': 0, + } + cherrypy.config.update(bind_ephemeral_conf) + cherrypy.engine.start() + assert cherrypy.server.bound_addr != cherrypy.server.bind_addr + _host, port = cherrypy.server.bound_addr + assert port > 0 + cherrypy.engine.stop() + assert cherrypy.server.bind_addr == cherrypy.server.bound_addr diff --git a/libraries/cherrypy/test/test_dynamicobjectmapping.py b/libraries/cherrypy/test/test_dynamicobjectmapping.py new file mode 100644 index 00000000..725a3ce0 --- /dev/null +++ b/libraries/cherrypy/test/test_dynamicobjectmapping.py @@ -0,0 +1,424 @@ +import six + +import cherrypy +from cherrypy.test import helper + +script_names = ['', '/foo', '/users/fred/blog', '/corp/blog'] + + +def setup_server(): + class SubSubRoot: + + @cherrypy.expose + def index(self): + return 'SubSubRoot index' + + @cherrypy.expose + def default(self, *args): + return 'SubSubRoot default' + + @cherrypy.expose + def handler(self): + return 'SubSubRoot handler' + + @cherrypy.expose + def dispatch(self): + return 'SubSubRoot dispatch' + + subsubnodes = { + '1': SubSubRoot(), + '2': SubSubRoot(), + } + + class SubRoot: + + @cherrypy.expose + def index(self): + return 'SubRoot index' + + @cherrypy.expose + def default(self, *args): + return 'SubRoot %s' % (args,) + + @cherrypy.expose + def handler(self): + return 'SubRoot handler' + + def _cp_dispatch(self, vpath): + return subsubnodes.get(vpath[0], None) + + subnodes = { + '1': SubRoot(), + '2': SubRoot(), + } + + class Root: + + @cherrypy.expose + def index(self): + return 'index' + + @cherrypy.expose + def default(self, *args): + return 'default %s' % (args,) + + @cherrypy.expose + def handler(self): + return 'handler' + + def _cp_dispatch(self, vpath): + return subnodes.get(vpath[0]) + + # ------------------------------------------------------------------------- + # DynamicNodeAndMethodDispatcher example. + # This example exposes a fairly naive HTTP api + class User(object): + + def __init__(self, id, name): + self.id = id + self.name = name + + def __unicode__(self): + return six.text_type(self.name) + + def __str__(self): + return str(self.name) + + user_lookup = { + 1: User(1, 'foo'), + 2: User(2, 'bar'), + } + + def make_user(name, id=None): + if not id: + id = max(*list(user_lookup.keys())) + 1 + user_lookup[id] = User(id, name) + return id + + @cherrypy.expose + class UserContainerNode(object): + + def POST(self, name): + """ + Allow the creation of a new Object + """ + return 'POST %d' % make_user(name) + + def GET(self): + return six.text_type(sorted(user_lookup.keys())) + + def dynamic_dispatch(self, vpath): + try: + id = int(vpath[0]) + except (ValueError, IndexError): + return None + return UserInstanceNode(id) + + @cherrypy.expose + class UserInstanceNode(object): + + def __init__(self, id): + self.id = id + self.user = user_lookup.get(id, None) + + # For all but PUT methods there MUST be a valid user identified + # by self.id + if not self.user and cherrypy.request.method != 'PUT': + raise cherrypy.HTTPError(404) + + def GET(self, *args, **kwargs): + """ + Return the appropriate representation of the instance. + """ + return six.text_type(self.user) + + def POST(self, name): + """ + Update the fields of the user instance. + """ + self.user.name = name + return 'POST %d' % self.user.id + + def PUT(self, name): + """ + Create a new user with the specified id, or edit it if it already + exists + """ + if self.user: + # Edit the current user + self.user.name = name + return 'PUT %d' % self.user.id + else: + # Make a new user with said attributes. + return 'PUT %d' % make_user(name, self.id) + + def DELETE(self): + """ + Delete the user specified at the id. + """ + id = self.user.id + del user_lookup[self.user.id] + del self.user + return 'DELETE %d' % id + + class ABHandler: + + class CustomDispatch: + + @cherrypy.expose + def index(self, a, b): + return 'custom' + + def _cp_dispatch(self, vpath): + """Make sure that if we don't pop anything from vpath, + processing still works. + """ + return self.CustomDispatch() + + @cherrypy.expose + def index(self, a, b=None): + body = ['a:' + str(a)] + if b is not None: + body.append(',b:' + str(b)) + return ''.join(body) + + @cherrypy.expose + 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. + """ + while vpath: + vpath.pop() + return self + + @cherrypy.expose + def index(self): + return 'IndexOnly index' + + class DecoratedPopArgs: + + """Test _cp_dispatch with @cherrypy.popargs.""" + + @cherrypy.expose + def index(self): + return 'no params' + + @cherrypy.expose + def hi(self): + return "hi was not interpreted as 'a' param" + DecoratedPopArgs = cherrypy.popargs( + 'a', 'b', handler=ABHandler())(DecoratedPopArgs) + + class NonDecoratedPopArgs: + + """Test _cp_dispatch = cherrypy.popargs()""" + + _cp_dispatch = cherrypy.popargs('a') + + @cherrypy.expose + def index(self, a): + return 'index: ' + str(a) + + class ParameterizedHandler: + + """Special handler created for each request""" + + def __init__(self, a): + self.a = a + + @cherrypy.expose + def index(self): + if 'a' in cherrypy.request.params: + raise Exception( + '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) + + Root.decorated = DecoratedPopArgs() + Root.undecorated = NonDecoratedPopArgs() + Root.index_only = IndexOnly() + Root.parameter_test = ParameterizedPopArgs() + + Root.users = UserContainerNode() + + md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch') + for url in script_names: + conf = { + '/': { + 'user': (url or '/').split('/')[-2], + }, + '/users': { + 'request.dispatch': md + }, + } + cherrypy.tree.mount(Root(), url, conf) + + +class DynamicObjectMappingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testObjectMapping(self): + for url in script_names: + self.script_name = url + + self.getPage('/') + self.assertBody('index') + + self.getPage('/handler') + self.assertBody('handler') + + # Dynamic dispatch will succeed here for the subnodes + # so the subroot gets called + self.getPage('/1/') + self.assertBody('SubRoot index') + + self.getPage('/2/') + self.assertBody('SubRoot index') + + self.getPage('/1/handler') + self.assertBody('SubRoot handler') + + self.getPage('/2/handler') + self.assertBody('SubRoot handler') + + # Dynamic dispatch will fail here for the subnodes + # so the default gets called + self.getPage('/asdf/') + self.assertBody("default ('asdf',)") + + self.getPage('/asdf/asdf') + self.assertBody("default ('asdf', 'asdf')") + + self.getPage('/asdf/handler') + self.assertBody("default ('asdf', 'handler')") + + # Dynamic dispatch will succeed here for the subsubnodes + # so the subsubroot gets called + self.getPage('/1/1/') + self.assertBody('SubSubRoot index') + + self.getPage('/2/2/') + self.assertBody('SubSubRoot index') + + self.getPage('/1/1/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/dispatch') + self.assertBody('SubSubRoot dispatch') + + # The exposed dispatch will not be called as a dispatch + # method. + self.getPage('/2/2/foo/foo') + self.assertBody('SubSubRoot default') + + # Dynamic dispatch will fail here for the subsubnodes + # so the SubRoot gets called + self.getPage('/1/asdf/') + self.assertBody("SubRoot ('asdf',)") + + self.getPage('/1/asdf/asdf') + self.assertBody("SubRoot ('asdf', 'asdf')") + + self.getPage('/1/asdf/handler') + self.assertBody("SubRoot ('asdf', 'handler')") + + def testMethodDispatch(self): + # GET acts like a container + self.getPage('/users') + self.assertBody('[1, 2]') + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to the container URI allows creation + self.getPage('/users', method='POST', body='name=baz') + self.assertBody('POST 3') + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to a specific instanct URI results in a 404 + # as the resource does not exit. + self.getPage('/users/5', method='POST', body='name=baz') + self.assertStatus(404) + + # PUT to a specific instanct URI results in creation + self.getPage('/users/5', method='PUT', body='name=boris') + self.assertBody('PUT 5') + self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT') + + # GET acts like a container + self.getPage('/users') + self.assertBody('[1, 2, 3, 5]') + self.assertHeader('Allow', 'GET, HEAD, POST') + + test_cases = ( + (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'), + (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'), + (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'), + (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'), + ) + for id, name, updatedname, headers in test_cases: + self.getPage('/users/%d' % id) + self.assertBody(name) + self.assertHeader('Allow', headers) + + # Make sure POSTs update already existings resources + 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.assertBody('PUT %d' % id) + self.assertHeader('Allow', headers) + + # Make sure DELETES Remove already existing resources. + self.getPage('/users/%d' % id, method='DELETE') + self.assertBody('DELETE %d' % id) + self.assertHeader('Allow', headers) + + # GET acts like a container + self.getPage('/users') + self.assertBody('[]') + self.assertHeader('Allow', 'GET, HEAD, POST') + + def testVpathDispatch(self): + self.getPage('/decorated/') + self.assertBody('no params') + + self.getPage('/decorated/hi') + self.assertBody("hi was not interpreted as 'a' param") + + self.getPage('/decorated/yo/') + self.assertBody('a:yo') + + self.getPage('/decorated/yo/there/') + self.assertBody('a:yo,b:there') + + self.getPage('/decorated/yo/there/delete') + self.assertBody('deleting yo and there') + + self.getPage('/decorated/yo/there/handled_by_dispatch/') + self.assertBody('custom') + + self.getPage('/undecorated/blah/') + self.assertBody('index: blah') + + self.getPage('/index_only/a/b/c/d/e/f/g/') + self.assertBody('IndexOnly index') + + self.getPage('/parameter_test/argument2/') + self.assertBody('argument2') diff --git a/libraries/cherrypy/test/test_encoding.py b/libraries/cherrypy/test/test_encoding.py new file mode 100644 index 00000000..ab24ab93 --- /dev/null +++ b/libraries/cherrypy/test/test_encoding.py @@ -0,0 +1,426 @@ +# coding: utf-8 + +import gzip +import io +from unittest import mock + +from six.moves.http_client import IncompleteRead +from six.moves.urllib.parse import quote as url_quote + +import cherrypy +from cherrypy._cpcompat import ntob, ntou + +from cherrypy.test import helper + + +europoundUnicode = ntou('£', encoding='utf-8') +sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8') + +sing8 = sing.encode('utf-8') +sing16 = sing.encode('utf-16') + + +class EncodingTests(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self, param): + assert param == europoundUnicode, '%r != %r' % ( + param, europoundUnicode) + yield europoundUnicode + + @cherrypy.expose + def mao_zedong(self): + return sing + + @cherrypy.expose + @cherrypy.config(**{'tools.encode.encoding': 'utf-8'}) + def utf8(self): + return sing8 + + @cherrypy.expose + def cookies_and_headers(self): + # if the headers have non-ascii characters and a cookie has + # 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.headers[ + 'Some-Header'] = 'My d\xc3\xb6g has fleas' + 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())] + ) + + @cherrypy.expose + @cherrypy.config(**{ + 'tools.encode.text_only': False, + 'tools.encode.add_charset': True, + }) + def nontext(self, *args, **kwargs): + cherrypy.response.headers[ + 'Content-Type'] = 'application/binary' + return '\x00\x01\x02\x03' + + class GZIP: + + @cherrypy.expose + def index(self): + yield 'Hello, world' + + @cherrypy.expose + # Turn encoding off so the gzip tool is the one doing the collapse. + @cherrypy.config(**{'tools.encode.on': False}) + def noshow(self): + # Test for ticket #147, where yield showed no exceptions + # (content-encoding was still gzip even though traceback + # wasn't zipped). + raise IndexError() + yield 'Here be dragons' + + @cherrypy.expose + @cherrypy.config(**{'response.stream': True}) + def noshow_stream(self): + # Test for ticket #147, where yield showed no exceptions + # (content-encoding was still gzip even though traceback + # wasn't zipped). + raise IndexError() + yield 'Here be dragons' + + class Decode: + + @cherrypy.expose + @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()]) + + @cherrypy.expose + @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()]) + + root = Root() + root.gzip = GZIP() + root.decode = Decode() + cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}}) + + def test_query_string_decoding(self): + URI_TMPL = '/reqparams?q={q}' + + europoundUtf8_2_bytes = europoundUnicode.encode('utf-8') + europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2] + + # Encoded utf8 query strings MUST be parsed correctly. + # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX + self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes))) + # The return value will be encoded as utf8. + self.assertBody(b'q: ' + europoundUtf8_2_bytes) + + # Query strings that are incorrectly encoded MUST raise 404. + # Here, q is the second byte of POUND SIGN U+A3 encoded in utf8 + # and then %HEX + # TODO: check whether this shouldn't raise 400 Bad Request instead + self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte))) + self.assertStatus(404) + self.assertErrorPage( + 404, + 'The given query string could not be processed. Query ' + "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.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.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.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.assertStatus(400) + self.assertErrorPage( + 400, + 'The request entity could not be decoded. The following charsets ' + "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.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.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.assertErrorPage( + 400, + 'The request entity could not be decoded. The following charsets ' + "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), + self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc') + + @mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1) + def test_multipart_decoding_bigger_maxrambytes(self): + """ + Decoding of a multipart entity should also pass when + the entity is bigger than maxrambytes. See ticket #1352. + """ + self.test_multipart_decoding() + + 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), + 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), + self.assertStatus(400) + self.assertErrorPage( + 400, + 'The request entity could not be decoded. The following charsets ' + "were attempted: ['us-ascii', 'utf-8']") + + def test_nontext(self): + self.getPage('/nontext') + self.assertHeader('Content-Type', 'application/binary;charset=utf-8') + self.assertBody('\x00\x01\x02\x03') + + def testEncoding(self): + # Default encoding should be utf-8 + self.getPage('/mao_zedong') + self.assertBody(sing8) + + # Ask for utf-16. + self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')]) + self.assertHeader('Content-Type', 'text/html;charset=utf-16') + self.assertBody(sing16) + + # 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.assertBody(sing16) + + # The "*" value should default to our default_encoding, utf-8 + self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')]) + self.assertBody(sing8) + + # 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.') + + # 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.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.') + + # Test the 'encoding' arg to encode. + self.getPage('/utf8') + self.assertBody(sing8) + self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')]) + 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.assertStatus('400 Bad Request') + + def testGzip(self): + zbuf = io.BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) + zfile.write(b'Hello, world') + zfile.close() + + self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')]) + self.assertInBody(zbuf.getvalue()[:3]) + self.assertHeader('Vary', 'Accept-Encoding') + self.assertHeader('Content-Encoding', 'gzip') + + # Test when gzip is denied. + self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')]) + self.assertHeader('Vary', 'Accept-Encoding') + self.assertNoHeader('Content-Encoding') + self.assertBody('Hello, world') + + self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')]) + self.assertHeader('Vary', 'Accept-Encoding') + self.assertNoHeader('Content-Encoding') + self.assertBody('Hello, world') + + # Test that trailing comma doesn't cause IndexError + # Ref: https://github.com/cherrypy/cherrypy/issues/988 + self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')]) + self.assertStatus(200) + self.assertNotInBody('IndexError') + + self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')]) + self.assertStatus(406) + self.assertNoHeader('Content-Encoding') + self.assertErrorPage(406, 'identity, gzip') + + # Test for ticket #147 + self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')]) + self.assertNoHeader('Content-Encoding') + self.assertStatus(500) + self.assertErrorPage(500, pattern='IndexError\n') + + # In this case, there's nothing we can do to deliver a + # 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')]) + 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')]) + + def test_UnicodeHeaders(self): + self.getPage('/cookies_and_headers') + self.assertBody('Any content') diff --git a/libraries/cherrypy/test/test_etags.py b/libraries/cherrypy/test/test_etags.py new file mode 100644 index 00000000..293eb866 --- /dev/null +++ b/libraries/cherrypy/test/test_etags.py @@ -0,0 +1,84 @@ +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.test import helper + + +class ETagTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def resource(self): + return 'Oh wah ta goo Siam.' + + @cherrypy.expose + def fail(self, code): + code = int(code) + if 300 <= code <= 399: + raise cherrypy.HTTPRedirect([], code) + else: + raise cherrypy.HTTPError(code) + + @cherrypy.expose + # In Python 3, tools.encode is on by default + @cherrypy.config(**{'tools.encode.on': True}) + def unicoded(self): + return ntou('I am a \u1ee4nicode string.', 'escape') + + conf = {'/': {'tools.etags.on': True, + 'tools.etags.autotags': True, + }} + cherrypy.tree.mount(Root(), config=conf) + + def test_etags(self): + self.getPage('/resource') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('Oh wah ta goo Siam.') + etag = self.assertHeader('ETag') + + # Test If-Match (both valid and invalid) + self.getPage('/resource', headers=[('If-Match', etag)]) + self.assertStatus('200 OK') + self.getPage('/resource', headers=[('If-Match', '*')]) + self.assertStatus('200 OK') + self.getPage('/resource', headers=[('If-Match', '*')], method='POST') + self.assertStatus('200 OK') + self.getPage('/resource', headers=[('If-Match', 'a bogus tag')]) + self.assertStatus('412 Precondition Failed') + + # 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.assertStatus('412 Precondition Failed') + self.getPage('/resource', headers=[('If-None-Match', '*')]) + self.assertStatus(304) + self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')]) + self.assertStatus('200 OK') + + def test_errors(self): + self.getPage('/resource') + self.assertStatus(200) + etag = self.assertHeader('ETag') + + # Test raising errors in page handler + self.getPage('/fail/412', headers=[('If-Match', etag)]) + self.assertStatus(412) + self.getPage('/fail/304', headers=[('If-Match', etag)]) + self.assertStatus(304) + self.getPage('/fail/412', headers=[('If-None-Match', '*')]) + self.assertStatus(412) + self.getPage('/fail/304', headers=[('If-None-Match', '*')]) + self.assertStatus(304) + + def test_unicode_body(self): + self.getPage('/unicoded') + self.assertStatus(200) + etag1 = self.assertHeader('ETag') + self.getPage('/unicoded', headers=[('If-Match', etag1)]) + self.assertStatus(200) + self.assertHeader('ETag', etag1) diff --git a/libraries/cherrypy/test/test_http.py b/libraries/cherrypy/test/test_http.py new file mode 100644 index 00000000..0899d4d0 --- /dev/null +++ b/libraries/cherrypy/test/test_http.py @@ -0,0 +1,307 @@ +# coding: utf-8 +"""Tests for managing HTTP issues (malformed requests, etc).""" + +import errno +import mimetypes +import socket +import sys +from unittest import mock + +import six +from six.moves.http_client import HTTPConnection +from six.moves import urllib + +import cherrypy +from cherrypy._cpcompat import HTTPSConnection, quote + +from cherrypy.test import helper + + +def is_ascii(text): + """ + Return True if the text encodes as ascii. + """ + try: + text.encode('ascii') + return True + except Exception: + pass + return False + + +def encode_filename(filename): + """ + Given a filename to be used in a multipart/form-data, + encode the name. Return the key and encoded filename. + """ + if is_ascii(filename): + return 'filename', '"{filename}"'.format(**locals()) + encoded = quote(filename, encoding='utf-8') + return 'filename*', "'".join(( + 'UTF-8', + '', # lang + encoded, + )) + + +def encode_multipart_formdata(files): + """Return (content_type, body) ready for httplib.HTTP instance. + + files: a sequence of (name, filename, value) tuples for multipart uploads. + filename can be a string or a tuple ('filename string', 'encoding') + """ + BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$' + L = [] + for key, filename, value in files: + L.append('--' + BOUNDARY) + + fn_key, encoded = encode_filename(filename) + 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) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = '\r\n'.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + +class HTTPTests(helper.CPWebCase): + + def make_connection(self): + if self.scheme == 'https': + return HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + return HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self, *args, **kwargs): + return 'Hello world!' + + @cherrypy.expose + @cherrypy.config(**{'request.process_request_body': False}) + def no_body(self, *args, **kwargs): + return 'Hello world!' + + @cherrypy.expose + def post_multipart(self, file): + """Return a summary ("a * 65536\nb * 65536") of the uploaded + file. + """ + contents = file.file.read() + summary = [] + curchar = None + count = 0 + for c in contents: + if c == curchar: + count += 1 + else: + if count: + if six.PY3: + curchar = chr(curchar) + summary.append('%s * %d' % (curchar, count)) + count = 1 + curchar = c + if count: + if six.PY3: + curchar = chr(curchar) + summary.append('%s * %d' % (curchar, count)) + return ', '.join(summary) + + @cherrypy.expose + def post_filename(self, myfile): + '''Return the name of the file which was uploaded.''' + return myfile.filename + + cherrypy.tree.mount(Root()) + cherrypy.config.update({'server.max_request_body_size': 30000000}) + + def test_no_content_length(self): + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. Even though + # the request is of method POST, this should be OK because we set + # request.process_request_body to False for our handler. + c = self.make_connection() + c.request('POST', '/no_body') + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(b'Hello world!') + + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + + # `_get_content_length` is needed for Python 3.6+ + with mock.patch.object( + c, + '_get_content_length', + lambda body, method: None, + create=True): + # `_set_content_length` is needed for Python 2.7-3.5 + with mock.patch.object(c, '_set_content_length', create=True): + c.request('POST', '/') + + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(411) + + def test_post_multipart(self): + alphabet = 'abcdefghijklmnopqrstuvwxyz' + # generate file contents for a large post + contents = ''.join([c * 65536 for c in alphabet]) + + # encode as multipart form data + files = [('file', 'file.txt', contents)] + content_type, body = encode_multipart_formdata(files) + body = body.encode('Latin-1') + + # post file + c = self.make_connection() + c.putrequest('POST', '/post_multipart') + c.putheader('Content-Type', content_type) + c.putheader('Content-Length', str(len(body))) + c.endheaders() + c.send(body) + + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + parts = ['%s * 65536' % ch for ch in alphabet] + self.assertBody(', '.join(parts)) + + def test_post_filename_with_special_characters(self): + '''Testing that we can handle filenames with special characters. This + was reported as a bug in: + https://github.com/cherrypy/cherrypy/issues/1146/ + https://github.com/cherrypy/cherrypy/issues/1397/ + https://github.com/cherrypy/cherrypy/issues/1694/ + ''' + # 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', + ] + for fname in fnames: + files = [('myfile', fname, 'yunyeenyunyue')] + content_type, body = encode_multipart_formdata(files) + body = body.encode('Latin-1') + + # post file + c = self.make_connection() + c.putrequest('POST', '/post_filename') + c.putheader('Content-Type', content_type) + c.putheader('Content-Length', str(len(body))) + c.endheaders() + c.send(body) + + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(fname) + + def test_malformed_request_line(self): + if getattr(cherrypy.server, 'using_apache', False): + return self.skip('skipped due to known Apache differences...') + + # Test missing version in Request-Line + c = self.make_connection() + c._output(b'geT /') + c._send_output() + if hasattr(c, 'strict'): + response = c.response_class(c.sock, strict=c.strict, method='GET') + else: + # Python 3.2 removed the 'strict' feature, saying: + # "http.client now always assumes HTTP/1.x compliant servers." + response = c.response_class(c.sock, method='GET') + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), b'Malformed Request-Line') + c.close() + + def test_request_line_split_issue_1220(self): + params = { + 'intervenant-entreprise-evenement_classaction': + 'evenement-mailremerciements', + '_path': 'intervenant-entreprise-evenement', + 'intervenant-entreprise-evenement_action-id': 19404, + 'intervenant-entreprise-evenement_id': 19404, + 'intervenant-entreprise_id': 28092, + } + Request_URI = '/index?' + urllib.parse.urlencode(params) + self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256) + self.getPage(Request_URI) + self.assertBody('Hello world!') + + def test_malformed_header(self): + c = self.make_connection() + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See https://github.com/cherrypy/cherrypy/issues/941 + c._output(b're, 1.2.3.4#015#012') + c.endheaders() + + response = c.getresponse() + self.status = str(response.status) + self.assertStatus(400) + self.body = response.fp.read(20) + self.assertBody('Illegal header line.') + + def test_http_over_https(self): + if self.scheme != 'https': + return self.skip('skipped (not running HTTPS)... ') + + # Try connecting without SSL. + conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + conn.putrequest('GET', '/', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + try: + 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.') + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise + + def test_garbage_in(self): + # Connect without SSL regardless of server.scheme + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c._output(b'gjkgjklsgjklsgjkljklsg') + c._send_output() + response = c.response_class(c.sock, method='GET') + try: + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), + b'Malformed Request-Line') + c.close() + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise diff --git a/libraries/cherrypy/test/test_httputil.py b/libraries/cherrypy/test/test_httputil.py new file mode 100644 index 00000000..656b8a3d --- /dev/null +++ b/libraries/cherrypy/test/test_httputil.py @@ -0,0 +1,80 @@ +"""Test helpers from ``cherrypy.lib.httputil`` module.""" +import pytest +from six.moves import http_client + +from cherrypy.lib import httputil + + +@pytest.mark.parametrize( + 'script_name,path_info,expected_url', + [ + ('/sn/', '/pi/', '/sn/pi/'), + ('/sn/', '/pi', '/sn/pi'), + ('/sn/', '/', '/sn/'), + ('/sn/', '', '/sn/'), + ('/sn', '/pi/', '/sn/pi/'), + ('/sn', '/pi', '/sn/pi'), + ('/sn', '/', '/sn/'), + ('/sn', '', '/sn'), + ('/', '/pi/', '/pi/'), + ('/', '/pi', '/pi'), + ('/', '/', '/'), + ('/', '', '/'), + ('', '/pi/', '/pi/'), + ('', '/pi', '/pi'), + ('', '/', '/'), + ('', '', '/'), + ] +) +def test_urljoin(script_name, path_info, expected_url): + """Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO.""" + actual_url = httputil.urljoin(script_name, path_info) + assert actual_url == expected_url + + +EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows') +EXPECTED_500 = ( + 500, + 'Internal Server Error', + 'The server encountered an unexpected condition which ' + 'prevented it from fulfilling the request.', +) +EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI') +EXPECTED_444 = (444, 'Non-existent reason', '') + + +@pytest.mark.parametrize( + 'status,expected_status', + [ + (None, EXPECTED_200), + (200, EXPECTED_200), + ('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 + statuses processing.""" + assert httputil.valid_status(status) == expected_status + + +@pytest.mark.parametrize( + 'status_code,error_msg', + [ + ('hey', "Illegal response status from server ('hey' is non-numeric)."), + ( + {'hey': 'hi'}, + 'Illegal response status from server ' + "({'hey': 'hi'} is non-numeric).", + ), + (1, 'Illegal response status from server (1 is out of range).'), + (600, '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.""" + with pytest.raises(ValueError) as excinfo: + httputil.valid_status(status_code) + + assert error_msg in str(excinfo) diff --git a/libraries/cherrypy/test/test_iterator.py b/libraries/cherrypy/test/test_iterator.py new file mode 100644 index 00000000..92f08e7c --- /dev/null +++ b/libraries/cherrypy/test/test_iterator.py @@ -0,0 +1,196 @@ +import six + +import cherrypy +from cherrypy.test import helper + + +class IteratorBase(object): + + created = 0 + datachunk = 'butternut squash' * 256 + + @classmethod + def incr(cls): + cls.created += 1 + + @classmethod + def decr(cls): + cls.created -= 1 + + +class OurGenerator(IteratorBase): + + def __iter__(self): + self.incr() + try: + for i in range(1024): + yield self.datachunk + finally: + self.decr() + + +class OurIterator(IteratorBase): + + started = False + closed_off = False + count = 0 + + def increment(self): + self.incr() + + def decrement(self): + if not self.closed_off: + self.closed_off = True + self.decr() + + def __iter__(self): + return self + + def __next__(self): + if not self.started: + self.started = True + self.increment() + self.count += 1 + if self.count > 1024: + raise StopIteration + return self.datachunk + + next = __next__ + + def __del__(self): + self.decrement() + + +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() + + +class OurUnclosableIterator(OurIterator): + close = 'close' # not callable! + + +class IteratorTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class Root(object): + + @cherrypy.expose + def count(self, clsname): + cherrypy.response.headers['Content-Type'] = 'text/plain' + return six.text_type(globals()[clsname].created) + + @cherrypy.expose + def getall(self, clsname): + cherrypy.response.headers['Content-Type'] = 'text/plain' + return globals()[clsname]() + + @cherrypy.expose + @cherrypy.config(**{'response.stream': True}) + def stream(self, clsname): + return self.getall(clsname) + + cherrypy.tree.mount(Root()) + + def test_iterator(self): + try: + self._test_iterator() + except Exception: + 'Test fails intermittently. See #1419' + + def _test_iterator(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Check the counts of all the classes, they should be zero. + closables = ['OurClosableIterator', 'OurGenerator'] + unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator'] + all_classes = closables + unclosables + + import random + random.shuffle(all_classes) + + for clsname in all_classes: + self.getPage('/count/' + clsname) + self.assertStatus(200) + self.assertBody('0') + + # We should also be able to read the entire content body + # successfully, though we don't need to, we just want to + # check the header. + for clsname in all_classes: + itr_conn = self.get_conn() + itr_conn.putrequest('GET', '/getall/' + clsname) + itr_conn.endheaders() + response = itr_conn.getresponse() + self.assertEqual(response.status, 200) + headers = response.getheaders() + for header_name, header_value in headers: + if header_name.lower() == 'content-length': + expected = six.text_type(1024 * 16 * 256) + assert header_value == expected, header_value + break + else: + raise AssertionError('No Content-Length header found') + + # As the response should be fully consumed by CherryPy + # before sending back, the count should still be at zero + # by the time the response has been sent. + self.getPage('/count/' + clsname) + self.assertStatus(200) + self.assertBody('0') + + # Now we do the same check with streaming - some classes will + # be automatically closed, while others cannot. + stream_counts = {} + for clsname in all_classes: + itr_conn = self.get_conn() + itr_conn.putrequest('GET', '/stream/' + clsname) + itr_conn.endheaders() + response = itr_conn.getresponse() + self.assertEqual(response.status, 200) + response.fp.read(65536) + + # Let's check the count - this should always be one. + self.getPage('/count/' + clsname) + self.assertBody('1') + + # Now if we close the connection, the count should go back + # to zero. + itr_conn.close() + self.getPage('/count/' + clsname) + + # If this is a response which should be easily closed, then + # 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) + + stream_counts[clsname] = int(self.body) + + # Check that we closed off the classes which should provide + # easy mechanisms for doing so. + for clsname in closables: + assert stream_counts[clsname] == 0, ( + 'did not close off stream response correctly, expected ' + 'count of zero for %s: %s' % (clsname, stream_counts) + ) diff --git a/libraries/cherrypy/test/test_json.py b/libraries/cherrypy/test/test_json.py new file mode 100644 index 00000000..1585f6e6 --- /dev/null +++ b/libraries/cherrypy/test/test_json.py @@ -0,0 +1,102 @@ +import cherrypy +from cherrypy.test import helper + +from cherrypy._cpcompat import json + + +json_out = cherrypy.config(**{'tools.json_out.on': True}) +json_in = cherrypy.config(**{'tools.json_in.on': True}) + + +class JsonTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root(object): + + @cherrypy.expose + def plain(self): + return 'hello' + + @cherrypy.expose + @json_out + def json_string(self): + return 'hello' + + @cherrypy.expose + @json_out + def json_list(self): + return ['a', 'b', 42] + + @cherrypy.expose + @json_out + def json_dict(self): + return {'answer': 42} + + @cherrypy.expose + @json_in + def json_post(self): + if cherrypy.request.json == [13, 'c']: + return 'ok' + else: + return 'nok' + + @cherrypy.expose + @json_out + @cherrypy.config(**{'tools.caching.on': True}) + def json_cached(self): + return 'hello there' + + root = Root() + cherrypy.tree.mount(root) + + def test_json_output(self): + if json is None: + self.skip('json not found ') + return + + self.getPage('/plain') + self.assertBody('hello') + + self.getPage('/json_string') + self.assertBody('"hello"') + + self.getPage('/json_list') + self.assertBody('["a", "b", 42]') + + self.getPage('/json_dict') + self.assertBody('{"answer": 42}') + + def test_json_input(self): + if json is None: + self.skip('json not found ') + return + + body = '[13, "c"]' + 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)))] + 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)))] + self.getPage('/json_post', method='POST', headers=headers, body=body) + self.assertStatus(400, 'Invalid JSON document') + + def test_cached(self): + if json is None: + self.skip('json not found ') + return + + self.getPage('/json_cached') + self.assertStatus(200, '"hello"') + + self.getPage('/json_cached') # 2'nd time to hit cache + self.assertStatus(200, '"hello"') diff --git a/libraries/cherrypy/test/test_logging.py b/libraries/cherrypy/test/test_logging.py new file mode 100644 index 00000000..c4948c20 --- /dev/null +++ b/libraries/cherrypy/test/test_logging.py @@ -0,0 +1,209 @@ +"""Basic tests for the CherryPy core: request handling.""" + +import os +from unittest import mock + +import six + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.test import helper, logtest + +localDir = os.path.dirname(__file__) +access_log = os.path.join(localDir, 'access.log') +error_log = os.path.join(localDir, 'error.log') + +# Some unicode strings. +tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') +erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') + + +def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'hello' + + @cherrypy.expose + def uni_code(self): + cherrypy.request.login = tartaros + cherrypy.request.remote.name = erebos + + @cherrypy.expose + def slashes(self): + cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1' + + @cherrypy.expose + def whitespace(self): + # User-Agent = "User-Agent" ":" 1*( product | comment ) + # comment = "(" *( ctext | quoted-pair | comment ) ")" + # ctext = <any TEXT excluding "(" and ")"> + # TEXT = <any OCTET except CTLs, but including LWS> + # LWS = [CRLF] 1*( SP | HT ) + cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)' + + @cherrypy.expose + def as_string(self): + return 'content' + + @cherrypy.expose + def as_yield(self): + yield 'content' + + @cherrypy.expose + @cherrypy.config(**{'tools.log_tracebacks.on': True}) + def error(self): + raise ValueError() + + root = Root() + + cherrypy.config.update({ + 'log.error_file': error_log, + 'log.access_file': access_log, + }) + cherrypy.tree.mount(root) + + +class AccessLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = access_log + + def testNormalReturn(self): + self.markLog() + self.getPage('/as_string', + headers=[('Referer', 'http://www.cherrypy.org/'), + ('User-Agent', 'Mozilla/5.0')]) + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + + def testNormalYield(self): + self.markLog() + self.getPage('/as_yield') + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' % + self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""' + % self.prefix()) + + @mock.patch( + 'cherrypy._cplogging.LogManager.access_log_format', + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}' + if six.PY3 else + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' + ) + def testCustomLogFormat(self): + """Test a customized access_log_format string, which is a + feature of _cplogging.LogManager.access().""" + self.markLog() + self.getPage('/as_string', headers=[('Referer', 'REFERER'), + ('User-Agent', 'USERAGENT'), + ('Host', 'HOST')]) + self.assertLog(-1, '%s - - [' % self.interface()) + self.assertLog(-1, '] "GET /as_string HTTP/1.1" ' + '200 7 "REFERER" "USERAGENT" HOST') + + @mock.patch( + 'cherrypy._cplogging.LogManager.access_log_format', + '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}' + if six.PY3 else + '%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' + ) + def testTimezLogFormat(self): + """Test a customized access_log_format string, which is a + feature of _cplogging.LogManager.access().""" + self.markLog() + + expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime()) + with mock.patch( + 'cherrypy._cplogging.LazyRfc3339UtcTime', + lambda: expected_time): + self.getPage('/as_string', headers=[('Referer', 'REFERER'), + ('User-Agent', 'USERAGENT'), + ('Host', 'HOST')]) + + self.assertLog(-1, '%s - - ' % self.interface()) + self.assertLog(-1, expected_time) + self.assertLog(-1, ' "GET /as_string HTTP/1.1" ' + '200 7 "REFERER" "USERAGENT" HOST') + + @mock.patch( + 'cherrypy._cplogging.LogManager.access_log_format', + '{i}' if six.PY3 else '%(i)s' + ) + def testUUIDv4ParameterLogFormat(self): + """Test rendering of UUID4 within access log.""" + self.markLog() + self.getPage('/as_string') + self.assertValidUUIDv4() + + def testEscapedOutput(self): + # Test unicode in access log pieces. + self.markLog() + self.getPage('/uni_code') + self.assertStatus(200) + if six.PY3: + # The repr of a bytestring in six.PY3 includes a b'' prefix + self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) + else: + self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) + # Test the erebos value. Included inline for your enlightenment. + # Note the 'r' prefix--those backslashes are literals. + self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') + + # Test backslashes in output. + self.markLog() + self.getPage('/slashes') + self.assertStatus(200) + if six.PY3: + self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"') + else: + self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') + + # Test whitespace in output. + self.markLog() + self.getPage('/whitespace') + self.assertStatus(200) + # Again, note the 'r' prefix. + self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') + + +class ErrorLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = error_log + + def testTracebacks(self): + # Test that tracebacks get written to the error log. + self.markLog() + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + self.getPage('/error') + self.assertInBody('raise ValueError()') + self.assertLog(0, 'HTTP') + self.assertLog(1, 'Traceback (most recent call last):') + self.assertLog(-2, 'raise ValueError()') + finally: + ignore.pop() diff --git a/libraries/cherrypy/test/test_mime.py b/libraries/cherrypy/test/test_mime.py new file mode 100644 index 00000000..ef35d10e --- /dev/null +++ b/libraries/cherrypy/test/test_mime.py @@ -0,0 +1,134 @@ +"""Tests for various MIME issues, including the safe_multipart Tool.""" + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.test import helper + + +def setup_server(): + + class Root: + + @cherrypy.expose + def multipart(self, parts): + return repr(parts) + + @cherrypy.expose + def multipart_form_data(self, **kwargs): + return repr(list(sorted(kwargs.items()))) + + @cherrypy.expose + def flashupload(self, Filedata, Upload, Filename): + 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()) + + +# Client-side code # + + +class MultipartTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_multipart(self): + text_part = ntou('This is the text version') + html_part = ntou( + """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> + <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type"> +</head> +<body bgcolor="#ffffff" text="#000000"> + +This is the <strong>HTML</strong> version +</body> +</html> +""") + 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))), + ] + self.getPage('/multipart', headers, 'POST', body) + 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), + self.assertBody( + repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) + + +class SafeMultipartHandlingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Flash_Upload(self): + headers = [ + ('Accept', 'text/*'), + ('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'<?xml version="1.0" encoding="UTF-8"?>\r\n' + b'<projectDescription>\r\n' + b'</projectDescription>\r\n') + body = ( + b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + b'Content-Disposition: form-data; name="Filename"\r\n' + b'\r\n' + b'.project\r\n' + b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 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'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + b'Content-Disposition: form-data; name="Upload"\r\n' + b'\r\n' + b'Submit Query\r\n' + # Flash apps omit the trailing \r\n on the last line: + b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' + ) + self.getPage('/flashupload', headers, 'POST', body) + self.assertBody('Upload: Submit Query, Filename: .project, ' + 'Filedata: %r' % filedata) diff --git a/libraries/cherrypy/test/test_misc_tools.py b/libraries/cherrypy/test/test_misc_tools.py new file mode 100644 index 00000000..fb85b8f8 --- /dev/null +++ b/libraries/cherrypy/test/test_misc_tools.py @@ -0,0 +1,210 @@ +import os + +import cherrypy +from cherrypy import tools +from cherrypy.test import helper + + +localDir = os.path.dirname(__file__) +logfile = os.path.join(localDir, 'test_misc_tools.log') + + +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, + }) + def other(self): + return 'salut' + + @cherrypy.config(**{'tools.accept.on': True}) + class Accept: + + @cherrypy.expose + def index(self): + return '<a href="feed">Atom feed</a>' + + @cherrypy.expose + @tools.accept(media='application/atom+xml') + def feed(self): + return """<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <title>Unknown Blog</title> +</feed>""" + + @cherrypy.expose + def select(self): + # We could also write this: mtype = cherrypy.lib.accept.accept(...) + mtype = tools.accept.callable(['text/html', 'text/plain']) + if mtype == 'text/html': + return '<h2>Page Title</h2>' + else: + 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' + cherrypy.request.headers.get('Accept-Encoding') + # Read a header directly with '__getitem__' + cherrypy.request.headers['Host'] + # Read a header directly with '__contains__' + 'If-Modified-Since' in cherrypy.request.headers + # Read a header directly + 'Range' in cherrypy.request.headers + # Call a lib function + 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}, + } + + root = Root() + root.referer = Referer() + root.accept = Accept() + root.autovary = AutoVary() + cherrypy.tree.mount(root, config=conf) + cherrypy.config.update({'log.error_file': logfile}) + + +class ResponseHeadersTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testResponseHeadersDecorator(self): + self.getPage('/') + self.assertHeader('Content-Language', 'en-GB') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + def testResponseHeaders(self): + self.getPage('/other') + self.assertHeader('Content-Language', 'fr') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + +class RefererTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testReferer(self): + self.getPage('/referer/accept') + self.assertErrorPage(403, 'Forbidden Referer header.') + + self.getPage('/referer/accept', + headers=[('Referer', 'http://www.example.com/')]) + self.assertStatus(200) + self.assertBody('Accepted!') + + # Reject + self.getPage('/referer/reject') + self.assertStatus(200) + self.assertBody('Accepted!') + + self.getPage('/referer/reject', + headers=[('Referer', 'http://www.example.com/')]) + self.assertErrorPage(403, 'Forbidden Referer header.') + + +class AcceptTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Accept_Tool(self): + # Test with no header provided + self.getPage('/accept/feed') + self.assertStatus(200) + self.assertInBody('<title>Unknown Blog</title>') + + # Specify exact media type + self.getPage('/accept/feed', + headers=[('Accept', 'application/atom+xml')]) + self.assertStatus(200) + self.assertInBody('<title>Unknown Blog</title>') + + # Specify matching media range + self.getPage('/accept/feed', headers=[('Accept', 'application/*')]) + self.assertStatus(200) + self.assertInBody('<title>Unknown Blog</title>') + + # Specify all media ranges + self.getPage('/accept/feed', headers=[('Accept', '*/*')]) + self.assertStatus(200) + self.assertInBody('<title>Unknown Blog</title>') + + # 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.') + + # Test resource where tool is 'on' but media is None (not set). + self.getPage('/accept/') + self.assertStatus(200) + self.assertBody('<a href="feed">Atom feed</a>') + + def test_accept_selection(self): + # Try both our expected media types + self.getPage('/accept/select', [('Accept', 'text/html')]) + self.assertStatus(200) + self.assertBody('<h2>Page Title</h2>') + 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.assertStatus(200) + self.assertBody('PAGE TITLE') + + # text/* and */* should prefer text/html since it comes first + # in our 'media' argument to tools.accept + self.getPage('/accept/select', [('Accept', 'text/*')]) + self.assertStatus(200) + self.assertBody('<h2>Page Title</h2>') + self.getPage('/accept/select', [('Accept', '*/*')]) + self.assertStatus(200) + self.assertBody('<h2>Page Title</h2>') + + # Try unacceptable media types + self.getPage('/accept/select', [('Accept', 'application/xml')]) + self.assertErrorPage( + 406, + 'Your client sent this Accept header: application/xml. ' + 'But this resource only emits these media types: ' + 'text/html, text/plain.') + + +class AutoVaryTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testAutoVary(self): + self.getPage('/autovary/') + self.assertHeader( + 'Vary', + 'Accept, Accept-Charset, Accept-Encoding, ' + 'Host, If-Modified-Since, Range' + ) diff --git a/libraries/cherrypy/test/test_native.py b/libraries/cherrypy/test/test_native.py new file mode 100644 index 00000000..caebc3f4 --- /dev/null +++ b/libraries/cherrypy/test/test_native.py @@ -0,0 +1,35 @@ +"""Test the native server.""" + +import pytest +from requests_toolbelt import sessions + +import cherrypy._cpnative_server + + +pytestmark = pytest.mark.skipif( + 'sys.platform == "win32"', + reason='tests fail on Windows', +) + + +@pytest.fixture +def cp_native_server(request): + """A native server.""" + class Root(object): + @cherrypy.expose + def index(self): + return 'Hello World!' + + cls = cherrypy._cpnative_server.CPHTTPServer + cherrypy.server.httpserver = cls(cherrypy.server) + + cherrypy.tree.mount(Root(), '/') + cherrypy.engine.start() + request.addfinalizer(cherrypy.engine.stop) + url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals()) + return sessions.BaseUrlSession(url) + + +def test_basic_request(cp_native_server): + """A request to a native server should succeed.""" + cp_native_server.get('/') diff --git a/libraries/cherrypy/test/test_objectmapping.py b/libraries/cherrypy/test/test_objectmapping.py new file mode 100644 index 00000000..98402b8b --- /dev/null +++ b/libraries/cherrypy/test/test_objectmapping.py @@ -0,0 +1,430 @@ +import sys +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy._cptree import Application +from cherrypy.test import helper + +script_names = ['', '/foo', '/users/fred/blog', '/corp/blog'] + + +class ObjectMappingTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self, name='world'): + return name + + @cherrypy.expose + def foobar(self): + return 'bar' + + @cherrypy.expose + def default(self, *params, **kwargs): + return 'default:' + repr(params) + + @cherrypy.expose + def other(self): + return 'other' + + @cherrypy.expose + def extra(self, *p): + return repr(p) + + @cherrypy.expose + def redirect(self): + raise cherrypy.HTTPRedirect('dir1/', 302) + + def notExposed(self): + return 'not exposed' + + @cherrypy.expose + def confvalue(self): + return cherrypy.request.config.get('user') + + @cherrypy.expose + def redirect_via_url(self, path): + raise cherrypy.HTTPRedirect(cherrypy.url(path)) + + @cherrypy.expose + def translate_html(self): + return 'OK' + + @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' + + @cherrypy.expose + @cherrypy.config(**{'tools.trailing_slash.extra': True}) + def myMethod(self): + return 'myMethod from dir1, path_info is:' + repr( + 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 + + @cherrypy.expose + def script_name(self): + return cherrypy.tree.script_name() + + @cherrypy.expose + def cherrypy_url(self): + return cherrypy.url('/extra') + + @cherrypy.expose + 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') + + # MethodDispatcher code + @cherrypy.expose + class ByMethod: + + def __init__(self, *things): + self.things = list(things) + + def GET(self): + return repr(self.things) + + def POST(self, thing): + self.things.append(thing) + + class Collection: + default = ByMethod('a', 'bit') + + Root.exposing = Exposing() + Root.exposingnew = ExposingNewStyle() + Root.dir1 = Dir1() + Root.dir1.dir2 = Dir2() + Root.dir1.dir2.dir3 = Dir3() + Root.dir1.dir2.dir3.dir4 = Dir4() + Root.defnoindex = DefNoIndex() + Root.bymethod = ByMethod('another') + Root.collection = Collection() + + d = cherrypy.dispatch.MethodDispatcher() + for url in script_names: + 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!' + + cherrypy.tree.mount(Isolated(), '/isolated') + + @cherrypy.expose + class AnotherApp: + + def GET(self): + return 'milk' + + cherrypy.tree.mount(AnotherApp(), '/app', + {'/': {'request.dispatch': d}}) + + def testObjectMapping(self): + for url in script_names: + self.script_name = url + + self.getPage('/') + self.assertBody('world') + + self.getPage('/dir1/myMethod') + self.assertBody( + "myMethod from dir1, path_info is:'/dir1/myMethod'") + + self.getPage('/this/method/does/not/exist') + self.assertBody( + "default:('this', 'method', 'does', 'not', 'exist')") + + self.getPage('/extra/too/much') + self.assertBody("('too', 'much')") + + self.getPage('/other') + self.assertBody('other') + + self.getPage('/notExposed') + self.assertBody("default:('notExposed',)") + + self.getPage('/dir1/dir2/') + self.assertBody('index for dir2, path is:/dir1/dir2/') + + # Test omitted trailing slash (should be redirected by default). + self.getPage('/dir1/dir2') + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/dir2/' % self.base()) + + # Test extra trailing slash (should be redirected if configured). + self.getPage('/dir1/myMethod/') + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/myMethod' % self.base()) + + # 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')") + + # Test *vpath when default() is defined but not index() + # This also tests HTTPRedirect with default. + self.getPage('/defnoindex') + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/contact' % self.base()) + self.getPage('/defnoindex/') + self.assertStatus((302, 303)) + 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.getPage('/redirect') + self.assertStatus('302 Found') + self.assertHeader('Location', '%s/dir1/' % self.base()) + + if not getattr(cherrypy.server, 'using_apache', False): + # Test that we can use URL's which aren't all valid Python + # identifiers + # This should also test the %XX-unquoting of URL's. + self.getPage('/Von%20B%fclow?ID=14') + self.assertBody('ID is 14') + + # Test that %2F in the path doesn't get unquoted too early; + # that is, it should not be used to separate path components. + # See ticket #393. + self.getPage('/page%2Fname') + self.assertBody("default:('page/name',)") + + self.getPage('/dir1/dir2/script_name') + self.assertBody(url) + self.getPage('/dir1/dir2/cherrypy_url') + self.assertBody('%s/extra' % self.base()) + + # Test that configs don't overwrite each other from different apps + self.getPage('/confvalue') + self.assertBody((url or '/').split('/')[-2]) + + self.script_name = '' + + # Test absoluteURI's in the Request-Line + 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.assertBody("default:('abs',)") + + self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') + self.assertBody("default:('rel',)") + + # Test that the "isolated" app doesn't leak url's into the root app. + # If it did leak, Root.default() would answer with + # "default:('isolated', 'doesnt', 'exist')". + self.getPage('/isolated/') + self.assertStatus('200 OK') + self.assertBody('made it!') + self.getPage('/isolated/doesnt/exist') + self.assertStatus('404 Not Found') + + # Make sure /foobar maps to Root.foobar and not to the app + # mounted at /foo. See + # https://github.com/cherrypy/cherrypy/issues/573 + self.getPage('/foobar') + self.assertBody('bar') + + def test_translate(self): + self.getPage('/translate_html') + self.assertStatus('200 OK') + self.assertBody('OK') + + self.getPage('/translate.html') + self.assertStatus('200 OK') + self.assertBody('OK') + + self.getPage('/translate-html') + self.assertStatus('200 OK') + self.assertBody('OK') + + def test_redir_using_url(self): + for url in script_names: + self.script_name = url + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + def testPositionalParams(self): + self.getPage('/dir1/dir2/posparam/18/24/hut/hike') + self.assertBody('18/24/hut/hike') + + # intermediate index methods should not receive posparams; + # only the "final" index method should do so. + self.getPage('/dir1/dir2/5/3/sir') + self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')") + + # test that extra positional args raises an 404 Not Found + # See https://github.com/cherrypy/cherrypy/issues/733. + self.getPage('/dir1/dir2/script_name/extra/stuff') + self.assertStatus(404) + + def testExpose(self): + # Test the cherrypy.expose function/decorator + self.getPage('/exposing/base') + self.assertBody('expose works!') + + self.getPage('/exposing/1') + self.assertBody('expose works!') + + self.getPage('/exposing/2') + self.assertBody('expose works!') + + self.getPage('/exposingnew/base') + self.assertBody('expose works!') + + self.getPage('/exposingnew/1') + self.assertBody('expose works!') + + self.getPage('/exposingnew/2') + self.assertBody('expose works!') + + def testMethodDispatch(self): + self.getPage('/bymethod') + self.assertBody("['another']") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage('/bymethod', method='HEAD') + self.assertBody('') + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage('/bymethod', method='POST', body='thing=one') + self.assertBody('') + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage('/bymethod') + self.assertBody(repr(['another', ntou('one')])) + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage('/bymethod', method='PUT') + self.assertErrorPage(405) + self.assertHeader('Allow', 'GET, HEAD, POST') + + # Test default with posparams + self.getPage('/collection/silly', method='POST') + self.getPage('/collection', method='GET') + self.assertBody("['a', 'bit', 'silly']") + + # Test custom dispatcher set on app root (see #737). + self.getPage('/app') + self.assertBody('milk') + + def testTreeMounting(self): + class Root(object): + + @cherrypy.expose + def hello(self): + return 'Hello world!' + + # When mounting an application instance, + # we can't specify a different script name in the call to mount. + a = Application(Root(), '/somewhere') + self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse') + + # When mounting an application instance... + a = Application(Root(), '/somewhere') + # ...we MUST allow in identical script name in the call to mount... + cherrypy.tree.mount(a, '/somewhere') + self.getPage('/somewhere/hello') + self.assertStatus(200) + # ...and MUST allow a missing script_name. + del cherrypy.tree.apps['/somewhere'] + cherrypy.tree.mount(a) + self.getPage('/somewhere/hello') + self.assertStatus(200) + + # In addition, we MUST be able to create an Application using + # script_name == None for access to the wsgi_environ. + a = Application(Root(), script_name=None) + # However, this does not apply to tree.mount + self.assertRaises(TypeError, cherrypy.tree.mount, a, None) + + def testKeywords(self): + if sys.version_info < (3,): + return self.skip('skipped (Python 3 only)') + exec("""class Root(object): + @cherrypy.expose + def hello(self, *, name='world'): + return 'Hello %s!' % name +cherrypy.tree.mount(Application(Root(), '/keywords'))""") + + self.getPage('/keywords/hello') + self.assertStatus(200) + self.getPage('/keywords/hello/extra') + self.assertStatus(404) diff --git a/libraries/cherrypy/test/test_params.py b/libraries/cherrypy/test/test_params.py new file mode 100644 index 00000000..73b4cb4c --- /dev/null +++ b/libraries/cherrypy/test/test_params.py @@ -0,0 +1,61 @@ +import sys +import textwrap + +import cherrypy +from cherrypy.test import helper + + +class ParamsTest(helper.CPWebCase): + @staticmethod + def setup_server(): + class Root: + @cherrypy.expose + @cherrypy.tools.json_out() + @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) + + def test_pass(self): + self.getPage('/resource') + self.assertStatus(200) + self.assertBody('"NoneType"') + + self.getPage('/resource?limit=0') + self.assertStatus(200) + self.assertBody('"int"') + + def test_error(self): + self.getPage('/resource?limit=') + self.assertStatus(400) + self.assertInBody('invalid literal for int') + + cherrypy.config['tools.params.error'] = 422 + self.getPage('/resource?limit=') + self.assertStatus(422) + self.assertInBody('invalid literal for int') + + cherrypy.config['tools.params.exception'] = TypeError + self.getPage('/resource?limit=') + self.assertStatus(500) + + def test_syntax(self): + if sys.version_info < (3,): + return self.skip('skipped (Python 3 only)') + code = textwrap.dedent(""" + class Root: + @cherrypy.expose + @cherrypy.tools.params() + def resource(self, limit: int): + return type(limit).__name__ + conf = {'/': {'tools.params.on': True}} + cherrypy.tree.mount(Root(), config=conf) + """) + exec(code) + + self.getPage('/resource?limit=0') + self.assertStatus(200) + self.assertBody('int') diff --git a/libraries/cherrypy/test/test_plugins.py b/libraries/cherrypy/test/test_plugins.py new file mode 100644 index 00000000..4d3aa6b1 --- /dev/null +++ b/libraries/cherrypy/test/test_plugins.py @@ -0,0 +1,14 @@ +from cherrypy.process import plugins + + +__metaclass__ = type + + +class TestAutoreloader: + def test_file_for_file_module_when_None(self): + """No error when module.__file__ is None. + """ + class test_module: + __file__ = None + + assert plugins.Autoreloader._file_for_file_module(test_module) is None diff --git a/libraries/cherrypy/test/test_proxy.py b/libraries/cherrypy/test/test_proxy.py new file mode 100644 index 00000000..4d34440a --- /dev/null +++ b/libraries/cherrypy/test/test_proxy.py @@ -0,0 +1,154 @@ +import cherrypy +from cherrypy.test import helper + +script_names = ['', '/path/to/myapp'] + + +class ProxyTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + # Set up site + 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) + + @cherrypy.expose + def pageurl(self): + return self.thisnewpage + + @cherrypy.expose + def index(self): + raise cherrypy.HTTPRedirect('dummy') + + @cherrypy.expose + def remoteip(self): + return cherrypy.request.remote.ip + + @cherrypy.expose + @cherrypy.config(**{ + 'tools.proxy.local': 'X-Host', + 'tools.trailing_slash.extra': True, + }) + def xhost(self): + raise cherrypy.HTTPRedirect('blah') + + @cherrypy.expose + def base(self): + return cherrypy.request.base + + @cherrypy.expose + @cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'}) + def ssl(self): + return cherrypy.request.base + + @cherrypy.expose + def newurl(self): + return ("Browse to <a href='%s'>this page</a>." + % cherrypy.url('/this/new/page')) + + @cherrypy.expose + @cherrypy.config(**{ + 'tools.proxy.base': None, + }) + def base_no_base(self): + return cherrypy.request.base + + for sn in script_names: + cherrypy.tree.mount(Root(sn), sn) + + def testProxy(self): + self.getPage('/') + 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')]) + 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) + # Test multiple X-Forwarded-Host headers + 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.assertBody('192.168.0.20') + # Fix bug #1268 + 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) + + # Test X-Forwarded-Proto (lighttpd) + self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')]) + self.assertBody('https://www.mydomain.test') + + # Test X-Forwarded-Ssl (webfaction?) + self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')]) + self.assertBody('https://www.mydomain.test') + + # Test cherrypy.url() + for sn in script_names: + # Test the value inside requests + self.getPage(sn + '/newurl') + self.assertBody( + "Browse to <a href='%s://www.mydomain.test" % self.scheme + + sn + "/this/new/page'>this page</a>.") + self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host', + 'http://www.example.test')]) + self.assertBody("Browse to <a href='http://www.example.test" + + sn + "/this/new/page'>this page</a>.") + + # Test the value outside requests + port = '' + if self.scheme == 'http' and self.PORT != 80: + port = ':%s' % self.PORT + elif self.scheme == 'https' and self.PORT != 443: + port = ':%s' % self.PORT + 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)) + 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) + + def test_no_base_port_in_host(self): + """ + If no base is indicated, and the host header is used to resolve + the base, it should rely on the host header for the port also. + """ + headers = {'Host': 'localhost:8080'}.items() + self.getPage('/base_no_base', headers=headers) + self.assertBody('http://localhost:8080') diff --git a/libraries/cherrypy/test/test_refleaks.py b/libraries/cherrypy/test/test_refleaks.py new file mode 100644 index 00000000..c2fe9e66 --- /dev/null +++ b/libraries/cherrypy/test/test_refleaks.py @@ -0,0 +1,66 @@ +"""Tests for refleaks.""" + +import itertools +import platform +import threading + +from six.moves.http_client import HTTPConnection + +import cherrypy +from cherrypy._cpcompat import HTTPSConnection +from cherrypy.test import helper + + +data = object() + + +class ReferenceTests(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class Root: + + @cherrypy.expose + def index(self, *args, **kwargs): + cherrypy.request.thing = data + return 'Hello world!' + + cherrypy.tree.mount(Root()) + + def test_threadlocal_garbage(self): + if platform.system() == 'Darwin': + self.skip('queue issues; see #1474') + success = itertools.count() + + def getpage(): + host = '%s:%s' % (self.interface(), self.PORT) + if self.scheme == 'https': + c = HTTPSConnection(host) + else: + c = HTTPConnection(host) + try: + c.putrequest('GET', '/') + c.endheaders() + response = c.getresponse() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello world!') + finally: + c.close() + next(success) + + ITERATIONS = 25 + + ts = [ + threading.Thread(target=getpage) + for _ in range(ITERATIONS) + ] + + for t in ts: + t.start() + + for t in ts: + t.join() + + self.assertEqual(next(success), ITERATIONS) diff --git a/libraries/cherrypy/test/test_request_obj.py b/libraries/cherrypy/test/test_request_obj.py new file mode 100644 index 00000000..6b93e13d --- /dev/null +++ b/libraries/cherrypy/test/test_request_obj.py @@ -0,0 +1,932 @@ +"""Basic tests for the cherrypy.Request object.""" + +from functools import wraps +import os +import sys +import types +import uuid + +import six +from six.moves.http_client import IncompleteRead + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.lib import httputil +from cherrypy.test import helper + +localDir = os.path.dirname(__file__) + +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' + + @cherrypy.expose + def scheme(self): + return cherrypy.request.scheme + + @cherrypy.expose + def created_example_com_3128(self): + """Handle CONNECT method.""" + cherrypy.response.status = 204 + + @cherrypy.expose + def body_example_com_3128(self): + """Handle CONNECT method.""" + return ( + cherrypy.request.method + + 'ed to ' + + cherrypy.request.path_info + ) + + @cherrypy.expose + def request_uuid4(self): + return [ + str(cherrypy.request.unique_id), + ' ', + str(cherrypy.request.unique_id), + ] + + root = Root() + + class TestType(type): + """Metaclass which automatically exposes all functions in each + 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) + + def ismap(self, x, y): + return 'Coordinates: %s, %s' % (x, y) + + @cherrypy.config(**{'request.query_string_encoding': 'latin1'}) + def default(self, *args, **kwargs): + return 'args: %s kwargs: %s' % (args, sorted(kwargs.items())) + + @cherrypy.expose + class ParamErrorsCallable(object): + + def __call__(self): + return 'data' + + 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' + + @cherrypy.expose + def one_positional_args(self, param1, *args): + return 'data' + + @cherrypy.expose + def one_positional_args_kwargs(self, param1, *args, **kwargs): + return 'data' + + @cherrypy.expose + def one_positional_kwargs(self, param1, **kwargs): + return 'data' + + @cherrypy.expose + def no_positional(self): + return 'data' + + @cherrypy.expose + def no_positional_args(self, *args): + return 'data' + + @cherrypy.expose + def no_positional_args_kwargs(self, *args, **kwargs): + return 'data' + + @cherrypy.expose + def no_positional_kwargs(self, **kwargs): + return 'data' + + callable_object = ParamErrorsCallable() + + @cherrypy.expose + def raise_type_error(self, **kwargs): + raise TypeError('Client Error') + + @cherrypy.expose + def raise_type_error_with_default_param(self, x, y=None): + return '%d' % 'a' # throw an exception + + @cherrypy.expose + @handler_dec + def raise_type_error_decorated(self, *args, **kwargs): + raise TypeError('Client Error') + + def callable_error_page(status, **kwargs): + return "Error %s - Well, I'm very sorry but you haven't paid!" % ( + 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, + }) + def custom(self, err='404'): + raise cherrypy.HTTPError( + int(err), 'No, <b>really</b>, not found!') + + @cherrypy.config(**{ + 'error_page.default': callable_error_page, + }) + def custom_default(self): + return 1 + 'a' # raise an unexpected error + + @cherrypy.config(**{'error_page.404': 'nonexistent.html'}) + def noexist(self): + raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!') + + def page_method(self): + raise ValueError() + + def page_yield(self): + yield 'howdy' + raise ValueError() + + @cherrypy.config(**{'response.stream': True}) + def page_streamed(self): + yield 'word up' + raise ValueError() + yield 'very oops' + + @cherrypy.config(**{'request.show_tracebacks': False}) + def cause_err_in_finalize(self): + # Since status must start with an int, this should error. + cherrypy.response.status = 'ZOO OK' + + @cherrypy.config(**{'request.throw_errors': True}) + def rethrow(self): + """Test that an error raised here will be thrown out to + the server. + """ + raise ValueError() + + class Expect(Test): + + def expectation_failed(self): + expect = cherrypy.request.headers.elements('Expect') + if expect and expect[0].value != '100-continue': + raise cherrypy.HTTPError(400) + 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] + + def doubledheaders(self): + # From https://github.com/cherrypy/cherrypy/issues/165: + # "header field names should not be case sensitive sayes the + # rfc. if i set a headerfield in complete lowercase i end up + # with two header fields, one in lowercase, the other in + # mixed-case." + + # Set the most common headers + hMap = cherrypy.response.headers + 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)) + + # Set a rare header for fun + hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' + + return 'double header test' + + def ifmatch(self): + val = cherrypy.request.headers['If-Match'] + assert isinstance(val, six.text_type) + cherrypy.response.headers['ETag'] = val + return val + + class HeaderElements(Test): + + def get_elements(self, headername): + e = cherrypy.request.headers.elements(headername) + return '\n'.join([six.text_type(x) for x in e]) + + class Method(Test): + + def index(self): + m = cherrypy.request.method + if m in defined_http_methods or m == 'CONNECT': + return m + + if m == 'LINK': + raise cherrypy.HTTPError(405) + else: + raise cherrypy.HTTPError(501) + + def parameterized(self, data): + return data + + def request_body(self): + # This should be a file object (temp file), + # which CP will just pipe back out if we tell it to. + return cherrypy.request.body + + def reachable(self): + return 'success' + + class Divorce(Test): + + """HTTP Method handlers shouldn't collide with normal method names. + For example, a GET-handler shouldn't collide with a method named + 'get'. + + If you build HTTP method dispatching into CherryPy, rewrite this + class to use your new dispatch mechanism and make sure that: + "GET /divorce HTTP/1.1" maps to divorce.index() and + "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() + """ + + documents = {} + + @cherrypy.expose + def index(self): + yield '<h1>Choose your document</h1>\n' + yield '<ul>\n' + for id, contents in self.documents.items(): + yield ( + " <li><a href='/divorce/get?ID=%s'>%s</a>:" + ' %s</li>\n' % (id, id, contents)) + yield '</ul>' + + @cherrypy.expose + def get(self, ID): + 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' + return existing + + appconf = { + '/method': { + 'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND', + 'PATCH') + }, + } + cherrypy.tree.mount(root, config=appconf) + + def test_scheme(self): + self.getPage('/scheme') + self.assertBody(self.scheme) + + 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) + ) + + self.getPage('/request_uuid4') + third_uuid4, _, _ = self.body.decode().partition(' ') + assert ( + uuid.UUID(first_uuid4, version=4) + != uuid.UUID(third_uuid4, version=4) + ) + + def testRelativeURIPathInfo(self): + self.getPage('/pathinfo/foo/bar') + self.assertBody('/pathinfo/foo/bar') + + def testAbsoluteURIPathInfo(self): + # http://cherrypy.org/ticket/1061 + self.getPage('http://localhost/pathinfo/foo/bar') + self.assertBody('/pathinfo/foo/bar') + + def testParams(self): + self.getPage('/params/?thing=a') + self.assertBody(repr(ntou('a'))) + + self.getPage('/params/?thing=a&thing=b&thing=c') + self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) + + # Test friendly error message when given params are not accepted. + cherrypy.config.update({'request.show_mismatched_params': True}) + self.getPage('/params/?notathing=meeting') + self.assertInBody('Missing parameters: thing') + self.getPage('/params/?thing=meeting¬athing=meeting') + self.assertInBody('Unexpected query string parameters: notathing') + + # Test ability to turn off friendly error messages + cherrypy.config.update({'request.show_mismatched_params': False}) + self.getPage('/params/?notathing=meeting') + self.assertInBody('Not Found') + self.getPage('/params/?thing=meeting¬athing=meeting') + self.assertInBody('Not Found') + + # 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'))])) + + # Make sure that encoded = and & get parsed correctly + self.getPage( + '/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2') + self.assertBody('args: %s kwargs: %s' % + (('code',), + [('url', ntou('http://cherrypy.org/index?a=1&b=2'))])) + + # Test coordinates sent by <img ismap> + self.getPage('/params/ismap?223,114') + self.assertBody('Coordinates: 223, 114') + + # 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'))])) + + 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', + ): + self.getPage(uri) + self.assertStatus(200) + + error_msgs = [ + 'Missing parameters', + 'Nothing matches the given URI', + 'Multiple values for parameters', + 'Unexpected query string parameters', + 'Unexpected body parameters', + 'Invalid path in Request-URI', + 'Illegal #fragment in Request-URI', + ] + + # uri should be tested for valid absolute path, the status must be 400. + for uri, error_idx in ( + ('invalid/path/without/leading/slash', 5), + ('/valid/path#invalid=fragment', 6), + ): + self.getPage(uri) + self.assertStatus(400) + self.assertInBody(error_msgs[error_idx]) + + # query string parameters are part of the URI, so if they are wrong + # for a particular handler, the status MUST be a 404. + for uri, msg in ( + ('/paramerrors/one_positional', error_msgs[0]), + ('/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/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/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}) + self.getPage(uri) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody('Not Found') + + # 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]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update( + {'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(400) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody('400 Bad') + + # 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]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update( + {'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody('Not Found') + + # 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', + ): + self.getPage(uri, method='GET') + self.assertStatus(500) + self.assertTrue('Client Error', self.body) + + def testErrorHandling(self): + self.getPage('/error/missing') + self.assertStatus(404) + self.assertErrorPage(404, "The path '/error/missing' was not found.") + + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + valerr = '\n raise ValueError()\nValueError' + self.getPage('/error/page_method') + self.assertErrorPage(500, pattern=valerr) + + 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)): + 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. + self.assertStatus(200) + self.assertBody('word up') + 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') + + # No traceback should be present + self.getPage('/error/cause_err_in_finalize') + msg = "Illegal response status from server ('ZOO' is non-numeric)." + self.assertErrorPage(500, msg, None) + finally: + ignore.pop() + + # Test HTTPError with a reason-phrase in the status arg. + self.getPage('/error/reason_phrase') + self.assertStatus("410 Gone fishin'") + + # Test custom error page for a specific error. + self.getPage('/error/custom') + self.assertStatus(404) + self.assertBody('Hello, world\r\n' + (' ' * 499)) + + # Test custom error page for a specific error. + self.getPage('/error/custom?err=401') + self.assertStatus(401) + self.assertBody( + 'Error 401 Unauthorized - ' + "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)) + + # Test error in custom error page (ticket #305). + # Note that the message is escaped for HTML (ticket #310). + self.getPage('/error/noexist') + self.assertStatus(404) + if sys.version_info >= (3, 3): + exc_name = 'FileNotFoundError' + else: + exc_name = 'IOError' + msg = ('No, <b>really</b>, not found!<br />' + 'In addition, the custom error page failed:\n<br />' + '%s: [Errno 2] ' + "No such file or directory: 'nonexistent.html'") % (exc_name,) + self.assertInBody(msg) + + if getattr(cherrypy.server, 'using_apache', False): + pass + else: + # Test throw_errors (ticket #186). + self.getPage('/error/rethrow') + self.assertInBody('raise ValueError()') + + def testExpect(self): + e = ('Expect', '100-continue') + self.getPage('/headerelements/get_elements?headername=Expect', [e]) + self.assertBody('100-continue') + + self.getPage('/expect/expectation_failed', [e]) + self.assertStatus(417) + + def testHeaderElements(self): + # Accept-* header elements should be sorted, with most preferred first. + 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') + + h = [ + ('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') + + # 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' + '*/*') + + # Test Accept-Charset + h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] + self.getPage( + '/headerelements/get_elements?headername=Accept-Charset', h) + self.assertStatus('200 OK') + self.assertBody('iso-8859-5\n' + 'unicode-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) + self.assertStatus('200 OK') + self.assertBody('gzip;q=1.0\n' + 'identity;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) + self.assertStatus('200 OK') + self.assertBody('da\n' + 'en-gb;q=0.8\n' + 'en;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.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.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']: + 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')) + + 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)]) + # 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.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10) + # Note: this is different output for Python3, but it decodes fine. + etag = self.assertHeader( + 'ETag', + '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '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.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.assertBody('application/json') + + def test_basic_HTTPMethods(self): + helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND', + 'PATCH') + + # Test that all defined HTTP methods work. + for m in defined_http_methods: + self.getPage('/method/', method=m) + + # HEAD requests should not return any body. + if m == 'HEAD': + self.assertBody('') + elif m == 'TRACE': + # Some HTTP servers (like modpy) have their own TRACE support + self.assertEqual(self.body[:5], b'TRACE') + else: + self.assertBody(m) + + # 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.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)))] + self.getPage('/method/request_body', headers=h, method='PATCH', body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a PATCH method with a file body but no Content-Type. + # See https://github.com/cherrypy/cherrypy/issues/790. + b = b'one thing on top of another' + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest('PATCH', '/method/request_body', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Length', str(len(b))) + conn.endheaders() + conn.send(b) + response = conn.response_class(conn.sock, method='PATCH') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(b) + finally: + self.persistent = False + + # Request a PATCH method with no body whatsoever (not an empty one). + # See https://github.com/cherrypy/cherrypy/issues/650. + # Provide a C-T or webtest will provide one (and a C-L) for us. + h = [('Content-Type', 'text/plain')] + self.getPage('/method/reachable', headers=h, method='PATCH') + self.assertStatus(411) + + # 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.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)))] + self.getPage('/method/request_body', headers=h, method='PUT', body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a PUT method with a file body but no Content-Type. + # See https://github.com/cherrypy/cherrypy/issues/790. + b = b'one thing on top of another' + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest('PUT', '/method/request_body', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Length', str(len(b))) + conn.endheaders() + conn.send(b) + response = conn.response_class(conn.sock, method='PUT') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(b) + finally: + self.persistent = False + + # Request a PUT method with no body whatsoever (not an empty one). + # See https://github.com/cherrypy/cherrypy/issues/650. + # Provide a C-T or webtest will provide one (and a C-L) for us. + h = [('Content-Type', 'text/plain')] + self.getPage('/method/reachable', headers=h, method='PUT') + self.assertStatus(411) + + # Request a custom method with a request body + b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n' + '<propfind xmlns="DAV:"><prop><getlastmodified/>' + '</prop></propfind>') + 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) + + # Request a disallowed method + self.getPage('/method/', method='LINK') + self.assertStatus(405) + + # Request an unknown method + self.getPage('/method/', method='SEARCH') + self.assertStatus(501) + + # For method dispatchers: make sure that an HTTP method doesn't + # collide with a virtual path atom. If you build HTTP-method + # dispatching into the core, rewrite these handlers to use + # your dispatch idioms. + self.getPage('/divorce/get?ID=13') + self.assertBody('Divorce document 13: empty') + self.assertStatus(200) + self.getPage('/divorce/', method='GET') + self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>') + self.assertStatus(200) + + def test_CONNECT_method(self): + self.persistent = True + try: + conn = self.HTTP_CONN + conn.request('CONNECT', 'created.example.com:3128') + response = conn.response_class(conn.sock, method='CONNECT') + response.begin() + self.assertEqual(response.status, 204) + finally: + self.persistent = False + + self.persistent = True + try: + conn = self.HTTP_CONN + conn.request('CONNECT', 'body.example.com:3128') + response = conn.response_class(conn.sock, method='CONNECT') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(b'CONNECTed to /body.example.com:3128') + finally: + 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']: + self.persistent = True + try: + conn = self.HTTP_CONN + conn.request('CONNECT', request_target) + response = conn.response_class(conn.sock, method='CONNECT') + 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.') + finally: + self.persistent = False + + def testEmptyThreadlocals(self): + results = [] + for x in range(20): + self.getPage('/threadlocal/') + results.append(self.body) + self.assertEqual(results, [b'None'] * 20) diff --git a/libraries/cherrypy/test/test_routes.py b/libraries/cherrypy/test/test_routes.py new file mode 100644 index 00000000..cc714765 --- /dev/null +++ b/libraries/cherrypy/test/test_routes.py @@ -0,0 +1,80 @@ +"""Test Routes dispatcher.""" +import os +import importlib + +import pytest + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class RoutesDispatchTest(helper.CPWebCase): + """Routes dispatcher test suite.""" + + @staticmethod + def setup_server(): + """Set up cherrypy test instance.""" + try: + importlib.import_module('routes') + except ImportError: + 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'), + ], + }) + def index(self, **kwargs): + return 'Welcome to %s, pop. %s' % (self.name, self.population) + + def update(self, **kwargs): + self.population = kwargs['pop'] + 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'])) + d.connect('main', ':action', controller=Dummy()) + + conf = {'/': {'request.dispatch': d}} + cherrypy.tree.mount(root=None, config=conf) + + def test_Routes_Dispatch(self): + """Check that routes package based URI dispatching works correctly.""" + self.getPage('/hounslow') + self.assertStatus('200 OK') + self.assertBody('Welcome to Hounslow, pop. 10000') + + self.getPage('/foo') + self.assertStatus('404 Not Found') + + self.getPage('/surbiton') + self.assertStatus('200 OK') + self.assertBody('Welcome to Surbiton, pop. 10000') + + self.getPage('/surbiton', method='POST', body='pop=1327') + self.assertStatus('200 OK') + self.assertBody('OK') + self.getPage('/surbiton') + self.assertStatus('200 OK') + self.assertHeader('Content-Language', 'en-GB') + self.assertBody('Welcome to Surbiton, pop. 1327') diff --git a/libraries/cherrypy/test/test_session.py b/libraries/cherrypy/test/test_session.py new file mode 100644 index 00000000..0083c97c --- /dev/null +++ b/libraries/cherrypy/test/test_session.py @@ -0,0 +1,512 @@ +import os +import threading +import time +import socket +import importlib + +from six.moves.http_client import HTTPConnection + +import pytest +from path import Path + +import cherrypy +from cherrypy._cpcompat import ( + json_decode, + HTTPSConnection, +) +from cherrypy.lib import sessions +from cherrypy.lib import reprconf +from cherrypy.lib.httputil import response_codes +from cherrypy.test import helper + +localDir = os.path.dirname(__file__) + + +def http_methods_allowed(methods=['GET', 'HEAD']): + method = cherrypy.request.method.upper() + if method not in methods: + cherrypy.response.headers['Allow'] = ', '.join(methods) + raise cherrypy.HTTPError(405) + + +cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed) + + +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), + }) + class Root: + + @cherrypy.expose + def clear(self): + cherrypy.session.cache.clear() + + @cherrypy.expose + def data(self): + cherrypy.session['aha'] = 'foo' + return repr(cherrypy.session._data) + + @cherrypy.expose + def testGen(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + yield str(counter) + + @cherrypy.expose + def testStr(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + return str(counter) + + @cherrypy.expose + @cherrypy.config(**{'tools.sessions.on': False}) + def set_session_cls(self, new_cls_name): + new_cls = reprconf.attributes(new_cls_name) + cfg = {'tools.sessions.storage_class': new_cls} + self.__class__._cp_config.update(cfg) + if hasattr(cherrypy, 'session'): + del cherrypy.session + if new_cls.clean_thread: + new_cls.clean_thread.stop() + new_cls.clean_thread.unsubscribe() + del new_cls.clean_thread + + @cherrypy.expose + def index(self): + sess = cherrypy.session + c = sess.get('counter', 0) + 1 + time.sleep(0.01) + sess['counter'] = c + return str(c) + + @cherrypy.expose + def keyin(self, key): + return str(key in cherrypy.session) + + @cherrypy.expose + def delete(self): + cherrypy.session.delete() + sessions.expire() + return 'done' + + @cherrypy.expose + def delkey(self, key): + del cherrypy.session[key] + return 'OK' + + @cherrypy.expose + def redir_target(self): + return self._cp_config['tools.sessions.storage_class'].__name__ + + @cherrypy.expose + def iredir(self): + raise cherrypy.InternalRedirect('/redir_target') + + @cherrypy.expose + @cherrypy.config(**{ + 'tools.allow.on': True, + 'tools.allow.methods': ['GET'], + }) + def restricted(self): + return cherrypy.request.method + + @cherrypy.expose + def regen(self): + cherrypy.tools.sessions.regenerate() + return 'logged in' + + @cherrypy.expose + 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, + }) + def session_cookie(self): + # Must load() to start the clean thread. + cherrypy.session.load() + return cherrypy.session.id + + cherrypy.tree.mount(Root()) + + +class SessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def tearDown(self): + # Clean up sessions. + for fname in os.listdir(localDir): + if fname.startswith(sessions.FileSession.SESSION_PREFIX): + path = Path(localDir) / fname + path.remove_p() + + @pytest.mark.xfail(reason='#1534') + def test_0_Session(self): + self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') + self.getPage('/clear') + + # Test that a normal request gets the same id in the cookies. + # Note: this wouldn't work if /data didn't load the session. + self.getPage('/data') + self.assertBody("{'aha': 'foo'}") + c = self.cookies[0] + self.getPage('/data', self.cookies) + self.assertEqual(self.cookies[0], c) + + self.getPage('/testStr') + self.assertBody('1') + cookie_parts = dict([p.strip().split('=') + for p in self.cookies[0][1].split(';')]) + # Assert there is an 'expires' param + self.assertEqual(set(cookie_parts.keys()), + set(['session_id', 'expires', 'Path'])) + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/data', self.cookies) + self.assertDictEqual(json_decode(self.body), + {'counter': 3, 'aha': 'foo'}) + self.getPage('/length', self.cookies) + self.assertBody('2') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession') + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(2) + self.getPage('/') + self.assertBody('1') + self.getPage('/length', self.cookies) + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody('True') + cookieset1 = self.cookies + + # Make a new session and test __len__ again + self.getPage('/') + self.getPage('/length', self.cookies) + self.assertBody('2') + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody('done') + self.getPage('/delete', cookieset1) + self.assertBody('done') + + def f(): + return [ + x + for x in os.listdir(localDir) + if x.startswith('session-') + ] + self.assertEqual(f(), []) + + # Wait for the cleanup thread to delete remaining session files + self.getPage('/') + self.assertNotEqual(f(), []) + time.sleep(2) + self.assertEqual(f(), []) + + def test_1_Ram_Concurrency(self): + self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') + self._test_Concurrency() + + @pytest.mark.xfail(reason='#1306') + def test_2_File_Concurrency(self): + self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession') + self._test_Concurrency() + + def _test_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage('/') + self.assertBody('1') + cookies = self.cookies + + data_dict = {} + errors = [] + + def request(index): + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + for i in range(request_count): + c.putrequest('GET', '/') + for k, v in cookies: + c.putheader(k, v) + c.endheaders() + response = c.getresponse() + body = response.read() + if response.status != 200 or not body.isdigit(): + errors.append((response.status, body)) + else: + data_dict[index] = max(data_dict[index], int(body)) + # Uncomment the following line to prove threads overlap. + # sys.stdout.write("%d " % index) + + # Start <request_count> requests from each of + # <client_thread_count> concurrent clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + + for e in errors: + print(e) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody('FileSession') + + def test_4_File_deletion(self): + # Start a new session + self.getPage('/testStr') + # Delete the session file manually and retry. + id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] + path = os.path.join(localDir, 'session-' + id) + os.unlink(path) + self.getPage('/testStr', self.cookies) + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage(404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) + + 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('/regen') + self.assertBody('logged in') + id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] + self.assertNotEqual(id1, id2) + + 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=/;')]) + id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] + self.assertNotEqual(id1, id2) + self.assertNotEqual(id2, 'maliciousid') + + def test_7_session_cookies(self): + self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') + 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(';')]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + id1 = cookie_parts['temp'] + self.assertEqual(list(sessions.RamSession.cache), [id1]) + + # Send another request in the same "browser session". + self.getPage('/session_cookie', self.cookies) + cookie_parts = dict([p.strip().split('=') + for p in self.cookies[0][1].split(';')]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + self.assertBody(id1) + self.assertEqual(list(sessions.RamSession.cache), [id1]) + + # 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(';')]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + # Assert a new id has been generated... + id2 = cookie_parts['temp'] + self.assertNotEqual(id1, id2) + self.assertEqual(set(sessions.RamSession.cache.keys()), + set([id1, id2])) + + # Wait for the session.timeout on both sessions + time.sleep(2.5) + cache = list(sessions.RamSession.cache) + if cache: + if cache == [id2]: + self.fail('The second session did not time out.') + else: + self.fail('Unknown session id in cache: %r', cache) + + def test_8_Ram_Cleanup(self): + def lock(): + s1 = sessions.RamSession() + s1.acquire_lock() + time.sleep(1) + s1.release_lock() + + t = threading.Thread(target=lock) + t.start() + start = time.time() + while not sessions.RamSession.locks and time.time() - start < 5: + time.sleep(0.01) + assert len(sessions.RamSession.locks) == 1, 'Lock not acquired' + s2 = sessions.RamSession() + s2.clean_up() + msg = 'Clean up should not remove active lock' + assert len(sessions.RamSession.locks) == 1, msg + t.join() + + +try: + importlib.import_module('memcache') + + host, port = '127.0.0.1', 11211 + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + raise + break +except (ImportError, socket.error): + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test(self): + return self.skip('memcached not reachable ') +else: + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_0_Session(self): + self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession') + + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/length', self.cookies) + self.assertErrorPage(500) + self.assertInBody('NotImplementedError') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(1.25) + self.getPage('/') + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody('True') + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody('done') + + def test_1_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage('/') + self.assertBody('1') + cookies = self.cookies + + data_dict = {} + + def request(index): + for i in range(request_count): + self.getPage('/', cookies) + # Uncomment the following line to prove threads overlap. + # sys.stdout.write("%d " % index) + if not self.body.isdigit(): + self.fail(self.body) + data_dict[index] = int(self.body) + + # Start <request_count> concurrent requests from + # each of <client_thread_count> clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody('memcached') + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage( + 404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) diff --git a/libraries/cherrypy/test/test_sessionauthenticate.py b/libraries/cherrypy/test/test_sessionauthenticate.py new file mode 100644 index 00000000..63053fcb --- /dev/null +++ b/libraries/cherrypy/test/test_sessionauthenticate.py @@ -0,0 +1,61 @@ +import cherrypy +from cherrypy.test import helper + + +class SessionAuthenticateTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + def check(username, password): + # Dummy check_username_and_password function + if username != 'test' or password != 'password': + return 'Wrong login/password' + + def augment_params(): + # A simple tool to add some things to request.params + # This is to check to make sure that session_auth can handle + # request params (ticket #780) + cherrypy.request.params['test'] = 'test' + + cherrypy.tools.augment_params = cherrypy.Tool( + 'before_handler', augment_params, None, priority=30) + + class Test: + + _cp_config = { + 'tools.sessions.on': True, + 'tools.session_auth.on': True, + 'tools.session_auth.check_username_and_password': check, + 'tools.augment_params.on': True, + } + + @cherrypy.expose + def index(self, **kwargs): + return 'Hi %s, you are logged in' % cherrypy.request.login + + cherrypy.tree.mount(Test()) + + def testSessionAuthenticate(self): + # request a page and check for login form + self.getPage('/') + self.assertInBody('<form method="post" action="do_login">') + + # setup credentials + login_body = 'username=test&password=password&from_page=/' + + # attempt a login + self.getPage('/do_login', method='POST', body=login_body) + self.assertStatus((302, 303)) + + # get the page now that we are logged in + self.getPage('/', self.cookies) + self.assertBody('Hi test, you are logged in') + + # do a logout + self.getPage('/do_logout', self.cookies, method='POST') + self.assertStatus((302, 303)) + + # verify we are logged out + self.getPage('/', self.cookies) + self.assertInBody('<form method="post" action="do_login">') diff --git a/libraries/cherrypy/test/test_states.py b/libraries/cherrypy/test/test_states.py new file mode 100644 index 00000000..606ca4f6 --- /dev/null +++ b/libraries/cherrypy/test/test_states.py @@ -0,0 +1,473 @@ +import os +import signal +import time +import unittest +import warnings + +from six.moves.http_client import BadStatusLine + +import pytest +import portend + +import cherrypy +import cherrypy.process.servers +from cherrypy.test import helper + +engine = cherrypy.engine +thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class Dependency: + + def __init__(self, bus): + self.bus = bus + self.running = False + self.startcount = 0 + self.gracecount = 0 + self.threads = {} + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + self.bus.subscribe('graceful', self.graceful) + self.bus.subscribe('start_thread', self.startthread) + self.bus.subscribe('stop_thread', self.stopthread) + + def start(self): + self.running = True + self.startcount += 1 + + def stop(self): + self.running = False + + def graceful(self): + self.gracecount += 1 + + def startthread(self, thread_id): + self.threads[thread_id] = None + + def stopthread(self, thread_id): + del self.threads[thread_id] + + +db_connection = Dependency(engine) + + +def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'Hello World' + + @cherrypy.expose + def ctrlc(self): + raise KeyboardInterrupt() + + @cherrypy.expose + def graceful(self): + engine.graceful() + return 'app was (gracefully) restarted succesfully' + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'environment': 'test_suite', + }) + + db_connection.subscribe() + +# ------------ Enough helpers. Time for real live test cases. ------------ # + + +class ServerStateTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def setUp(self): + cherrypy.server.socket_timeout = 0.1 + self.do_gc_test = False + + def test_0_NormalStateFlow(self): + engine.stop() + # Our db_connection should not be running + self.assertEqual(db_connection.running, False) + self.assertEqual(db_connection.startcount, 1) + self.assertEqual(len(db_connection.threads), 0) + + # Test server start + engine.start() + self.assertEqual(engine.state, engine.states.STARTED) + + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + portend.occupied(host, port, timeout=0.1) + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.startcount, 2) + self.assertEqual(len(db_connection.threads), 0) + + self.getPage('/') + self.assertBody('Hello World') + self.assertEqual(len(db_connection.threads), 1) + + # Test engine stop. This will also stop the HTTP server. + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + + # Verify that our custom stop function was called + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + # Block the main thread now and verify that exit() works. + def exittest(): + self.getPage('/') + self.assertBody('Hello World') + engine.exit() + cherrypy.server.start() + engine.start_with_callback(exittest) + engine.block() + self.assertEqual(engine.state, engine.states.EXITING) + + def test_1_Restart(self): + cherrypy.server.start() + engine.start() + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + grace = db_connection.gracecount + + self.getPage('/') + self.assertBody('Hello World') + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from this thread + engine.graceful() + self.assertEqual(engine.state, engine.states.STARTED) + self.getPage('/') + self.assertBody('Hello World') + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 1) + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from inside a page handler + self.getPage('/graceful') + self.assertEqual(engine.state, engine.states.STARTED) + self.assertBody('app was (gracefully) restarted succesfully') + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 2) + # Since we are requesting synchronously, is only one thread used? + # Note that the "/graceful" request has been flushed. + self.assertEqual(len(db_connection.threads), 0) + + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + def test_2_KeyboardInterrupt(self): + # Raise a keyboard interrupt in the HTTP server's main thread. + # We must start the server in this, the main thread + engine.start() + cherrypy.server.start() + + self.persistent = True + try: + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody('Hello World') + self.assertNoHeader('Connection') + + cherrypy.server.httpserver.interrupt = KeyboardInterrupt + engine.block() + + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + self.assertEqual(engine.state, engine.states.EXITING) + finally: + self.persistent = False + + # Raise a keyboard interrupt in a page handler; on multithreaded + # servers, this should occur in one of the worker threads. + # This should raise a BadStatusLine error, since the worker + # thread will just die without writing a response. + engine.start() + cherrypy.server.start() + # From python3.5 a new exception is retuned when the connection + # ends abruptly: + # http.client.RemoteDisconnected + # RemoteDisconnected is a subclass of: + # (ConnectionResetError, http.client.BadStatusLine) + # and ConnectionResetError is an indirect subclass of: + # OSError + # From python 3.3 an up socket.error is an alias to OSError + # following PEP-3151, therefore http.client.RemoteDisconnected + # is considered a socket.error. + # + # raise_subcls specifies the classes that are not going + # to be considered as a socket.error for the retries. + # Given that RemoteDisconnected is part BadStatusLine + # we can use the same call for all py3 versions without + # sideffects. python < 3.5 will raise directly BadStatusLine + # which is not a subclass for socket.error/OSError. + try: + self.getPage('/ctrlc', raise_subcls=BadStatusLine) + except BadStatusLine: + pass + else: + print(self.body) + self.fail('AssertionError: BadStatusLine not raised') + + engine.block() + 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. + if engine.state != engine.states.EXITING: + engine.exit() + + # Start the demo script in a new process + p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) + p.write_conf(extra='test_case_name: "test_4_Autoreload"') + p.start(imports='cherrypy.test._test_states_demo') + try: + self.getPage('/start') + start = float(self.body) + + # Give the autoreloader time to cache the file time. + time.sleep(2) + + # Touch the file + os.utime(os.path.join(thisdir, '_test_states_demo.py'), None) + + # Give the autoreloader time to re-exec the process + time.sleep(2) + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + portend.occupied(host, port, timeout=5) + + self.getPage('/start') + if not (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') + p.join() + + def test_5_Start_Error(self): + # If test_3 has not been executed, the server won't be stopped, + # so we'll have to do it. + if engine.state != engine.states.EXITING: + engine.exit() + + # 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.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: + self.fail('Process failed to return nonzero exit code.') + + +class PluginTests(helper.CPWebCase): + + def test_daemonize(self): + if os.name not in ['posix']: + return self.skip('skipped (not on posix) ') + self.HOST = '127.0.0.1' + self.PORT = 8081 + # 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.start(imports='cherrypy.test._test_states_demo') + try: + # Just get the pid of the daemonization process. + self.getPage('/pid') + self.assertStatus(200) + page_pid = int(self.body) + self.assertEqual(page_pid, p.get_pid()) + finally: + # Shut down the spawned process + self.getPage('/exit') + p.join() + + # Wait until here to test the exit code because we want to ensure + # that we wait for the daemon to finish running before we fail. + if p.exit_code != 0: + self.fail('Daemonized parent process failed to exit cleanly.') + + +class SignalHandlingTests(helper.CPWebCase): + + def test_SIGHUP_tty(self): + # When not daemonized, SIGHUP should shut down the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip('skipped (no SIGHUP) ') + + # Spawn the process. + p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) + 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) + # This might hang if things aren't working right, but meh. + p.join() + + def test_SIGHUP_daemonized(self): + # When daemonized, SIGHUP should restart the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip('skipped (no SIGHUP) ') + + if os.name not in ['posix']: + return self.skip('skipped (not on posix) ') + + # 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.start(imports='cherrypy.test._test_states_demo') + + pid = p.get_pid() + try: + # Send a SIGHUP + os.kill(pid, SIGHUP) + # Give the server some time to restart + time.sleep(2) + self.getPage('/pid') + self.assertStatus(200) + new_pid = int(self.body) + self.assertNotEqual(new_pid, pid) + finally: + # Shut down the spawned process + self.getPage('/exit') + p.join() + + def _require_signal_and_kill(self, signal_name): + if not hasattr(signal, signal_name): + self.skip('skipped (no %(signal_name)s)' % vars()) + + if not hasattr(os, 'kill'): + self.skip('skipped (no os.kill)') + + def test_SIGTERM(self): + '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.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), signal.SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + 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.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), signal.SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + def test_signal_handler_unsubscribe(self): + self._require_signal_and_kill('SIGTERM') + + # Although Windows has `os.kill` and SIGTERM is defined, the + # platform does not implement signals and sending SIGTERM + # will result in a forced termination of the process. + # Therefore, this test is not suitable for Windows. + if os.name == 'nt': + self.skip('SIGTERM not available') + + # Spawn a normal, undaemonized process. + p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) + 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) + # This might hang if things aren't working right, but meh. + 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 + ) + + +class WaitTests(unittest.TestCase): + + def test_safe_wait_INADDR_ANY(self): + """ + Wait on INADDR_ANY should not raise IOError + + In cases where the loopback interface does not exist, CherryPy cannot + effectively determine if a port binding to INADDR_ANY was effected. + In this situation, CherryPy should assume that it failed to detect + the binding (not that the binding failed) and only warn that it could + not verify it. + """ + # At such a time that CherryPy can reliably determine one or more + # viable IP addresses of the host, this test may be removed. + + # Simulate the behavior we observe when no loopback interface is + # present by: finding a port that's not occupied, then wait on it. + + free_port = portend.find_available_local_port() + + servers = cherrypy.process.servers + + inaddr_any = '0.0.0.0' + + # Wait on the free port that's unbound + with warnings.catch_warnings(record=True) as w: + with servers._safe_wait(inaddr_any, free_port): + portend.occupied(inaddr_any, free_port, timeout=1) + self.assertEqual(len(w), 1) + self.assertTrue(isinstance(w[0], warnings.WarningMessage)) + self.assertTrue( + 'Unable to verify that the server is bound on ' in str(w[0])) + + # The wait should still raise an IO error if INADDR_ANY was + # not supplied. + with pytest.raises(IOError): + with servers._safe_wait('127.0.0.1', free_port): + portend.occupied('127.0.0.1', free_port, timeout=1) diff --git a/libraries/cherrypy/test/test_static.py b/libraries/cherrypy/test/test_static.py new file mode 100644 index 00000000..5dc5a144 --- /dev/null +++ b/libraries/cherrypy/test/test_static.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +import contextlib +import io +import os +import sys +import platform +import tempfile + +from six import text_type as str +from six.moves import urllib +from six.moves.http_client import HTTPConnection + +import pytest +import py.path + +import cherrypy +from cherrypy.lib import static +from cherrypy._cpcompat import HTTPSConnection, ntou, tonative +from cherrypy.test import helper + + +@pytest.fixture +def unicode_filesystem(tmpdir): + filename = tmpdir / ntou('☃', 'utf-8') + tmpl = 'File system encoding ({encoding}) cannot support unicode filenames' + msg = tmpl.format(encoding=sys.getfilesystemencoding()) + try: + io.open(str(filename), 'w').close() + except UnicodeEncodeError: + pytest.skip(msg) + + +def ensure_unicode_filesystem(): + """ + TODO: replace with simply pytest fixtures once webtest.TestCase + no longer implies unittest. + """ + tmpdir = py.path.local(tempfile.mkdtemp()) + try: + unicode_filesystem(tmpdir) + finally: + tmpdir.remove() + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +has_space_filepath = os.path.join(curdir, 'static', 'has space.html') +bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log') + +# 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 +BIGFILE_SIZE = 32 * MB + + +class StaticTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + if not os.path.exists(has_space_filepath): + 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 + ) + 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): + self.f = static.serve_file(bigfile_filepath) + return self.f + + @cherrypy.expose + def tell(self): + if self.f.input.closed: + return '' + return repr(self.f.input.tell()).rstrip('L') + + @cherrypy.expose + def fileobj(self): + f = open(os.path.join(curdir, 'style.css'), 'rb') + return static.serve_fileobj(f, content_type='text/css') + + @cherrypy.expose + def bytesio(self): + f = io.BytesIO(b'Fee\nfie\nfo\nfum') + return static.serve_fileobj(f, content_type='text/plain') + + class Static: + + @cherrypy.expose + def index(self): + return 'You want the Baron? You can have the Baron!' + + @cherrypy.expose + def dynamic(self): + return 'This is a DYNAMIC page' + + root = Root() + root.static = Static() + + rootconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + '/static-long': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': r'\\?\%s' % curdir, + }, + '/style.css': { + 'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(curdir, 'style.css'), + }, + '/docroot': { + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, + '/error': { + 'tools.staticdir.on': True, + 'request.show_tracebacks': True, + }, + '/404test': { + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'error_page.404': error_page_404, + } + } + rootApp = cherrypy.Application(root) + rootApp.merge(rootconf) + + test_app_conf = { + '/test': { + 'tools.staticdir.index': 'index.html', + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + }, + } + testApp = cherrypy.Application(Static()) + testApp.merge(test_app_conf) + + vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) + cherrypy.tree.graft(vhost) + + @staticmethod + def teardown_server(): + for f in (has_space_filepath, bigfile_filepath): + if os.path.exists(f): + try: + os.unlink(f) + except Exception: + pass + + def test_static(self): + self.getPage('/static/index.html') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Using a staticdir.root value in a subdir... + self.getPage('/docroot/index.html') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Check a filename with spaces in it + self.getPage('/static/has%20space.html') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + self.getPage('/style.css') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css') + # Note: The body should be exactly 'Dummy stylesheet\n', but + # unfortunately some tools such as WinZip sometimes turn \n + # into \r\n on Windows when extracting the CherryPy tarball so + # we just check the content + self.assertMatchesBody('^Dummy stylesheet') + + @pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only') + def test_static_longpath(self): + """Test serving of a file in subdir of a Windows long-path + staticdir.""" + self.getPage('/static-long/static/index.html') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + def test_fallthrough(self): + # Test that NotFound will then try dynamic handlers (see [878]). + self.getPage('/static/dynamic') + self.assertBody('This is a DYNAMIC page') + + # Check a directory via fall-through to dynamic handler. + self.getPage('/static/') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('You want the Baron? You can have the Baron!') + + def test_index(self): + # Check a directory via "staticdir.index". + self.getPage('/docroot/') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + # The same page should be returned even if redirected. + self.getPage('/docroot') + self.assertStatus(301) + self.assertHeader('Location', '%s/docroot/' % self.base()) + self.assertMatchesBody( + "This resource .* <a href=(['\"])%s/docroot/\\1>" + '%s/docroot/</a>.' + % (self.base(), self.base()) + ) + + def test_config_errors(self): + # Check that we get an error if no .file or .dir + self.getPage('/error/thing.html') + self.assertErrorPage(500) + if sys.version_info >= (3, 3): + errmsg = ( + r'TypeError: staticdir\(\) missing 2 ' + 'required positional arguments' + ) + else: + errmsg = ( + r'TypeError: staticdir\(\) takes at least 2 ' + r'(positional )?arguments \(0 given\)' + ) + self.assertMatchesBody(errmsg.encode('ascii')) + + def test_security(self): + # Test up-level security + self.getPage('/static/../../test/style.css') + self.assertStatus((400, 403)) + + def test_modif(self): + # Test modified-since on a reasonably-large file + self.getPage('/static/dirback.jpg') + self.assertStatus('200 OK') + lastmod = '' + for k, v in self.headers: + if k == 'Last-Modified': + lastmod = v + ims = ('If-Modified-Since', lastmod) + self.getPage('/static/dirback.jpg', headers=[ims]) + self.assertStatus(304) + self.assertNoHeader('Content-Type') + self.assertNoHeader('Content-Length') + self.assertNoHeader('Content-Disposition') + self.assertBody('') + + def test_755_vhost(self): + self.getPage('/test/', [('Host', 'virt.net')]) + self.assertStatus(200) + self.getPage('/test', [('Host', 'virt.net')]) + self.assertStatus(301) + self.assertHeader('Location', self.scheme + '://virt.net/test/') + + def test_serve_fileobj(self): + self.getPage('/fileobj') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + self.assertMatchesBody('^Dummy stylesheet') + + def test_serve_bytesio(self): + self.getPage('/bytesio') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.assertHeader('Content-Length', 14) + self.assertMatchesBody('Fee\nfie\nfo\nfum') + + @pytest.mark.xfail(reason='#1475') + def test_file_stream(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/bigfile', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + + body = b'' + remaining = BIGFILE_SIZE + while remaining > 0: + data = response.fp.read(65536) + if not data: + break + body += data + remaining -= len(data) + + if self.scheme == 'https': + newconn = HTTPSConnection + else: + newconn = HTTPConnection + s, h, b = helper.webtest.openURL( + 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 + else: + tell_position = int(b) + + read_so_far = len(body) + + # It is difficult for us to force the server to only read + # the bytes that we ask for - there are going to be buffers + # inbetween. + # + # CherryPy will attempt to write as much data as it can to + # the socket, and we don't have a way to determine what that + # size will be. So we make the following assumption - by + # the time we have read in the entire file on the server, + # we will have at least received half of it. If this is not + # the case, then this is an indicator that either: + # - machines that are running this test are using buffer + # sizes greater than half of BIGFILE_SIZE; or + # - streaming is broken. + # + # At the time of writing, we seem to have encountered + # buffer sizes bigger than 512K, so we've increased + # BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB. + # This test is going to keep failing according to the + # improvements in hardware and OS buffers. + if tell_position >= BIGFILE_SIZE: + if read_so_far < (BIGFILE_SIZE / 2): + self.fail( + '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) + 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)) + + if body != b'x' * BIGFILE_SIZE: + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (BIGFILE_SIZE, body[:50], len(body))) + conn.close() + + def test_file_stream_deadlock(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Make an initial request but abort early. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/bigfile', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + 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))) + response.close() + conn.close() + + # Make a second request, which should fetch the whole file. + 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))) + + def test_error_page_with_serve_file(self): + self.getPage('/404test/yunyeen') + self.assertStatus(404) + self.assertInBody("I couldn't find that thing") + + def test_null_bytes(self): + self.getPage('/static/\x00') + self.assertStatus('404 Not Found') + + @staticmethod + @contextlib.contextmanager + def unicode_file(): + filename = ntou('Слава Україні.html', 'utf-8') + filepath = os.path.join(curdir, 'static', filename) + with io.open(filepath, 'w', encoding='utf-8') as strm: + strm.write(ntou('Героям Слава!', 'utf-8')) + try: + yield + finally: + os.remove(filepath) + + py27_on_windows = ( + platform.system() == 'Windows' and + sys.version_info < (3,) + ) + @pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301 + def test_unicode(self): + ensure_unicode_filesystem() + with self.unicode_file(): + url = ntou('/static/Слава Україні.html', 'utf-8') + # quote function requires str + url = tonative(url, 'utf-8') + url = urllib.parse.quote(url) + self.getPage(url) + + expected = ntou('Героям Слава!', 'utf-8') + self.assertInBody(expected) + + +def error_page_404(status, message, traceback, version): + path = os.path.join(curdir, 'static', '404.html') + return static.serve_file(path, content_type='text/html') diff --git a/libraries/cherrypy/test/test_tools.py b/libraries/cherrypy/test/test_tools.py new file mode 100644 index 00000000..a73a3898 --- /dev/null +++ b/libraries/cherrypy/test/test_tools.py @@ -0,0 +1,468 @@ +"""Test the various means of instantiating and invoking tools.""" + +import gzip +import io +import sys +import time +import types +import unittest +import operator + +import six +from six.moves import range, map +from six.moves.http_client import IncompleteRead + +import cherrypy +from cherrypy import tools +from cherrypy._cpcompat import ntou +from cherrypy.test import helper, _test_decorators + + +timeout = 0.2 +europoundUnicode = ntou('\x80\xa3') + + +# Client-side code # + + +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) + + def numerify(): + def number_it(body): + for chunk in 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(six.iteritems(m)) + cherrypy.request.hooks.attach('on_start_resource', makemap) + + def critical(): + cherrypy.request.error_response = cherrypy.HTTPError( + 502).set_response + critical.failsafe = True + + cherrypy.request.hooks.attach('on_start_resource', critical) + cherrypy.request.hooks.attach(self._point, self.callable) + + tools.numerify = NumTool('before_finalize', numerify) + + # It's not mandatory to inherit from cherrypy.Tool. + class NadsatTool: + + def __init__(self): + self.ended = {} + self._name = 'nadsat' + + def nadsat(self): + def nadsat_it_up(body): + for chunk in 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): + # This runs after the request has been completely written out. + cherrypy.response.body = [b'razdrez'] + 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(): + cherrypy.request.process_request_body = False + clen = int(cherrypy.request.headers['Content-Length']) + cherrypy.request.body = cherrypy.request.rfile.read(clen) + + # 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() + if six.PY3: + r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] + else: + r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] + cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) + + def stream_handler(next_handler, *args, **kwargs): + actual = cherrypy.request.config.get('tools.streamer.arg') + assert actual == 'arg value' + cherrypy.response.output = o = io.BytesIO() + try: + next_handler(*args, **kwargs) + # Ignore the response and return our accumulated output + # instead. + return o.getvalue() + finally: + o.close() + cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool( + 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', + }) + def tarfile(self): + actual = cherrypy.request.config.get('tools.streamer.arg') + assert actual == 'arg value' + cherrypy.response.output.write(b'I am ') + cherrypy.response.output.write(b'a tarfile') + + @cherrypy.expose + def euro(self): + hooks = list(cherrypy.request.hooks['before_finalize']) + hooks.sort() + cbnames = [x.callback.__name__ for x in hooks] + assert cbnames == ['gzip'], cbnames + priorities = [x.priority for x in hooks] + assert priorities == [80], priorities + yield ntou('Hello,') + yield ntou('world') + yield europoundUnicode + + # Bare hooks + @cherrypy.expose + @cherrypy.config(**{'hooks.before_request_body': pipe_body}) + def pipe(self): + return cherrypy.request.body + + # Multiple decorators; include kwargs just for fun. + # Note that rotator must run before gzip. + @cherrypy.expose + 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) + + root = Root() + + class TestType(type): + """Metaclass which automatically exposes all functions in each + 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 six.itervalues(dct): + 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' + + def ended(self, id): + return repr(tools.nadsat.ended[id]) + + def err(self, id=None): + raise ValueError() + + def errinstream(self, id=None): + yield 'nonconfidential' + raise ValueError() + yield 'confidential' + + # METHOD TWO: decorator using Tool() + # We support Python 2.3, but the @-deco syntax would look like + # this: + # @tools.check_access() + def restricted(self): + return 'Welcome!' + restricted = myauthtools.check_access()(restricted) + userid = restricted + + def err_in_onstart(self): + return 'success!' + + @cherrypy.config(**{'response.stream': True}) + def stream(self, id=None): + for x in range(100000000): + yield str(x) + + conf = { + # METHOD THREE: + # Declare Tools in detached config + '/demo': { + 'tools.numerify.on': True, + 'tools.numerify.map': {b'pie': b'3.14159'}, + }, + '/demo/restricted': { + 'request.show_tracebacks': False, + }, + '/demo/userid': { + 'request.show_tracebacks': False, + 'myauth.check_access.default': True, + }, + '/demo/errinstream': { + 'response.stream': True, + }, + '/demo/err_in_onstart': { + # Because this isn't a dict, on_start_resource will error. + 'tools.numerify.map': 'pie->3.14159' + }, + # Combined tools + '/euro': { + 'tools.gzip.on': True, + 'tools.encode.on': True, + }, + # Priority specified in config + '/decorated_euro/subpath': { + 'tools.gzip.priority': 10, + }, + # Handler wrappers + '/tarfile': {'tools.streamer.on': True} + } + app = cherrypy.tree.mount(root, config=conf) + app.request_class.namespaces['myauth'] = myauthtools + + root.tooldecs = _test_decorators.ToolExamples() + + def testHookErrors(self): + self.getPage('/demo/?id=1') + # If body is "razdrez", then on_end_request is being called too early. + self.assertBody('A horrorshow lomtick of cherry 3.14159') + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage('/demo/ended/1') + self.assertBody('True') + + valerr = '\n raise ValueError()\nValueError' + self.getPage('/demo/err?id=3') + # If body is "razdrez", then on_end_request is being called too early. + self.assertErrorPage(502, pattern=valerr) + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage('/demo/ended/3') + 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)): + 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. + self.assertStatus('200 OK') + self.assertBody('nonconfidential') + else: + # 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') + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage('/demo/ended/5') + self.assertBody('True') + + # Test the "__call__" technique (compile-time decorator). + self.getPage('/demo/restricted') + self.assertErrorPage(401) + + # Test compile-time decorator with kwargs from config. + self.getPage('/demo/userid') + self.assertBody('Welcome!') + + def testEndRequestOnDrop(self): + old_timeout = None + try: + httpserver = cherrypy.server.httpserver + old_timeout = httpserver.timeout + except (AttributeError, IndexError): + return self.skip() + + try: + httpserver.timeout = timeout + + # Test that on_end_request is called even if the client drops. + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest('GET', '/demo/stream?id=9', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + # Skip the rest of the request and close the conn. This will + # cause the server's active socket to error, which *should* + # result in the request being aborted, and request.close being + # called all the way up the stack (including WSGI middleware), + # eventually calling our on_end_request hook. + finally: + self.persistent = False + time.sleep(timeout * 2) + # Test that the on_end_request hook was called. + self.getPage('/demo/ended/9') + self.assertBody('True') + finally: + if old_timeout is not None: + httpserver.timeout = old_timeout + + def testGuaranteedHooks(self): + # The 'critical' on_start_resource hook is 'failsafe' (guaranteed + # to run even if there are failures in other on_start methods). + # This is NOT true of the other hooks. + # Here, we have set up a failure in NumerifyTool.numerify_map, + # but our 'critical' hook should run and set the error to 502. + self.getPage('/demo/err_in_onstart') + self.assertErrorPage(502) + tmpl = "AttributeError: 'str' object has no attribute '{attr}'" + expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems') + self.assertInBody(expected_msg) + + def testCombinedTools(self): + 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.assertInBody(zbuf.getvalue()[:3]) + + zbuf = io.BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6) + zfile.write(expectedResult) + zfile.close() + + self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')]) + self.assertInBody(zbuf.getvalue()[:3]) + + # This returns a different value because gzip's priority was + # 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')]) + if six.PY3: + self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) + else: + self.assertInBody(''.join([chr((ord(x) + 3) % 256) + for x in zbuf.getvalue()])) + + 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.assertBody(content) + + def testHandlerWrapperTool(self): + self.getPage('/tarfile') + self.assertBody('I am a tarfile') + + def testToolWithConfig(self): + if not sys.version_info >= (2, 5): + return self.skip('skipped (Python 2.5+ only)') + + self.getPage('/tooldecs/blah') + self.assertHeader('Content-Type', 'application/data') + + def testWarnToolOn(self): + # get + try: + cherrypy.tools.numerify.on + except AttributeError: + pass + else: + raise AssertionError('Tool.on did not error as it should have.') + + # set + try: + cherrypy.tools.numerify.on = True + except AttributeError: + pass + else: + raise AssertionError('Tool.on did not error as it should have.') + + 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, + ) + def example(): + pass + self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool)) + self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize') + self.assertEqual(cherrypy.tools.renamed._name, 'renamed') + self.assertEqual(cherrypy.tools.renamed._priority, 60) + + +class SessionAuthTest(unittest.TestCase): + + def test_login_screen_returns_bytes(self): + """ + login_screen must return bytes even if unicode parameters are passed. + Issue 1132 revealed that login_screen would return unicode if the + username and password were unicode. + """ + sa = cherrypy.lib.cptools.SessionAuth() + res = sa.login_screen(None, username=six.text_type('nobody'), + password=six.text_type('anypass')) + self.assertTrue(isinstance(res, bytes)) + + +class TestHooks: + def test_priorities(self): + """ + Hooks should sort by priority order. + """ + Hook = cherrypy._cprequest.Hook + hooks = [ + Hook(None, priority=48), + Hook(None), + Hook(None, priority=49), + ] + hooks.sort() + by_priority = operator.attrgetter('priority') + priorities = list(map(by_priority, hooks)) + assert priorities == [48, 49, 50] diff --git a/libraries/cherrypy/test/test_tutorials.py b/libraries/cherrypy/test/test_tutorials.py new file mode 100644 index 00000000..efa35b99 --- /dev/null +++ b/libraries/cherrypy/test/test_tutorials.py @@ -0,0 +1,210 @@ +import sys +import imp +import types +import importlib + +import six + +import cherrypy +from cherrypy.test import helper + + +class TutorialTest(helper.CPWebCase): + + @classmethod + def setup_server(cls): + """ + Mount something so the engine starts. + """ + class Dummy: + pass + cherrypy.tree.mount(Dummy()) + + @staticmethod + def load_module(name): + """ + Import or reload tutorial module as needed. + """ + target = 'cherrypy.tutorial.' + name + if target in sys.modules: + module = imp.reload(sys.modules[target]) + else: + module = importlib.import_module(target) + return module + + @classmethod + def setup_tutorial(cls, name, root_name, config={}): + cherrypy.config.reset() + module = cls.load_module(name) + root = getattr(module, root_name) + conf = getattr(module, 'tutconf') + class_types = type, + if six.PY2: + class_types += types.ClassType, + if isinstance(root, class_types): + root = root() + cherrypy.tree.mount(root, config=conf) + cherrypy.config.update(config) + + def test01HelloWorld(self): + self.setup_tutorial('tut01_helloworld', 'HelloWorld') + self.getPage('/') + self.assertBody('Hello world!') + + def test02ExposeMethods(self): + self.setup_tutorial('tut02_expose_methods', 'HelloWorld') + self.getPage('/show_msg') + self.assertBody('Hello world!') + + def test03GetAndPost(self): + self.setup_tutorial('tut03_get_and_post', 'WelcomePage') + + # Try different GET queries + self.getPage('/greetUser?name=Bob') + self.assertBody("Hey Bob, what's up?") + + self.getPage('/greetUser') + self.assertBody('Please enter your name <a href="./">here</a>.') + + self.getPage('/greetUser?name=') + self.assertBody('No, really, enter your name <a href="./">here</a>.') + + # Try the same with POST + self.getPage('/greetUser', method='POST', body='name=Bob') + self.assertBody("Hey Bob, what's up?") + + self.getPage('/greetUser', method='POST', body='name=') + self.assertBody('No, really, enter your name <a href="./">here</a>.') + + def test04ComplexSite(self): + self.setup_tutorial('tut04_complex_site', 'root') + + msg = ''' + <p>Here are some extra useful links:</p> + + <ul> + <li><a href="http://del.icio.us">del.icio.us</a></li> + <li><a href="http://www.cherrypy.org">CherryPy</a></li> + </ul> + + <p>[<a href="../">Return to links page</a>]</p>''' + self.getPage('/links/extra/') + self.assertBody(msg) + + def test05DerivedObjects(self): + self.setup_tutorial('tut05_derived_objects', 'HomePage') + msg = ''' + <html> + <head> + <title>Another Page</title> + <head> + <body> + <h2>Another Page</h2> + + <p> + And this is the amazing second page! + </p> + + </body> + </html> + ''' + # the tutorial has some annoying spaces in otherwise blank lines + msg = msg.replace('</h2>\n\n', '</h2>\n \n') + msg = msg.replace('</p>\n\n', '</p>\n \n') + self.getPage('/another/') + self.assertBody(msg) + + def test06DefaultMethod(self): + self.setup_tutorial('tut06_default_method', 'UsersPage') + self.getPage('/hendrik') + self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' + '(<a href="./">back</a>)') + + def test07Sessions(self): + self.setup_tutorial('tut07_sessions', 'HitCounter') + + self.getPage('/') + self.assertBody( + "\n During your current session, you've viewed this" + '\n page 1 times! Your life is a patio of fun!' + '\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 ') + + def test08GeneratorsAndYield(self): + self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo') + self.getPage('/') + self.assertBody('<html><body><h2>Generators rule!</h2>' + '<h3>List of users:</h3>' + 'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>' + '</body></html>') + + 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') + b += 'a' * filesize + '\n' + '--x--\n' + self.getPage('/upload', h, 'POST', b) + self.assertBody('''<html> + <body> + myFile length: %d<br /> + myFile filename: hello.txt<br /> + myFile mime-type: text/plain + </body> + </html>''' % 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.assertEqual(len(self.body), 85698) + + def test10HTTPErrors(self): + self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo') + + @cherrypy.expose + def traceback_setting(): + return repr(cherrypy.request.show_tracebacks) + cherrypy.tree.mount(traceback_setting, '/traceback_setting') + + self.getPage('/') + self.assertInBody("""<a href="toggleTracebacks">""") + self.assertInBody("""<a href="/doesNotExist">""") + self.assertInBody("""<a href="/error?code=403">""") + self.assertInBody("""<a href="/error?code=500">""") + self.assertInBody("""<a href="/messageArg">""") + + self.getPage('/traceback_setting') + setting = self.body + self.getPage('/toggleTracebacks') + self.assertStatus((302, 303)) + self.getPage('/traceback_setting') + self.assertBody(str(not eval(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.getPage('/error?code=403') + self.assertStatus(403) + self.assertInBody("<h2>You can't do that!</h2>") + + self.getPage('/messageArg') + self.assertStatus(500) + self.assertInBody("If you construct an HTTPError with a 'message'") diff --git a/libraries/cherrypy/test/test_virtualhost.py b/libraries/cherrypy/test/test_virtualhost.py new file mode 100644 index 00000000..de88f927 --- /dev/null +++ b/libraries/cherrypy/test/test_virtualhost.py @@ -0,0 +1,113 @@ +import os + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class VirtualHostTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'Hello, world' + + @cherrypy.expose + def dom4(self): + return 'Under construction' + + @cherrypy.expose + def method(self, value): + return 'You sent %s' % value + + class VHost: + + def __init__(self, sitename): + self.sitename = sitename + + @cherrypy.expose + def index(self): + return 'Welcome to %s' % self.sitename + + @cherrypy.expose + def vmethod(self, value): + return 'You sent %s' % value + + @cherrypy.expose + def url(self): + return cherrypy.url('nextpage') + + # Test static as a handler (section must NOT include vhost prefix) + static = cherrypy.tools.staticdir.handler( + 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', + }, + }) + + def testVirtualHost(self): + self.getPage('/', [('Host', 'www.mydom1.com')]) + self.assertBody('Hello, world') + self.getPage('/mydom2/', [('Host', 'www.mydom1.com')]) + self.assertBody('Welcome to Domain 2') + + self.getPage('/', [('Host', 'www.mydom2.com')]) + self.assertBody('Welcome to Domain 2') + self.getPage('/', [('Host', 'www.mydom3.com')]) + self.assertBody('Welcome to Domain 3') + self.getPage('/', [('Host', 'www.mydom4.com')]) + self.assertBody('Under construction') + + # Test GET, POST, and positional params + self.getPage('/method?value=root') + 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.assertBody('You sent dom3 POST') + self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')]) + self.assertBody('You sent pos') + + # Test that cherrypy.url uses the browser url, not the virtual url + self.getPage('/url', [('Host', 'www.mydom2.com')]) + self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme) + + def test_VHost_plus_Static(self): + # Test static as a handler + self.getPage('/static/style.css', [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + + # Test static in config + self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg']) + + # Test static config with "index" arg + self.getPage('/static2/', [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertBody('Hello, world\r\n') + # Since tools.trailing_slash is on by default, this should redirect + self.getPage('/static2', [('Host', 'www.mydom2.com')]) + self.assertStatus(301) diff --git a/libraries/cherrypy/test/test_wsgi_ns.py b/libraries/cherrypy/test/test_wsgi_ns.py new file mode 100644 index 00000000..3545724c --- /dev/null +++ b/libraries/cherrypy/test/test_wsgi_ns.py @@ -0,0 +1,93 @@ +import cherrypy +from cherrypy.test import helper + + +class WSGI_Namespace_Test(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + def __iter__(self): + return self + + def next(self): + return self.iter.next() + + def __next__(self): + return next(self.iter) + + def close(self): + if hasattr(self.appresults, 'close'): + self.appresults.close() + + class ChangeCase(object): + + def __init__(self, app, to=None): + self.app = app + self.to = to + + 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 + + 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(): + line = line.replace(k, v) + return line + + def __next__(this): + line = next(this.iter) + 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'}, + } + + app = cherrypy.Application(Root()) + app.wsgiapp.pipeline.append(('changecase', ChangeCase)) + app.wsgiapp.config['changecase'] = {'to': 'upper'} + cherrypy.tree.mount(app, config={'/': root_conf}) + + def test_pipeline(self): + if not cherrypy.server.httpserver: + return self.skip() + + self.getPage('/') + # If body is "HEXXO WORXD!", the middleware was applied out of order. + self.assertBody('HERRO WORRD!') diff --git a/libraries/cherrypy/test/test_wsgi_unix_socket.py b/libraries/cherrypy/test/test_wsgi_unix_socket.py new file mode 100644 index 00000000..8f1cc00b --- /dev/null +++ b/libraries/cherrypy/test/test_wsgi_unix_socket.py @@ -0,0 +1,93 @@ +import os +import socket +import atexit +import tempfile + +from six.moves.http_client import HTTPConnection + +import pytest + +import cherrypy +from cherrypy.test import helper + + +def usocket_path(): + fd, path = tempfile.mkstemp('cp_test.sock') + os.close(fd) + os.remove(path) + return path + + +USOCKET_PATH = usocket_path() + + +class USocketHTTPConnection(HTTPConnection): + """ + HTTPConnection over a unix socket. + """ + + def __init__(self, path): + HTTPConnection.__init__(self, 'localhost') + self.path = path + + def __call__(self, *args, **kwargs): + """ + Catch-all method just to present itself as a constructor for the + HTTPConnection. + """ + return self + + def connect(self): + """ + Override the connect method and assign a unix socket as a transport. + """ + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock + atexit.register(lambda: os.remove(self.path)) + + +@pytest.mark.skipif("sys.platform == 'win32'") +class WSGI_UnixSocket_Test(helper.CPWebCase): + """ + Test basic behavior on a cherrypy wsgi server listening + on a unix socket. + + 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' + + @cherrypy.expose + def error(self): + raise Exception('Invalid page') + + config = { + 'server.socket_file': USOCKET_PATH + } + cherrypy.config.update(config) + cherrypy.tree.mount(Root()) + + def tearDown(self): + cherrypy.config.update({'server.socket_file': None}) + + def test_simple_request(self): + self.getPage('/') + self.assertStatus('200 OK') + self.assertInBody('Test OK') + + def test_not_found(self): + self.getPage('/invalid_path') + self.assertStatus('404 Not Found') + + def test_internal_error(self): + self.getPage('/error') + self.assertStatus('500 Internal Server Error') + self.assertInBody('Invalid page') diff --git a/libraries/cherrypy/test/test_wsgi_vhost.py b/libraries/cherrypy/test/test_wsgi_vhost.py new file mode 100644 index 00000000..2b6e5ba9 --- /dev/null +++ b/libraries/cherrypy/test/test_wsgi_vhost.py @@ -0,0 +1,35 @@ +import cherrypy +from cherrypy.test import helper + + +class WSGI_VirtualHost_Test(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class ClassOfRoot(object): + + def __init__(self, name): + self.name = name + + @cherrypy.expose + def index(self): + return 'Welcome to the %s website!' % self.name + + default = cherrypy.Application(None) + + domains = {} + for year in range(1997, 2008): + app = cherrypy.Application(ClassOfRoot('Class of %s' % year)) + domains['www.classof%s.example' % year] = app + + cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains)) + + def test_welcome(self): + if not cherrypy.server.using_wsgi: + return self.skip('skipped (not using WSGI)... ') + + for year in range(1997, 2008): + self.getPage( + '/', headers=[('Host', 'www.classof%s.example' % year)]) + self.assertBody('Welcome to the Class of %s website!' % year) diff --git a/libraries/cherrypy/test/test_wsgiapps.py b/libraries/cherrypy/test/test_wsgiapps.py new file mode 100644 index 00000000..1b3bf28f --- /dev/null +++ b/libraries/cherrypy/test/test_wsgiapps.py @@ -0,0 +1,120 @@ +import sys + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.test import helper + + +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'] + keys = list(environ.keys()) + keys.sort() + for k in keys: + output.append('%s: %s\n' % (k, environ[k])) + return [ntob(x, 'utf-8') for x in output] + + def test_empty_string_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + return [ + b'Hello', b'', b' ', b'', b'world', + ] + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + 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() + + def close(self): + if hasattr(self.appresults, 'close'): + self.appresults.close() + + class ReversingMiddleware(object): + + def __init__(self, app): + self.app = app + + 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() + return ''.join(line) + + return Reverser(results) + + class Root: + + @cherrypy.expose + def index(self): + return ntob("I'm a regular CherryPy page handler!") + + cherrypy.tree.mount(Root()) + + cherrypy.tree.graft(test_app, '/hosted/app1') + cherrypy.tree.graft(test_empty_string_app, '/hosted/app3') + + # Set script_name explicitly to None to signal CP that it should + # be pulled from the WSGI environ each time. + 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!''' + + def test_01_standard_app(self): + self.getPage('/') + self.assertBody("I'm a regular CherryPy page handler!") + + def test_04_pure_wsgi(self): + if not cherrypy.server.using_wsgi: + return self.skip('skipped (not using WSGI)... ') + self.getPage('/hosted/app1') + self.assertHeader('Content-Type', 'text/plain') + self.assertInBody(self.wsgi_output) + + def test_05_wrapped_cp_app(self): + if not cherrypy.server.using_wsgi: + return self.skip('skipped (not using WSGI)... ') + self.getPage('/hosted/app2/') + body = list("I'm a regular CherryPy page handler!") + body.reverse() + body = ''.join(body) + self.assertInBody(body) + + def test_06_empty_string_app(self): + if not cherrypy.server.using_wsgi: + return self.skip('skipped (not using WSGI)... ') + self.getPage('/hosted/app3') + self.assertHeader('Content-Type', 'text/plain') + self.assertInBody('Hello world') diff --git a/libraries/cherrypy/test/test_xmlrpc.py b/libraries/cherrypy/test/test_xmlrpc.py new file mode 100644 index 00000000..ad93b821 --- /dev/null +++ b/libraries/cherrypy/test/test_xmlrpc.py @@ -0,0 +1,183 @@ +import sys + +import six + +from six.moves.xmlrpc_client import ( + DateTime, Fault, + ProtocolError, ServerProxy, SafeTransport +) + +import cherrypy +from cherrypy import _cptools +from cherrypy.test import helper + +if six.PY3: + HTTPSTransport = SafeTransport + + # Python 3.0's SafeTransport still mistakenly checks for socket.ssl + import socket + if not hasattr(socket, 'ssl'): + socket.ssl = True +else: + class HTTPSTransport(SafeTransport): + + """Subclass of SafeTransport to fix sock.recv errors (by using file). + """ + + def request(self, host, handler, request_body, verbose=0): + # issue XML-RPC request + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + errcode, errmsg, headers = h.getreply() + if errcode != 200: + raise ProtocolError(host + handler, errcode, errmsg, headers) + + self.verbose = verbose + + # Here's where we differ from the superclass. It says: + # try: + # sock = h._conn.sock + # except AttributeError: + # sock = None + # return self._parse_response(h.getfile(), sock) + + return self.parse_response(h.getfile()) + + +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!' + + @cherrypy.expose + def return_single_item_list(self): + return [42] + + @cherrypy.expose + def return_string(self): + return 'here is a string' + + @cherrypy.expose + def return_tuple(self): + return ('here', 'is', 1, 'tuple') + + @cherrypy.expose + def return_dict(self): + return dict(a=1, b=2, c=3) + + @cherrypy.expose + def return_composite(self): + return dict(a=1, z=26), 'hi', ['welcome', 'friend'] + + @cherrypy.expose + def return_int(self): + return 42 + + @cherrypy.expose + def return_float(self): + return 3.14 + + @cherrypy.expose + def return_datetime(self): + return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)) + + @cherrypy.expose + def return_boolean(self): + return True + + @cherrypy.expose + def test_argument_passing(self, num): + return num * 2 + + @cherrypy.expose + def test_returning_Fault(self): + return Fault(1, 'custom Fault response') + + root = Root() + root.xmlrpc = XmlRpc() + 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) + proxy = ServerProxy(url, transport=HTTPSTransport()) + else: + url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT) + proxy = ServerProxy(url) + + # begin the tests ... + self.getPage('/xmlrpc/foo') + self.assertBody('Hello world!') + + 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_dict(), {'a': 1, 'c': 3, 'b': 2}) + 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_boolean(), True) + self.assertEqual(proxy.test_argument_passing(22), 22 * 2) + + # Test an error in the page handler (should raise an xmlrpclib.Fault) + try: + proxy.test_argument_passing({}) + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ('unsupported operand type(s) ' + "for *: 'dict' and 'int'")) + else: + self.fail('Expected xmlrpclib.Fault') + + # https://github.com/cherrypy/cherrypy/issues/533 + # if a method is not found, an xmlrpclib.Fault should be raised + try: + proxy.non_method() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, + 'method "non_method" is not supported') + else: + self.fail('Expected xmlrpclib.Fault') + + # Test returning a Fault from the page handler. + try: + proxy.test_returning_Fault() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ('custom Fault response')) + else: + self.fail('Expected xmlrpclib.Fault') diff --git a/libraries/cherrypy/test/webtest.py b/libraries/cherrypy/test/webtest.py new file mode 100644 index 00000000..9fb6ce62 --- /dev/null +++ b/libraries/cherrypy/test/webtest.py @@ -0,0 +1,11 @@ +# for compatibility, expose cheroot webtest here +import warnings + +from cheroot.test.webtest import ( # noqa + interface, + WebCase, cleanHeaders, shb, openURL, + ServerError, server_error, +) + + +warnings.warn('Use cheroot.test.webtest', DeprecationWarning) diff --git a/libraries/cherrypy/tutorial/README.rst b/libraries/cherrypy/tutorial/README.rst new file mode 100644 index 00000000..c47e7d32 --- /dev/null +++ b/libraries/cherrypy/tutorial/README.rst @@ -0,0 +1,16 @@ +CherryPy Tutorials +------------------ + +This is a series of tutorials explaining how to develop dynamic web +applications using CherryPy. A couple of notes: + + +- Each of these tutorials builds on the ones before it. If you're + new to CherryPy, we recommend you start with 01_helloworld.py and + work your way upwards. :) + +- In most of these tutorials, you will notice that all output is done + by returning normal Python strings, often using simple Python + variable substitution. In most real-world applications, you will + probably want to use a separate template package (like Cheetah, + CherryTemplate or XML/XSL). diff --git a/libraries/cherrypy/tutorial/__init__.py b/libraries/cherrypy/tutorial/__init__.py new file mode 100644 index 00000000..08c142c5 --- /dev/null +++ b/libraries/cherrypy/tutorial/__init__.py @@ -0,0 +1,3 @@ + +# This is used in test_config to test unrepr of "from A import B" +thing2 = object() diff --git a/libraries/cherrypy/tutorial/custom_error.html b/libraries/cherrypy/tutorial/custom_error.html new file mode 100644 index 00000000..d0f30c8a --- /dev/null +++ b/libraries/cherrypy/tutorial/custom_error.html @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> +<head> + <title>403 Unauthorized</title> +</head> + <body> + <h2>You can't do that!</h2> + <p>%(message)s</p> + <p>This is a custom error page that is read from a file.<p> + <pre>%(traceback)s</pre> + </body> +</html> diff --git a/libraries/cherrypy/tutorial/pdf_file.pdf b/libraries/cherrypy/tutorial/pdf_file.pdf new file mode 100644 index 0000000000000000000000000000000000000000..38b4f15eabdd65d4a674cb32034361245aa7b97e GIT binary patch literal 85698 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 literal 0 HcmV?d00001 diff --git a/libraries/cherrypy/tutorial/tut01_helloworld.py b/libraries/cherrypy/tutorial/tut01_helloworld.py new file mode 100644 index 00000000..e86793c8 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut01_helloworld.py @@ -0,0 +1,34 @@ +""" +Tutorial - Hello World + +The most basic (working) CherryPy application possible. +""" + +import os.path + +# Import CherryPy global namespace +import cherrypy + + +class HelloWorld: + + """ Sample request handler class. """ + + # Expose the index method through the web. CherryPy will never + # publish methods that don't have the exposed attribute set to True. + @cherrypy.expose + def index(self): + # CherryPy will call this method for the root URI ("/") and send + # its return value to the client. Because this is tutorial + # lesson number 01, we'll just send something really simple. + # How about... + return 'Hello world!' + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HelloWorld(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut02_expose_methods.py b/libraries/cherrypy/tutorial/tut02_expose_methods.py new file mode 100644 index 00000000..8afbf7d8 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut02_expose_methods.py @@ -0,0 +1,32 @@ +""" +Tutorial - Multiple methods + +This tutorial shows you how to link to other methods of your request +handler. +""" + +import os.path + +import cherrypy + + +class HelloWorld: + + @cherrypy.expose + def index(self): + # Let's link to another method here. + return 'We have an <a href="show_msg">important message</a> for you!' + + @cherrypy.expose + def show_msg(self): + # Here's the important message! + return 'Hello world!' + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HelloWorld(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut03_get_and_post.py b/libraries/cherrypy/tutorial/tut03_get_and_post.py new file mode 100644 index 00000000..0b3d4613 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut03_get_and_post.py @@ -0,0 +1,51 @@ +""" +Tutorial - Passing variables + +This tutorial shows you how to pass GET/POST variables to methods. +""" + +import os.path + +import cherrypy + + +class WelcomePage: + + @cherrypy.expose + def index(self): + # Ask for the user's name. + return ''' + <form action="greetUser" method="GET"> + What is your name? + <input type="text" name="name" /> + <input type="submit" /> + </form>''' + + @cherrypy.expose + def greetUser(self, name=None): + # CherryPy passes all GET and POST variables as method parameters. + # It doesn't make a difference where the variables come from, how + # large their contents are, and so on. + # + # You can define default parameter values as usual. In this + # example, the "name" parameter defaults to None so we can check + # if a name was actually specified. + + if name: + # Greet the user! + return "Hey %s, what's up?" % name + else: + if name is None: + # No name was specified + return 'Please enter your name <a href="./">here</a>.' + else: + return 'No, really, enter your name <a href="./">here</a>.' + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(WelcomePage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut04_complex_site.py b/libraries/cherrypy/tutorial/tut04_complex_site.py new file mode 100644 index 00000000..3caa1775 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut04_complex_site.py @@ -0,0 +1,103 @@ +""" +Tutorial - Multiple objects + +This tutorial shows you how to create a site structure through multiple +possibly nested request handler objects. +""" + +import os.path + +import cherrypy + + +class HomePage: + + @cherrypy.expose + def index(self): + return ''' + <p>Hi, this is the home page! Check out the other + fun stuff on this site:</p> + + <ul> + <li><a href="/joke/">A silly joke</a></li> + <li><a href="/links/">Useful links</a></li> + </ul>''' + + +class JokePage: + + @cherrypy.expose + def index(self): + return ''' + <p>"In Python, how do you create a string of random + characters?" -- "Read a Perl file!"</p> + <p>[<a href="../">Return</a>]</p>''' + + +class LinksPage: + + def __init__(self): + # Request handler objects can create their own nested request + # handler objects. Simply create them inside their __init__ + # methods! + self.extra = ExtraLinksPage() + + @cherrypy.expose + def index(self): + # Note the way we link to the extra links page (and back). + # 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 ''' + <p>Here are some useful links:</p> + + <ul> + <li> + <a href="http://www.cherrypy.org">The CherryPy Homepage</a> + </li> + <li> + <a href="http://www.python.org">The Python Homepage</a> + </li> + </ul> + + <p>You can check out some extra useful + links <a href="./extra/">here</a>.</p> + + <p>[<a href="../">Return</a>]</p> + ''' + + +class ExtraLinksPage: + + @cherrypy.expose + def index(self): + # Note the relative link back to the Links page! + return ''' + <p>Here are some extra useful links:</p> + + <ul> + <li><a href="http://del.icio.us">del.icio.us</a></li> + <li><a href="http://www.cherrypy.org">CherryPy</a></li> + </ul> + + <p>[<a href="../">Return to links page</a>]</p>''' + + +# Of course we can also mount request handler objects right here! +root = HomePage() +root.joke = JokePage() +root.links = LinksPage() + +# Remember, we don't need to mount ExtraLinksPage here, because +# LinksPage does that itself on initialization. In fact, there is +# no reason why you shouldn't let your root object take care of +# creating all contained request handler objects. + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(root, config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut05_derived_objects.py b/libraries/cherrypy/tutorial/tut05_derived_objects.py new file mode 100644 index 00000000..f626e03f --- /dev/null +++ b/libraries/cherrypy/tutorial/tut05_derived_objects.py @@ -0,0 +1,80 @@ +""" +Tutorial - Object inheritance + +You are free to derive your request handler classes from any base +class you wish. In most real-world applications, you will probably +want to create a central base class used for all your pages, which takes +care of things like printing a common page header and footer. +""" + +import os.path + +import cherrypy + + +class Page: + # Store the page title in a class attribute + title = 'Untitled Page' + + def header(self): + return ''' + <html> + <head> + <title>%s</title> + <head> + <body> + <h2>%s</h2> + ''' % (self.title, self.title) + + def footer(self): + return ''' + </body> + </html> + ''' + + # Note that header and footer don't get their exposed attributes + # set to True. This isn't necessary since the user isn't supposed + # to call header or footer directly; instead, we'll call them from + # within the actually exposed handler methods defined in this + # class' subclasses. + + +class HomePage(Page): + # Different title for this page + title = 'Tutorial 5' + + def __init__(self): + # create a subpage + self.another = AnotherPage() + + @cherrypy.expose + def index(self): + # Note that we call the header and footer methods inherited + # from the Page class! + return self.header() + ''' + <p> + Isn't this exciting? There's + <a href="./another/">another page</a>, too! + </p> + ''' + self.footer() + + +class AnotherPage(Page): + title = 'Another Page' + + @cherrypy.expose + def index(self): + return self.header() + ''' + <p> + And this is the amazing second page! + </p> + ''' + self.footer() + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HomePage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut06_default_method.py b/libraries/cherrypy/tutorial/tut06_default_method.py new file mode 100644 index 00000000..0ce4cabe --- /dev/null +++ b/libraries/cherrypy/tutorial/tut06_default_method.py @@ -0,0 +1,61 @@ +""" +Tutorial - The default method + +Request handler objects can implement a method called "default" that +is called when no other suitable method/object could be found. +Essentially, if CherryPy2 can't find a matching request handler object +for the given request URI, it will use the default method of the object +located deepest on the URI path. + +Using this mechanism you can easily simulate virtual URI structures +by parsing the extra URI string, which you can access through +cherrypy.request.virtualPath. + +The application in this tutorial simulates an URI structure looking +like /users/<username>. Since the <username> bit will not be found (as +there are no matching methods), it is handled by the default method. +""" + +import os.path + +import cherrypy + + +class UsersPage: + + @cherrypy.expose + 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 ''' + <a href="./remi">Remi Delon</a><br/> + <a href="./hendrik">Hendrik Mans</a><br/> + <a href="./lorenzo">Lorenzo Lamas</a><br/> + ''' + + @cherrypy.expose + def default(self, user): + # Here we react depending on the virtualPath -- the part of the + # path that could not be mapped to an object method. In a real + # application, we would probably do some database lookups here + # instead of the silly if/elif/else construct. + if user == 'remi': + out = 'Remi Delon, CherryPy lead developer' + elif user == 'hendrik': + out = 'Hendrik Mans, CherryPy co-developer & crazy German' + elif user == 'lorenzo': + out = 'Lorenzo Lamas, famous actor and singer!' + else: + out = 'Unknown user. :-(' + + return '%s (<a href="./">back</a>)' % out + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(UsersPage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut07_sessions.py b/libraries/cherrypy/tutorial/tut07_sessions.py new file mode 100644 index 00000000..204322b5 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut07_sessions.py @@ -0,0 +1,41 @@ +""" +Tutorial - Sessions + +Storing session data in CherryPy applications is very easy: cherrypy +provides a dictionary called "session" that represents the session +data for the current user. If you use RAM based sessions, you can store +any kind of object into that dictionary; otherwise, you are limited to +objects that can be pickled. +""" + +import os.path + +import cherrypy + + +class HitCounter: + + _cp_config = {'tools.sessions.on': True} + + @cherrypy.expose + def index(self): + # Increase the silly hit counter + count = cherrypy.session.get('count', 0) + 1 + + # Store the new value in the session dictionary + cherrypy.session['count'] = count + + # And display a silly hit count message! + return ''' + During your current session, you've viewed this + page %s times! Your life is a patio of fun! + ''' % count + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HitCounter(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py new file mode 100644 index 00000000..18f42f93 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py @@ -0,0 +1,44 @@ +""" +Bonus Tutorial: Using generators to return result bodies + +Instead of returning a complete result string, you can use the yield +statement to return one result part after another. This may be convenient +in situations where using a template package like CherryPy or Cheetah +would be overkill, and messy string concatenation too uncool. ;-) +""" + +import os.path + +import cherrypy + + +class GeneratorDemo: + + def header(self): + return '<html><body><h2>Generators rule!</h2>' + + def footer(self): + return '</body></html>' + + @cherrypy.expose + def index(self): + # Let's make up a list of users for presentation purposes + users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas'] + + # Every yield line adds one part to the total result body. + yield self.header() + yield '<h3>List of users:</h3>' + + for user in users: + yield '%s<br/>' % user + + yield self.footer() + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(GeneratorDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut09_files.py b/libraries/cherrypy/tutorial/tut09_files.py new file mode 100644 index 00000000..48585cbe --- /dev/null +++ b/libraries/cherrypy/tutorial/tut09_files.py @@ -0,0 +1,105 @@ +""" + +Tutorial: File upload and download + +Uploads +------- + +When a client uploads a file to a CherryPy application, it's placed +on disk immediately. CherryPy will pass it to your exposed method +as an argument (see "myFile" below); that arg will have a "file" +attribute, which is a handle to the temporary uploaded file. +If you wish to permanently save the file, you need to read() +from myFile.file and write() somewhere else. + +Note the use of 'enctype="multipart/form-data"' and 'input type="file"' +in the HTML which the client uses to upload the file. + + +Downloads +--------- + +If you wish to send a file to the client, you have two options: +First, you can simply return a file-like object from your page handler. +CherryPy will read the file and serve it as the content (HTTP body) +of the response. However, that doesn't tell the client that +the response is a file to be saved, rather than displayed. +Use cherrypy.lib.static.serve_file for that; it takes four +arguments: + +serve_file(path, content_type=None, disposition=None, name=None) + +Set "name" to the filename that you expect clients to use when they save +your file. Note that the "name" argument is ignored if you don't also +provide a "disposition" (usually "attachement"). You can manually set +"content_type", but be aware that if you also use the encoding tool, it +may choke if the file extension is not recognized as belonging to a known +Content-Type. Setting the content_type to "application/x-download" works +in most cases, and should prompt the user with an Open/Save dialog in +popular browsers. + +""" + +import os +import os.path + +import cherrypy +from cherrypy.lib import static + +localDir = os.path.dirname(__file__) +absDir = os.path.join(os.getcwd(), localDir) + + +class FileDemo(object): + + @cherrypy.expose + def index(self): + return """ + <html><body> + <h2>Upload a file</h2> + <form action="upload" method="post" enctype="multipart/form-data"> + filename: <input type="file" name="myFile" /><br /> + <input type="submit" /> + </form> + <h2>Download a file</h2> + <a href='download'>This one</a> + </body></html> + """ + + @cherrypy.expose + def upload(self, myFile): + out = """<html> + <body> + myFile length: %s<br /> + myFile filename: %s<br /> + myFile mime-type: %s + </body> + </html>""" + + # Although this just counts the file length, it demonstrates + # how to read large files in chunks instead of all at once. + # CherryPy reads the uploaded file into a temporary file; + # myFile.file.read reads from that. + size = 0 + while True: + data = myFile.file.read(8192) + if not data: + break + size += len(data) + + return out % (size, myFile.filename, myFile.content_type) + + @cherrypy.expose + def download(self): + path = os.path.join(absDir, 'pdf_file.pdf') + return static.serve_file(path, 'application/x-download', + 'attachment', os.path.basename(path)) + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(FileDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut10_http_errors.py b/libraries/cherrypy/tutorial/tut10_http_errors.py new file mode 100644 index 00000000..18f02fd0 --- /dev/null +++ b/libraries/cherrypy/tutorial/tut10_http_errors.py @@ -0,0 +1,84 @@ +""" + +Tutorial: HTTP errors + +HTTPError is used to return an error response to the client. +CherryPy has lots of options regarding how such errors are +logged, displayed, and formatted. + +""" + +import os +import os.path + +import cherrypy + +localDir = os.path.dirname(__file__) +curpath = os.path.normpath(os.path.join(os.getcwd(), localDir)) + + +class HTTPErrorDemo(object): + + # Set a custom response for 403 errors. + _cp_config = {'error_page.403': + os.path.join(curpath, 'custom_error.html')} + + @cherrypy.expose + def index(self): + # display some links that will result in errors + tracebacks = cherrypy.request.show_tracebacks + if tracebacks: + trace = 'off' + else: + trace = 'on' + + return """ + <html><body> + <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p> + <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p> + <p> + <a href="/error?code=403"> + Use a custom error page from a file. + </a> + </p> + <p>These errors are explicitly raised by the application:</p> + <ul> + <li><a href="/error?code=400">400</a></li> + <li><a href="/error?code=401">401</a></li> + <li><a href="/error?code=402">402</a></li> + <li><a href="/error?code=500">500</a></li> + </ul> + <p><a href="/messageArg">You can also set the response body + when you raise an error.</a></p> + </body></html> + """ % trace + + @cherrypy.expose + def toggleTracebacks(self): + # simple function to toggle tracebacks on and off + tracebacks = cherrypy.request.show_tracebacks + cherrypy.config.update({'request.show_tracebacks': not tracebacks}) + + # redirect back to the index + raise cherrypy.HTTPRedirect('/') + + @cherrypy.expose + def error(self, code): + # raise an error based on the get query + raise cherrypy.HTTPError(status=code) + + @cherrypy.expose + def messageArg(self): + 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) + + +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HTTPErrorDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tutorial.conf b/libraries/cherrypy/tutorial/tutorial.conf new file mode 100644 index 00000000..43dfa60f --- /dev/null +++ b/libraries/cherrypy/tutorial/tutorial.conf @@ -0,0 +1,4 @@ +[global] +server.socket_host = "127.0.0.1" +server.socket_port = 8080 +server.thread_pool = 10 diff --git a/libraries/contextlib2.py b/libraries/contextlib2.py new file mode 100644 index 00000000..f08df14c --- /dev/null +++ b/libraries/contextlib2.py @@ -0,0 +1,436 @@ +"""contextlib2 - backports and enhancements to the contextlib module""" + +import sys +import warnings +from collections import deque +from functools import wraps + +__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", + "redirect_stdout", "redirect_stderr", "suppress"] + +# Backwards compatibility +__all__ += ["ContextStack"] + +class ContextDecorator(object): + "A base class or mixin that enables context managers to work as decorators." + + def refresh_cm(self): + """Returns the context manager used to actually wrap the call to the + decorated function. + + The default implementation just returns *self*. + + Overriding this method allows otherwise one-shot context managers + like _GeneratorContextManager to support use as decorators via + implicit recreation. + + DEPRECATED: refresh_cm was never added to the standard library's + ContextDecorator API + """ + warnings.warn("refresh_cm was never added to the standard library", + DeprecationWarning) + return self._recreate_cm() + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. + """ + return self + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + return inner + + +class _GeneratorContextManager(ContextDecorator): + """Helper for @contextmanager decorator.""" + + def __init__(self, func, args, kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + # Issue 19330: ensure context manager instances have good docstrings + doc = getattr(func, "__doc__", None) + if doc is None: + doc = type(self).__doc__ + self.__doc__ = doc + # Unfortunately, this still doesn't provide good help output when + # inspecting the created context manager instances, since pydoc + # currently bypasses the instance docstring and shows the docstring + # for the class instead. + # See http://bugs.python.org/issue19404 for more details. + + def _recreate_cm(self): + # _GCM instances are one-shot context managers, so the + # CM must be recreated each time a decorated function is + # called + return self.__class__(self.func, self.args, self.kwds) + + def __enter__(self): + try: + return next(self.gen) + except StopIteration: + raise RuntimeError("generator didn't yield") + + def __exit__(self, type, value, traceback): + if type is None: + try: + next(self.gen) + except StopIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = type() + try: + self.gen.throw(type, value, traceback) + raise RuntimeError("generator didn't stop after throw()") + except StopIteration as exc: + # Suppress StopIteration *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed. + return exc is not value + except RuntimeError as exc: + # Don't re-raise the passed in exception + if exc is value: + return False + # Likewise, avoid suppressing if a StopIteration exception + # was passed to throw() and later wrapped into a RuntimeError + # (see PEP 479). + if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: + return False + raise + except: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. + # + if sys.exc_info()[1] is not value: + raise + + +def contextmanager(func): + """@contextmanager decorator. + + Typical usage: + + @contextmanager + def some_generator(<arguments>): + <setup> + try: + yield <value> + finally: + <cleanup> + + This makes this: + + with some_generator(<arguments>) as <variable>: + <body> + + equivalent to this: + + <setup> + try: + <variable> = <value> + <body> + finally: + <cleanup> + + """ + @wraps(func) + def helper(*args, **kwds): + return _GeneratorContextManager(func, args, kwds) + return helper + + +class closing(object): + """Context to automatically close something at the end of a block. + + Code like this: + + with closing(<module>.open(<arguments>)) as f: + <block> + + is equivalent to this: + + f = <module>.open(<arguments>) + try: + <block> + finally: + f.close() + + """ + def __init__(self, thing): + self.thing = thing + def __enter__(self): + return self.thing + def __exit__(self, *exc_info): + self.thing.close() + + +class _RedirectStream(object): + + _stream = None + + def __init__(self, new_target): + self._new_target = new_target + # We use a list of old targets to make this CM re-entrant + self._old_targets = [] + + def __enter__(self): + self._old_targets.append(getattr(sys, self._stream)) + setattr(sys, self._stream, self._new_target) + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + setattr(sys, self._stream, self._old_targets.pop()) + + +class redirect_stdout(_RedirectStream): + """Context manager for temporarily redirecting stdout to another file. + + # How to send help() to stderr + with redirect_stdout(sys.stderr): + help(dir) + + # How to write help() to a file + with open('help.txt', 'w') as f: + with redirect_stdout(f): + help(pow) + """ + + _stream = "stdout" + + +class redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + + _stream = "stderr" + + +class suppress(object): + """Context manager to suppress specified exceptions + + After the exception is suppressed, execution proceeds with the next + statement following the with statement. + + with suppress(FileNotFoundError): + os.remove(somefile) + # Execution still resumes here if the file was already removed + """ + + def __init__(self, *exceptions): + self._exceptions = exceptions + + def __enter__(self): + pass + + def __exit__(self, exctype, excinst, exctb): + # Unlike isinstance and issubclass, CPython exception handling + # currently only looks at the concrete type hierarchy (ignoring + # the instance and subclass checking hooks). While Guido considers + # that a bug rather than a feature, it's a fairly hard one to fix + # due to various internal implementation details. suppress provides + # the simpler issubclass based semantics, rather than trying to + # exactly reproduce the limitations of the CPython interpreter. + # + # See http://bugs.python.org/issue12029 for more details + return exctype is not None and issubclass(exctype, self._exceptions) + + +# Context manipulation is Python 3 only +_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 +if _HAVE_EXCEPTION_CHAINING: + def _make_context_fixer(frame_exc): + def _fix_exception_context(new_exc, old_exc): + # Context may not be correct, so find the end of the chain + while 1: + exc_context = new_exc.__context__ + if exc_context is old_exc: + # Context is already set correctly (see issue 20317) + return + if exc_context is None or exc_context is frame_exc: + break + new_exc = exc_context + # Change the end of the chain to point to the exception + # we expect it to reference + new_exc.__context__ = old_exc + return _fix_exception_context + + def _reraise_with_existing_context(exc_details): + try: + # bare "raise exc_details[1]" replaces our carefully + # set-up context + fixed_ctx = exc_details[1].__context__ + raise exc_details[1] + except BaseException: + exc_details[1].__context__ = fixed_ctx + raise +else: + # No exception context in Python 2 + def _make_context_fixer(frame_exc): + return lambda new_exc, old_exc: None + + # Use 3 argument raise in Python 2, + # but use exec to avoid SyntaxError in Python 3 + def _reraise_with_existing_context(exc_details): + exc_type, exc_value, exc_tb = exc_details + exec ("raise exc_type, exc_value, exc_tb") + +# Handle old-style classes if they exist +try: + from types import InstanceType +except ImportError: + # Python 3 doesn't have old-style classes + _get_type = type +else: + # Need to handle old-style context managers on Python 2 + def _get_type(obj): + obj_type = type(obj) + if obj_type is InstanceType: + return obj.__class__ # Old-style class + return obj_type # New-style class + +# Inspired by discussions on http://bugs.python.org/issue13585 +class ExitStack(object): + """Context manager for dynamic management of a stack of exit callbacks + + For example: + + with ExitStack() as stack: + files = [stack.enter_context(open(fname)) for fname in filenames] + # All opened files will automatically be closed at the end of + # the with statement, even if attempts to open files later + # in the list raise an exception + + """ + def __init__(self): + self._exit_callbacks = deque() + + def pop_all(self): + """Preserve the context stack by transferring it to a new instance""" + new_stack = type(self)() + new_stack._exit_callbacks = self._exit_callbacks + self._exit_callbacks = deque() + return new_stack + + def _push_cm_exit(self, cm, cm_exit): + """Helper to correctly register callbacks to __exit__ methods""" + def _exit_wrapper(*exc_details): + return cm_exit(cm, *exc_details) + _exit_wrapper.__self__ = cm + self.push(_exit_wrapper) + + def push(self, exit): + """Registers a callback with the standard __exit__ method signature + + Can suppress exceptions the same way __exit__ methods can. + + Also accepts any object with an __exit__ method (registering a call + to the method instead of the object itself) + """ + # We use an unbound method rather than a bound method to follow + # the standard lookup behaviour for special methods + _cb_type = _get_type(exit) + try: + exit_method = _cb_type.__exit__ + except AttributeError: + # Not a context manager, so assume its a callable + self._exit_callbacks.append(exit) + else: + self._push_cm_exit(exit, exit_method) + return exit # Allow use as a decorator + + def callback(self, callback, *args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ + def _exit_wrapper(exc_type, exc, tb): + callback(*args, **kwds) + # We changed the signature, so using @wraps is not appropriate, but + # setting __wrapped__ may still help with introspection + _exit_wrapper.__wrapped__ = callback + self.push(_exit_wrapper) + return callback # Allow use as a decorator + + def enter_context(self, cm): + """Enters the supplied context manager + + If successful, also pushes its __exit__ method as a callback and + returns the result of the __enter__ method. + """ + # We look up the special methods on the type to match the with statement + _cm_type = _get_type(cm) + _exit = _cm_type.__exit__ + result = _cm_type.__enter__(cm) + self._push_cm_exit(cm, _exit) + return result + + def close(self): + """Immediately unwind the context stack""" + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, *exc_details): + received_exc = exc_details[0] is not None + + # We manipulate the exception state so it behaves as though + # we were actually nesting multiple with statements + frame_exc = sys.exc_info()[1] + _fix_exception_context = _make_context_fixer(frame_exc) + + # Callbacks are invoked in LIFO order to match the behaviour of + # nested context managers + suppressed_exc = False + pending_raise = False + while self._exit_callbacks: + cb = self._exit_callbacks.pop() + try: + if cb(*exc_details): + suppressed_exc = True + pending_raise = False + exc_details = (None, None, None) + except: + new_exc_details = sys.exc_info() + # simulate the stack of exceptions by setting the context + _fix_exception_context(new_exc_details[1], exc_details[1]) + pending_raise = True + exc_details = new_exc_details + if pending_raise: + _reraise_with_existing_context(exc_details) + return received_exc and suppressed_exc + +# Preserve backwards compatibility +class ContextStack(ExitStack): + """Backwards compatibility alias for ExitStack""" + + def __init__(self): + warnings.warn("ContextStack has been renamed to ExitStack", + DeprecationWarning) + super(ContextStack, self).__init__() + + def register_exit(self, callback): + return self.push(callback) + + def register(self, callback, *args, **kwds): + return self.callback(callback, *args, **kwds) + + def preserve(self): + return self.pop_all() diff --git a/libraries/more_itertools/__init__.py b/libraries/more_itertools/__init__.py new file mode 100644 index 00000000..bba462c3 --- /dev/null +++ b/libraries/more_itertools/__init__.py @@ -0,0 +1,2 @@ +from more_itertools.more import * # noqa +from more_itertools.recipes import * # noqa diff --git a/libraries/more_itertools/more.py b/libraries/more_itertools/more.py new file mode 100644 index 00000000..05e851ee --- /dev/null +++ b/libraries/more_itertools/more.py @@ -0,0 +1,2211 @@ +from __future__ import print_function + +from collections import Counter, defaultdict, deque +from functools import partial, wraps +from heapq import merge +from itertools import ( + chain, + compress, + count, + cycle, + dropwhile, + groupby, + islice, + repeat, + starmap, + takewhile, + tee +) +from operator import itemgetter, lt, gt, sub +from sys import maxsize, version_info +try: + from collections.abc import Sequence +except ImportError: + from collections import Sequence + +from six import binary_type, string_types, text_type +from six.moves import filter, map, range, zip, zip_longest + +from .recipes import consume, flatten, take + +__all__ = [ + 'adjacent', + 'always_iterable', + 'always_reversible', + 'bucket', + 'chunked', + 'circular_shifts', + 'collapse', + 'collate', + 'consecutive_groups', + 'consumer', + 'count_cycle', + 'difference', + 'distinct_permutations', + 'distribute', + 'divide', + 'exactly_n', + 'first', + 'groupby_transform', + 'ilen', + 'interleave_longest', + 'interleave', + 'intersperse', + 'islice_extended', + 'iterate', + 'last', + 'locate', + 'lstrip', + 'make_decorator', + 'map_reduce', + 'numeric_range', + 'one', + 'padded', + 'peekable', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'seekable', + 'SequenceView', + 'side_effect', + 'sliced', + 'sort_together', + 'split_at', + 'split_after', + 'split_before', + 'spy', + 'stagger', + 'strip', + 'unique_to_each', + 'windowed', + 'with_iter', + 'zip_offset', +] + +_marker = object() + + +def chunked(iterable, n): + """Break *iterable* into lists of length *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) + [[1, 2, 3], [4, 5, 6]] + + If the length of *iterable* is not evenly divisible by *n*, the last + returned list will be shorter: + + >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) + [[1, 2, 3], [4, 5, 6], [7, 8]] + + To use a fill-in value instead, see the :func:`grouper` recipe. + + :func:`chunked` is useful for splitting up a computation on a large number + of keys into batches, to be pickled and sent off to worker processes. One + example is operations on rows in MySQL, which does not implement + server-side cursors properly and would otherwise load the entire dataset + into RAM on the client. + + """ + return iter(partial(take, n, iter(iterable)), []) + + +def first(iterable, default=_marker): + """Return the first item of *iterable*, or *default* if *iterable* is + empty. + + >>> first([0, 1, 2, 3]) + 0 + >>> first([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + + :func:`first` is useful when you have a generator of expensive-to-retrieve + values and want any arbitrary one. It is marginally shorter than + ``next(iter(iterable), default)``. + + """ + try: + return next(iter(iterable)) + except StopIteration: + # I'm on the edge about raising ValueError instead of StopIteration. At + # the moment, ValueError wins, because the caller could conceivably + # want to do something different with flow control when I raise the + # exception, and it's weird to explicitly catch StopIteration. + if default is _marker: + raise ValueError('first() was called on an empty iterable, and no ' + 'default value was provided.') + return default + + +def last(iterable, default=_marker): + """Return the last item of *iterable*, or *default* if *iterable* is + empty. + + >>> last([0, 1, 2, 3]) + 3 + >>> last([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + try: + try: + # Try to access the last item directly + return iterable[-1] + except (TypeError, AttributeError, KeyError): + # If not slice-able, iterate entirely using length-1 deque + return deque(iterable, maxlen=1)[0] + except IndexError: # If the iterable was empty + if default is _marker: + raise ValueError('last() was called on an empty iterable, and no ' + 'default value was provided.') + return default + + +class peekable(object): + """Wrap an iterator to allow lookahead and prepending elements. + + Call :meth:`peek` on the result to get the value that will be returned + by :func:`next`. This won't advance the iterator: + + >>> p = peekable(['a', 'b']) + >>> p.peek() + 'a' + >>> next(p) + 'a' + + Pass :meth:`peek` a default value to return that instead of raising + ``StopIteration`` when the iterator is exhausted. + + >>> p = peekable([]) + >>> p.peek('hi') + 'hi' + + peekables also offer a :meth:`prepend` method, which "inserts" items + at the head of the iterable: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> p.peek() + 11 + >>> list(p) + [11, 12, 1, 2, 3] + + peekables can be indexed. Index 0 is the item that will be returned by + :func:`next`, index 1 is the item after that, and so on: + The values up to the given index will be cached. + + >>> p = peekable(['a', 'b', 'c', 'd']) + >>> p[0] + 'a' + >>> p[1] + 'b' + >>> next(p) + 'a' + + Negative indexes are supported, but be aware that they will cache the + remaining items in the source iterator, which may require significant + storage. + + To check whether a peekable is exhausted, check its truth value: + + >>> p = peekable(['a', 'b']) + >>> if p: # peekable has items + ... list(p) + ['a', 'b'] + >>> if not p: # peekable is exhaused + ... list(p) + [] + + """ + def __init__(self, iterable): + self._it = iter(iterable) + self._cache = deque() + + def __iter__(self): + return self + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def __nonzero__(self): + # For Python 2 compatibility + return self.__bool__() + + def peek(self, default=_marker): + """Return the item that will be next returned from ``next()``. + + Return ``default`` if there are no items left. If ``default`` is not + provided, raise ``StopIteration``. + + """ + if not self._cache: + try: + self._cache.append(next(self._it)) + except StopIteration: + if default is _marker: + raise + return default + return self._cache[0] + + def prepend(self, *items): + """Stack up items to be the next ones returned from ``next()`` or + ``self.peek()``. The items will be returned in + first in, first out order:: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> list(p) + [11, 12, 1, 2, 3] + + It is possible, by prepending items, to "resurrect" a peekable that + previously raised ``StopIteration``. + + >>> p = peekable([]) + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + >>> p.prepend(1) + >>> next(p) + 1 + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + + """ + self._cache.extendleft(reversed(items)) + + def __next__(self): + if self._cache: + return self._cache.popleft() + + return next(self._it) + + next = __next__ # For Python 2 compatibility + + def _get_slice(self, index): + # Normalize the slice's arguments + step = 1 if (index.step is None) else index.step + if step > 0: + start = 0 if (index.start is None) else index.start + stop = maxsize if (index.stop is None) else index.stop + elif step < 0: + start = -1 if (index.start is None) else index.start + stop = (-maxsize - 1) if (index.stop is None) else index.stop + else: + raise ValueError('slice step cannot be zero') + + # If either the start or stop index is negative, we'll need to cache + # the rest of the iterable in order to slice from the right side. + if (start < 0) or (stop < 0): + self._cache.extend(self._it) + # Otherwise we'll need to find the rightmost index and cache to that + # point. + else: + n = min(max(start, stop) + 1, maxsize) + cache_len = len(self._cache) + if n >= cache_len: + self._cache.extend(islice(self._it, n - cache_len)) + + return list(self._cache)[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return self._get_slice(index) + + cache_len = len(self._cache) + if index < 0: + self._cache.extend(self._it) + elif index >= cache_len: + self._cache.extend(islice(self._it, index + 1 - cache_len)) + + return self._cache[index] + + +def _collate(*iterables, **kwargs): + """Helper for ``collate()``, called when the user is using the ``reverse`` + or ``key`` keyword arguments on Python versions below 3.5. + + """ + key = kwargs.pop('key', lambda a: a) + reverse = kwargs.pop('reverse', False) + + min_or_max = partial(max if reverse else min, key=itemgetter(0)) + peekables = [peekable(it) for it in iterables] + peekables = [p for p in peekables if p] # Kill empties. + while peekables: + _, p = min_or_max((key(p.peek()), p) for p in peekables) + yield next(p) + peekables = [x for x in peekables if x] + + +def collate(*iterables, **kwargs): + """Return a sorted merge of the items from each of several already-sorted + *iterables*. + + >>> list(collate('ACDZ', 'AZ', 'JKL')) + ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] + + Works lazily, keeping only the next value from each iterable in memory. Use + :func:`collate` to, for example, perform a n-way mergesort of items that + don't fit in memory. + + If a *key* function is specified, the iterables will be sorted according + to its result: + + >>> key = lambda s: int(s) # Sort by numeric value, not by string + >>> list(collate(['1', '10'], ['2', '11'], key=key)) + ['1', '2', '10', '11'] + + + If the *iterables* are sorted in descending order, set *reverse* to + ``True``: + + >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) + [5, 4, 3, 2, 1, 0] + + If the elements of the passed-in iterables are out of order, you might get + unexpected results. + + On Python 2.7, this function delegates to :func:`heapq.merge` if neither + of the keyword arguments are specified. On Python 3.5+, this function + is an alias for :func:`heapq.merge`. + + """ + if not kwargs: + return merge(*iterables) + + return _collate(*iterables, **kwargs) + + +# If using Python version 3.5 or greater, heapq.merge() will be faster than +# collate - use that instead. +if version_info >= (3, 5, 0): + _collate_docstring = collate.__doc__ + collate = partial(merge) + collate.__doc__ = _collate_docstring + + +def consumer(func): + """Decorator that automatically advances a PEP-342-style "reverse iterator" + to its first yield point so you don't have to call ``next()`` on it + manually. + + >>> @consumer + ... def tally(): + ... i = 0 + ... while True: + ... print('Thing number %s is %s.' % (i, (yield))) + ... i += 1 + ... + >>> t = tally() + >>> t.send('red') + Thing number 0 is red. + >>> t.send('fish') + Thing number 1 is fish. + + Without the decorator, you would have to call ``next(t)`` before + ``t.send()`` could be used. + + """ + @wraps(func) + def wrapper(*args, **kwargs): + gen = func(*args, **kwargs) + next(gen) + return gen + return wrapper + + +def ilen(iterable): + """Return the number of items in *iterable*. + + >>> ilen(x for x in range(1000000) if x % 3 == 0) + 333334 + + This consumes the iterable, so handle with care. + + """ + # maxlen=1 only stores the last item in the deque + d = deque(enumerate(iterable, 1), maxlen=1) + # since we started enumerate at 1, + # the first item of the last pair will be the length of the iterable + # (assuming there were items) + return d[0][0] if d else 0 + + +def iterate(func, start): + """Return ``start``, ``func(start)``, ``func(func(start))``, ... + + >>> from itertools import islice + >>> list(islice(iterate(lambda x: 2*x, 1), 10)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + """ + while True: + yield start + start = func(start) + + +def with_iter(context_manager): + """Wrap an iterable in a ``with`` statement, so it closes once exhausted. + + For example, this will close the file when the iterator is exhausted:: + + upper_lines = (line.upper() for line in with_iter(open('foo'))) + + Any context manager which returns an iterable is a candidate for + ``with_iter``. + + """ + with context_manager as iterable: + for item in iterable: + yield item + + +def one(iterable, too_short=None, too_long=None): + """Return the first item from *iterable*, which is expected to contain only + that item. Raise an exception if *iterable* is empty or has more than one + item. + + :func:`one` is useful for ensuring that an iterable contains only one item. + For example, it can be used to retrieve the result of a database query + that is expected to return a single row. + + If *iterable* is empty, ``ValueError`` will be raised. You may specify a + different exception with the *too_short* keyword: + + >>> it = [] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (expected 1)' + >>> too_short = IndexError('too few items') + >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + IndexError: too few items + + Similarly, if *iterable* contains more than one item, ``ValueError`` will + be raised. You may specify a different exception with the *too_long* + keyword: + + >>> it = ['too', 'many'] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (expected 1)' + >>> too_long = RuntimeError + >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + Note that :func:`one` attempts to advance *iterable* twice to ensure there + is only one item. If there is more than one, both items will be discarded. + See :func:`spy` or :func:`peekable` to check iterable contents less + destructively. + + """ + it = iter(iterable) + + try: + value = next(it) + except StopIteration: + raise too_short or ValueError('too few items in iterable (expected 1)') + + try: + next(it) + except StopIteration: + pass + else: + raise too_long or ValueError('too many items in iterable (expected 1)') + + return value + + +def distinct_permutations(iterable): + """Yield successive distinct permutations of the elements in *iterable*. + + >>> sorted(distinct_permutations([1, 0, 1])) + [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + + Equivalent to ``set(permutations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + Duplicate permutations arise when there are duplicated elements in the + input iterable. The number of items returned is + `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of + items input, and each `x_i` is the count of a distinct item in the input + sequence. + + """ + def perm_unique_helper(item_counts, perm, i): + """Internal helper function + + :arg item_counts: Stores the unique items in ``iterable`` and how many + times they are repeated + :arg perm: The permutation that is being built for output + :arg i: The index of the permutation being modified + + The output permutations are built up recursively; the distinct items + are placed until their repetitions are exhausted. + """ + if i < 0: + yield tuple(perm) + else: + for item in item_counts: + if item_counts[item] <= 0: + continue + perm[i] = item + item_counts[item] -= 1 + for x in perm_unique_helper(item_counts, perm, i - 1): + yield x + item_counts[item] += 1 + + item_counts = Counter(iterable) + length = sum(item_counts.values()) + + return perm_unique_helper(item_counts, [None] * length, length - 1) + + +def intersperse(e, iterable, n=1): + """Intersperse filler element *e* among the items in *iterable*, leaving + *n* items between each filler element. + + >>> list(intersperse('!', [1, 2, 3, 4, 5])) + [1, '!', 2, '!', 3, '!', 4, '!', 5] + + >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) + [1, 2, None, 3, 4, None, 5] + + """ + if n == 0: + raise ValueError('n must be > 0') + elif n == 1: + # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2... + # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2... + return islice(interleave(repeat(e), iterable), 1, None) + else: + # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... + # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... + # flatten(...) -> x_0, x_1, e, x_2, x_3... + filler = repeat([e]) + chunks = chunked(iterable, n) + return flatten(islice(interleave(filler, chunks), 1, None)) + + +def unique_to_each(*iterables): + """Return the elements from each of the input iterables that aren't in the + other input iterables. + + For example, suppose you have a set of packages, each with a set of + dependencies:: + + {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} + + If you remove one package, which dependencies can also be removed? + + If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not + associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for + ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: + + >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) + [['A'], ['C'], ['D']] + + If there are duplicates in one input iterable that aren't in the others + they will be duplicated in the output. Input order is preserved:: + + >>> unique_to_each("mississippi", "missouri") + [['p', 'p'], ['o', 'u', 'r']] + + It is assumed that the elements of each iterable are hashable. + + """ + pool = [list(it) for it in iterables] + counts = Counter(chain.from_iterable(map(set, pool))) + uniques = {element for element in counts if counts[element] == 1} + return [list(filter(uniques.__contains__, it)) for it in pool] + + +def windowed(seq, n, fillvalue=None, step=1): + """Return a sliding window of width *n* over the given iterable. + + >>> all_windows = windowed([1, 2, 3, 4, 5], 3) + >>> list(all_windows) + [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + + When the window is larger than the iterable, *fillvalue* is used in place + of missing values:: + + >>> list(windowed([1, 2, 3], 4)) + [(1, 2, 3, None)] + + Each window will advance in increments of *step*: + + >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) + [(1, 2, 3), (3, 4, 5), (5, 6, '!')] + + """ + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + yield tuple() + return + if step < 1: + raise ValueError('step must be >= 1') + + it = iter(seq) + window = deque([], n) + append = window.append + + # Initial deque fill + for _ in range(n): + append(next(it, fillvalue)) + yield tuple(window) + + # Appending new items to the right causes old items to fall off the left + i = 0 + for item in it: + append(item) + i = (i + 1) % step + if i % step == 0: + yield tuple(window) + + # If there are items from the iterable in the window, pad with the given + # value and emit them. + if (i % step) and (step - i < n): + for _ in range(step - i): + append(fillvalue) + yield tuple(window) + + +class bucket(object): + """Wrap *iterable* and return an object that buckets it iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) + + +def spy(iterable, n=1): + """Return a 2-tuple with a list containing the first *n* elements of + *iterable*, and an iterator with the same items as *iterable*. + This allows you to "look ahead" at the items in the iterable without + advancing it. + + There is one item in the list by default: + + >>> iterable = 'abcdefg' + >>> head, iterable = spy(iterable) + >>> head + ['a'] + >>> list(iterable) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + You may use unpacking to retrieve items instead of lists: + + >>> (head,), iterable = spy('abcdefg') + >>> head + 'a' + >>> (first, second), iterable = spy('abcdefg', 2) + >>> first + 'a' + >>> second + 'b' + + The number of items requested can be larger than the number of items in + the iterable: + + >>> iterable = [1, 2, 3, 4, 5] + >>> head, iterable = spy(iterable, 10) + >>> head + [1, 2, 3, 4, 5] + >>> list(iterable) + [1, 2, 3, 4, 5] + + """ + it = iter(iterable) + head = take(n, it) + + return head, chain(head, it) + + +def interleave(*iterables): + """Return a new iterable yielding from each iterable in turn, + until the shortest is exhausted. + + >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7] + + For a version that doesn't terminate after the shortest iterable is + exhausted, see :func:`interleave_longest`. + + """ + return chain.from_iterable(zip(*iterables)) + + +def interleave_longest(*iterables): + """Return a new iterable yielding from each iterable in turn, + skipping any that are exhausted. + + >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7, 3, 8] + + This function produces the same output as :func:`roundrobin`, but may + perform better for some inputs (in particular when the number of iterables + is large). + + """ + i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) + return (x for x in i if x is not _marker) + + +def collapse(iterable, base_type=None, levels=None): + """Flatten an iterable with multiple levels of nesting (e.g., a list of + lists of tuples) into non-iterable types. + + >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] + >>> list(collapse(iterable)) + [1, 2, 3, 4, 5, 6] + + String types are not considered iterable and will not be collapsed. + To avoid collapsing other types, specify *base_type*: + + >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] + >>> list(collapse(iterable, base_type=tuple)) + ['ab', ('cd', 'ef'), 'gh', 'ij'] + + Specify *levels* to stop flattening after a certain level: + + >>> iterable = [('a', ['b']), ('c', ['d'])] + >>> list(collapse(iterable)) # Fully flattened + ['a', 'b', 'c', 'd'] + >>> list(collapse(iterable, levels=1)) # Only one level flattened + ['a', ['b'], 'c', ['d']] + + """ + def walk(node, level): + if ( + ((levels is not None) and (level > levels)) or + isinstance(node, string_types) or + ((base_type is not None) and isinstance(node, base_type)) + ): + yield node + return + + try: + tree = iter(node) + except TypeError: + yield node + return + else: + for child in tree: + for x in walk(child, level + 1): + yield x + + for x in walk(iterable, 0): + yield x + + +def side_effect(func, iterable, chunk_size=None, before=None, after=None): + """Invoke *func* on each item in *iterable* (or on each *chunk_size* group + of items) before yielding the item. + + `func` must be a function that takes a single argument. Its return value + will be discarded. + + *before* and *after* are optional functions that take no arguments. They + will be executed before iteration starts and after it ends, respectively. + + `side_effect` can be used for logging, updating progress bars, or anything + that is not functionally "pure." + + Emitting a status message: + + >>> from more_itertools import consume + >>> func = lambda item: print('Received {}'.format(item)) + >>> consume(side_effect(func, range(2))) + Received 0 + Received 1 + + Operating on chunks of items: + + >>> pair_sums = [] + >>> func = lambda chunk: pair_sums.append(sum(chunk)) + >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) + [0, 1, 2, 3, 4, 5] + >>> list(pair_sums) + [1, 5, 9] + + Writing to a file-like object: + + >>> from io import StringIO + >>> from more_itertools import consume + >>> f = StringIO() + >>> func = lambda x: print(x, file=f) + >>> before = lambda: print(u'HEADER', file=f) + >>> after = f.close + >>> it = [u'a', u'b', u'c'] + >>> consume(side_effect(func, it, before=before, after=after)) + >>> f.closed + True + + """ + try: + if before is not None: + before() + + if chunk_size is None: + for item in iterable: + func(item) + yield item + else: + for chunk in chunked(iterable, chunk_size): + func(chunk) + for item in chunk: + yield item + finally: + if after is not None: + after() + + +def sliced(seq, n): + """Yield slices of length *n* from the sequence *seq*. + + >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) + [(1, 2, 3), (4, 5, 6)] + + If the length of the sequence is not divisible by the requested slice + length, the last slice will be shorter. + + >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) + [(1, 2, 3), (4, 5, 6), (7, 8)] + + This function will only work for iterables that support slicing. + For non-sliceable iterables, see :func:`chunked`. + + """ + return takewhile(bool, (seq[i: i + n] for i in count(0, n))) + + +def split_at(iterable, pred): + """Yield lists of items from *iterable*, where each list is delimited by + an item where callable *pred* returns ``True``. The lists do not include + the delimiting items. + + >>> list(split_at('abcdcba', lambda x: x == 'b')) + [['a'], ['c', 'd', 'c'], ['a']] + + >>> list(split_at(range(10), lambda n: n % 2 == 1)) + [[0], [2], [4], [6], [8], []] + """ + buf = [] + for item in iterable: + if pred(item): + yield buf + buf = [] + else: + buf.append(item) + yield buf + + +def split_before(iterable, pred): + """Yield lists of items from *iterable*, where each list starts with an + item where callable *pred* returns ``True``: + + >>> list(split_before('OneTwo', lambda s: s.isupper())) + [['O', 'n', 'e'], ['T', 'w', 'o']] + + >>> list(split_before(range(10), lambda n: n % 3 == 0)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + """ + buf = [] + for item in iterable: + if pred(item) and buf: + yield buf + buf = [] + buf.append(item) + yield buf + + +def split_after(iterable, pred): + """Yield lists of items from *iterable*, where each list ends with an + item where callable *pred* returns ``True``: + + >>> list(split_after('one1two2', lambda s: s.isdigit())) + [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] + + >>> list(split_after(range(10), lambda n: n % 3 == 0)) + [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] + + """ + buf = [] + for item in iterable: + buf.append(item) + if pred(item) and buf: + yield buf + buf = [] + if buf: + yield buf + + +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*:: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + """ + it = iter(iterable) + if n is None: + for item in chain(it, repeat(fillvalue)): + yield item + elif n < 1: + raise ValueError('n must be at least 1') + else: + item_count = 0 + for item in it: + yield item + item_count += 1 + + remaining = (n - item_count) % n if next_multiple else n - item_count + for _ in range(remaining): + yield fillvalue + + +def distribute(n, iterable): + """Distribute the items from *iterable* among *n* smaller iterables. + + >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 3, 5] + >>> list(group_2) + [2, 4, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 4, 7], [2, 5], [3, 6]] + + If the length of *iterable* is smaller than *n*, then the last returned + iterables will be empty: + + >>> children = distribute(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function uses :func:`itertools.tee` and may require significant + storage. If you need the order items in the smaller iterables to match the + original iterable, see :func:`divide`. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + children = tee(iterable, n) + return [islice(it, index, None, n) for index, it in enumerate(children)] + + +def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): + """Yield tuples whose elements are offset from *iterable*. + The amount by which the `i`-th item in each tuple is offset is given by + the `i`-th item in *offsets*. + + >>> list(stagger([0, 1, 2, 3])) + [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + >>> list(stagger(range(8), offsets=(0, 2, 4))) + [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] + + By default, the sequence will end when the final element of a tuple is the + last item in the iterable. To continue until the first element of a tuple + is the last item in the iterable, set *longest* to ``True``:: + + >>> list(stagger([0, 1, 2, 3], longest=True)) + [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + children = tee(iterable, len(offsets)) + + return zip_offset( + *children, offsets=offsets, longest=longest, fillvalue=fillvalue + ) + + +def zip_offset(*iterables, **kwargs): + """``zip`` the input *iterables* together, but offset the `i`-th iterable + by the `i`-th item in *offsets*. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] + + This can be used as a lightweight alternative to SciPy or pandas to analyze + data sets in which somes series have a lead or lag relationship. + + By default, the sequence will end when the shortest iterable is exhausted. + To continue until the longest iterable is exhausted, set *longest* to + ``True``. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + offsets = kwargs['offsets'] + longest = kwargs.get('longest', False) + fillvalue = kwargs.get('fillvalue', None) + + if len(iterables) != len(offsets): + raise ValueError("Number of iterables and offsets didn't match") + + staggered = [] + for it, n in zip(iterables, offsets): + if n < 0: + staggered.append(chain(repeat(fillvalue, -n), it)) + elif n > 0: + staggered.append(islice(it, n, None)) + else: + staggered.append(it) + + if longest: + return zip_longest(*staggered, fillvalue=fillvalue) + + return zip(*staggered) + + +def sort_together(iterables, key_list=(0,), reverse=False): + """Return the input iterables sorted together, with *key_list* as the + priority for sorting. All iterables are trimmed to the length of the + shortest one. + + This can be used like the sorting function in a spreadsheet. If each + iterable represents a column of data, the key list determines which + columns are used for sorting. + + By default, all iterables are sorted using the ``0``-th iterable:: + + >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] + >>> sort_together(iterables) + [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] + + Set a different key list to sort according to another iterable. + Specifying mutliple keys dictates how ties are broken:: + + >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] + >>> sort_together(iterables, key_list=(1, 2)) + [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + + Set *reverse* to ``True`` to sort in descending order. + + >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) + [(3, 2, 1), ('a', 'b', 'c')] + + """ + return list(zip(*sorted(zip(*iterables), + key=itemgetter(*key_list), + reverse=reverse))) + + +def divide(n, iterable): + """Divide the elements from *iterable* into *n* parts, maintaining + order. + + >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 2, 3] + >>> list(group_2) + [4, 5, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 2, 3], [4, 5], [6, 7]] + + If the length of the iterable is smaller than n, then the last returned + iterables will be empty: + + >>> children = divide(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function will exhaust the iterable before returning and may require + significant storage. If order is not important, see :func:`distribute`, + which does not first pull the iterable into memory. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + seq = tuple(iterable) + q, r = divmod(len(seq), n) + + ret = [] + for i in range(n): + start = (i * q) + (i if i < r else r) + stop = ((i + 1) * q) + (i + 1 if i + 1 < r else r) + ret.append(iter(seq[start:stop])) + + return ret + + +def always_iterable(obj, base_type=(text_type, binary_type)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) + + +def adjacent(predicate, iterable, distance=1): + """Return an iterable over `(bool, item)` tuples where the `item` is + drawn from *iterable* and the `bool` indicates whether + that item satisfies the *predicate* or is adjacent to an item that does. + + For example, to find whether items are adjacent to a ``3``:: + + >>> list(adjacent(lambda x: x == 3, range(6))) + [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] + + Set *distance* to change what counts as adjacent. For example, to find + whether items are two places away from a ``3``: + + >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) + [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] + + This is useful for contextualizing the results of a search function. + For example, a code comparison tool might want to identify lines that + have changed, but also surrounding lines to give the viewer of the diff + context. + + The predicate function will only be called once for each item in the + iterable. + + See also :func:`groupby_transform`, which can be used with this function + to group ranges of items with the same `bool` value. + + """ + # Allow distance=0 mainly for testing that it reproduces results with map() + if distance < 0: + raise ValueError('distance must be at least 0') + + i1, i2 = tee(iterable) + padding = [False] * distance + selected = chain(padding, map(predicate, i1), padding) + adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) + return zip(adjacent_to_selected, i2) + + +def groupby_transform(iterable, keyfunc=None, valuefunc=None): + """An extension of :func:`itertools.groupby` that transforms the values of + *iterable* after grouping them. + *keyfunc* is a function used to compute a grouping key for each item. + *valuefunc* is a function for transforming the items after grouping. + + >>> iterable = 'AaaABbBCcA' + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: x.lower() + >>> grouper = groupby_transform(iterable, keyfunc, valuefunc) + >>> [(k, ''.join(g)) for k, g in grouper] + [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')] + + *keyfunc* and *valuefunc* default to identity functions if they are not + specified. + + :func:`groupby_transform` is useful when grouping elements of an iterable + using a separate iterable as the key. To do this, :func:`zip` the iterables + and pass a *keyfunc* that extracts the first element and a *valuefunc* + that extracts the second element:: + + >>> from operator import itemgetter + >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] + >>> values = 'abcdefghi' + >>> iterable = zip(keys, values) + >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) + >>> [(k, ''.join(g)) for k, g in grouper] + [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] + + Note that the order of items in the iterable is significant. + Only adjacent items are grouped together, so if you don't want any + duplicate groups, you should sort the iterable by the key function. + + """ + valuefunc = (lambda x: x) if valuefunc is None else valuefunc + return ((k, map(valuefunc, g)) for k, g in groupby(iterable, keyfunc)) + + +def numeric_range(*args): + """An extension of the built-in ``range()`` function whose arguments can + be any orderable numeric type. + + With only *stop* specified, *start* defaults to ``0`` and *step* + defaults to ``1``. The output items will match the type of *stop*: + + >>> list(numeric_range(3.5)) + [0.0, 1.0, 2.0, 3.0] + + With only *start* and *stop* specified, *step* defaults to ``1``. The + output items will match the type of *start*: + + >>> from decimal import Decimal + >>> start = Decimal('2.1') + >>> stop = Decimal('5.1') + >>> list(numeric_range(start, stop)) + [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] + + With *start*, *stop*, and *step* specified the output items will match + the type of ``start + step``: + + >>> from fractions import Fraction + >>> start = Fraction(1, 2) # Start at 1/2 + >>> stop = Fraction(5, 2) # End at 5/2 + >>> step = Fraction(1, 2) # Count by 1/2 + >>> list(numeric_range(start, stop, step)) + [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] + + If *step* is zero, ``ValueError`` is raised. Negative steps are supported: + + >>> list(numeric_range(3, -1, -1.0)) + [3.0, 2.0, 1.0, 0.0] + + Be aware of the limitations of floating point numbers; the representation + of the yielded numbers may be surprising. + + """ + argc = len(args) + if argc == 1: + stop, = args + start = type(stop)(0) + step = 1 + elif argc == 2: + start, stop = args + step = 1 + elif argc == 3: + start, stop, step = args + else: + err_msg = 'numeric_range takes at most 3 arguments, got {}' + raise TypeError(err_msg.format(argc)) + + values = (start + (step * n) for n in count()) + if step > 0: + return takewhile(partial(gt, stop), values) + elif step < 0: + return takewhile(partial(lt, stop), values) + else: + raise ValueError('numeric_range arg 3 must not be zero') + + +def count_cycle(iterable, n=None): + """Cycle through the items from *iterable* up to *n* times, yielding + the number of completed cycles along with each item. If *n* is omitted the + process repeats indefinitely. + + >>> list(count_cycle('AB', 3)) + [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] + + """ + iterable = tuple(iterable) + if not iterable: + return iter(()) + counter = count() if n is None else range(n) + return ((i, item) for i in counter for item in iterable) + + +def locate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(locate([0, 1, 1, 0, 1, 0, 0])) + [1, 2, 4] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item. + + >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) + [1, 3] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(locate(iterable, pred=pred, window_size=3)) + [1, 5, 9] + + Use with :func:`seekable` to find indexes and then retrieve the associated + items: + + >>> from itertools import count + >>> from more_itertools import seekable + >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) + >>> it = seekable(source) + >>> pred = lambda x: x > 100 + >>> indexes = locate(it, pred=pred) + >>> i = next(indexes) + >>> it.seek(i) + >>> next(it) + 106 + + """ + if window_size is None: + return compress(count(), map(pred, iterable)) + + if window_size < 1: + raise ValueError('window size must be at least 1') + + it = windowed(iterable, window_size, fillvalue=_marker) + return compress(count(), starmap(pred, it)) + + +def lstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the beginning + for which *pred* returns ``True``. + + For example, to remove a set of items from the start of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(lstrip(iterable, pred)) + [1, 2, None, 3, False, None] + + This function is analogous to to :func:`str.lstrip`, and is essentially + an wrapper for :func:`itertools.dropwhile`. + + """ + return dropwhile(pred, iterable) + + +def rstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the end + for which *pred* returns ``True``. + + For example, to remove a set of items from the end of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(rstrip(iterable, pred)) + [None, False, None, 1, 2, None, 3] + + This function is analogous to :func:`str.rstrip`. + + """ + cache = [] + cache_append = cache.append + for x in iterable: + if pred(x): + cache_append(x) + else: + for y in cache: + yield y + del cache[:] + yield x + + +def strip(iterable, pred): + """Yield the items from *iterable*, but strip any from the + beginning and end for which *pred* returns ``True``. + + For example, to remove a set of items from both ends of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(strip(iterable, pred)) + [1, 2, None, 3] + + This function is analogous to :func:`str.strip`. + + """ + return rstrip(lstrip(iterable, pred), pred) + + +def islice_extended(iterable, *args): + """An extension of :func:`itertools.islice` that supports negative values + for *stop*, *start*, and *step*. + + >>> iterable = iter('abcdefgh') + >>> list(islice_extended(iterable, -4, -1)) + ['e', 'f', 'g'] + + Slices with negative values require some caching of *iterable*, but this + function takes care to minimize the amount of memory required. + + For example, you can use a negative step with an infinite iterator: + + >>> from itertools import count + >>> list(islice_extended(count(), 110, 99, -2)) + [110, 108, 106, 104, 102, 100] + + """ + s = slice(*args) + start = s.start + stop = s.stop + if s.step == 0: + raise ValueError('step argument must be a non-zero integer or None.') + step = s.step or 1 + + it = iter(iterable) + + if step > 0: + start = 0 if (start is None) else start + + if (start < 0): + # Consume all but the last -start items + cache = deque(enumerate(it, 1), maxlen=-start) + len_iter = cache[-1][0] if cache else 0 + + # Adjust start to be positive + i = max(len_iter + start, 0) + + # Adjust stop to be positive + if stop is None: + j = len_iter + elif stop >= 0: + j = min(stop, len_iter) + else: + j = max(len_iter + stop, 0) + + # Slice the cache + n = j - i + if n <= 0: + return + + for index, item in islice(cache, 0, n, step): + yield item + elif (stop is not None) and (stop < 0): + # Advance to the start position + next(islice(it, start, start), None) + + # When stop is negative, we have to carry -stop items while + # iterating + cache = deque(islice(it, -stop), maxlen=-stop) + + for index, item in enumerate(it): + cached_item = cache.popleft() + if index % step == 0: + yield cached_item + cache.append(item) + else: + # When both start and stop are positive we have the normal case + for item in islice(it, start, stop, step): + yield item + else: + start = -1 if (start is None) else start + + if (stop is not None) and (stop < 0): + # Consume all but the last items + n = -stop - 1 + cache = deque(enumerate(it, 1), maxlen=n) + len_iter = cache[-1][0] if cache else 0 + + # If start and stop are both negative they are comparable and + # we can just slice. Otherwise we can adjust start to be negative + # and then slice. + if start < 0: + i, j = start, stop + else: + i, j = min(start - len_iter, -1), None + + for index, item in list(cache)[i:j:step]: + yield item + else: + # Advance to the stop position + if stop is not None: + m = stop + 1 + next(islice(it, m, m), None) + + # stop is positive, so if start is negative they are not comparable + # and we need the rest of the items. + if start < 0: + i = start + n = None + # stop is None and start is positive, so we just need items up to + # the start index. + elif stop is None: + i = None + n = start + 1 + # Both stop and start are positive, so they are comparable. + else: + i = None + n = start - stop + if n <= 0: + return + + cache = list(islice(it, n)) + + for item in cache[i::step]: + yield item + + +def always_reversible(iterable): + """An extension of :func:`reversed` that supports all iterables, not + just those which implement the ``Reversible`` or ``Sequence`` protocols. + + >>> print(*always_reversible(x for x in range(3))) + 2 1 0 + + If the iterable is already reversible, this function returns the + result of :func:`reversed()`. If the iterable is not reversible, + this function will cache the remaining items in the iterable and + yield them in reverse order, which may require significant storage. + """ + try: + return reversed(iterable) + except TypeError: + return reversed(list(iterable)) + + +def consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + For finding runs of adjacent letters, try using the :meth:`index` method + of a string of letters: + + >>> from string import ascii_lowercase + >>> iterable = 'abcdfgilmnop' + >>> ordering = ascii_lowercase.index + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + """ + for k, g in groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(itemgetter(1), g) + + +def difference(iterable, func=sub): + """By default, compute the first difference of *iterable* using + :func:`operator.sub`. + + >>> iterable = [0, 1, 3, 6, 10] + >>> list(difference(iterable)) + [0, 1, 2, 3, 4] + + This is the opposite of :func:`accumulate`'s default behavior: + + >>> from more_itertools import accumulate + >>> iterable = [0, 1, 2, 3, 4] + >>> list(accumulate(iterable)) + [0, 1, 3, 6, 10] + >>> list(difference(accumulate(iterable))) + [0, 1, 2, 3, 4] + + By default *func* is :func:`operator.sub`, but other functions can be + specified. They will be applied as follows:: + + A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... + + For example, to do progressive division: + + >>> iterable = [1, 2, 6, 24, 120] # Factorial sequence + >>> func = lambda x, y: x // y + >>> list(difference(iterable, func)) + [1, 2, 3, 4, 5] + + """ + a, b = tee(iterable) + try: + item = next(b) + except StopIteration: + return iter([]) + return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b))) + + +class SequenceView(Sequence): + """Return a read-only view of the sequence object *target*. + + :class:`SequenceView` objects are analagous to Python's built-in + "dictionary view" types. They provide a dynamic view of a sequence's items, + meaning that when the sequence updates, so does the view. + + >>> seq = ['0', '1', '2'] + >>> view = SequenceView(seq) + >>> view + SequenceView(['0', '1', '2']) + >>> seq.append('3') + >>> view + SequenceView(['0', '1', '2', '3']) + + Sequence views support indexing, slicing, and length queries. They act + like the underlying sequence, except they don't allow assignment: + + >>> view[1] + '1' + >>> view[1:-1] + ['1', '2'] + >>> len(view) + 4 + + Sequence views are useful as an alternative to copying, as they don't + require (much) extra storage. + + """ + def __init__(self, target): + if not isinstance(target, Sequence): + raise TypeError + self._target = target + + def __getitem__(self, index): + return self._target[index] + + def __len__(self): + return len(self._target) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._target)) + + +class seekable(object): + """Wrap an iterator to allow for seeking backward and forward. This + progressively caches the items in the source iterable so they can be + re-visited. + + Call :meth:`seek` with an index to seek to that position in the source + iterable. + + To "reset" an iterator, seek to ``0``: + + >>> from itertools import count + >>> it = seekable((str(n) for n in count())) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.seek(0) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> next(it) + '3' + + You can also seek forward: + + >>> it = seekable((str(n) for n in range(20))) + >>> it.seek(10) + >>> next(it) + '10' + >>> it.seek(20) # Seeking past the end of the source isn't a problem + >>> list(it) + [] + >>> it.seek(0) # Resetting works even after hitting the end + >>> next(it), next(it), next(it) + ('0', '1', '2') + + The cache grows as the source iterable progresses, so beware of wrapping + very large or infinite iterables. + + You may view the contents of the cache with the :meth:`elements` method. + That returns a :class:`SequenceView`, a view that updates automatically: + + >>> it = seekable((str(n) for n in range(10))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> elements = it.elements() + >>> elements + SequenceView(['0', '1', '2']) + >>> next(it) + '3' + >>> elements + SequenceView(['0', '1', '2', '3']) + + """ + + def __init__(self, iterable): + self._source = iter(iterable) + self._cache = [] + self._index = None + + def __iter__(self): + return self + + def __next__(self): + if self._index is not None: + try: + item = self._cache[self._index] + except IndexError: + self._index = None + else: + self._index += 1 + return item + + item = next(self._source) + self._cache.append(item) + return item + + next = __next__ + + def elements(self): + return SequenceView(self._cache) + + def seek(self, index): + self._index = index + remainder = index - len(self._cache) + if remainder > 0: + consume(self, remainder) + + +class run_length(object): + """ + :func:`run_length.encode` compresses an iterable with run-length encoding. + It yields groups of repeated items with the count of how many times they + were repeated: + + >>> uncompressed = 'abbcccdddd' + >>> list(run_length.encode(uncompressed)) + [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + + :func:`run_length.decode` decompresses an iterable that was previously + compressed with run-length encoding. It yields the items of the + decompressed iterable: + + >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> list(run_length.decode(compressed)) + ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] + + """ + + @staticmethod + def encode(iterable): + return ((k, ilen(g)) for k, g in groupby(iterable)) + + @staticmethod + def decode(iterable): + return chain.from_iterable(repeat(k, n) for k, n in iterable) + + +def exactly_n(iterable, n, predicate=bool): + """Return ``True`` if exactly ``n`` items in the iterable are ``True`` + according to the *predicate* function. + + >>> exactly_n([True, True, False], 2) + True + >>> exactly_n([True, True, False], 1) + False + >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) + True + + The iterable will be advanced until ``n + 1`` truthy items are encountered, + so avoid calling it on infinite iterables. + + """ + return len(take(n + 1, filter(predicate, iterable))) == n + + +def circular_shifts(iterable): + """Return a list of circular shifts of *iterable*. + + >>> circular_shifts(range(4)) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + """ + lst = list(iterable) + return take(len(lst), windowed(cycle(lst), len(lst))) + + +def make_decorator(wrapping_func, result_index=0): + """Return a decorator version of *wrapping_func*, which is a function that + modifies an iterable. *result_index* is the position in that function's + signature where the iterable goes. + + This lets you use itertools on the "production end," i.e. at function + definition. This can augment what the function returns without changing the + function's code. + + For example, to produce a decorator version of :func:`chunked`: + + >>> from more_itertools import chunked + >>> chunker = make_decorator(chunked, result_index=0) + >>> @chunker(3) + ... def iter_range(n): + ... return iter(range(n)) + ... + >>> list(iter_range(9)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + To only allow truthy items to be returned: + + >>> truth_serum = make_decorator(filter, result_index=1) + >>> @truth_serum(bool) + ... def boolean_test(): + ... return [0, 1, '', ' ', False, True] + ... + >>> list(boolean_test()) + [1, ' ', True] + + The :func:`peekable` and :func:`seekable` wrappers make for practical + decorators: + + >>> from more_itertools import peekable + >>> peekable_function = make_decorator(peekable) + >>> @peekable_function() + ... def str_range(*args): + ... return (str(x) for x in range(*args)) + ... + >>> it = str_range(1, 20, 2) + >>> next(it), next(it), next(it) + ('1', '3', '5') + >>> it.peek() + '7' + >>> next(it) + '7' + + """ + # See https://sites.google.com/site/bbayles/index/decorator_factory for + # notes on how this works. + def decorator(*wrapping_args, **wrapping_kwargs): + def outer_wrapper(f): + def inner_wrapper(*args, **kwargs): + result = f(*args, **kwargs) + wrapping_args_ = list(wrapping_args) + wrapping_args_.insert(result_index, result) + return wrapping_func(*wrapping_args_, **wrapping_kwargs) + + return inner_wrapper + + return outer_wrapper + + return decorator + + +def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): + """Return a dictionary that maps the items in *iterable* to categories + defined by *keyfunc*, transforms them with *valuefunc*, and + then summarizes them by category with *reducefunc*. + + *valuefunc* defaults to the identity function if it is unspecified. + If *reducefunc* is unspecified, no summarization takes place: + + >>> keyfunc = lambda x: x.upper() + >>> result = map_reduce('abbccc', keyfunc) + >>> sorted(result.items()) + [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] + + Specifying *valuefunc* transforms the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> result = map_reduce('abbccc', keyfunc, valuefunc) + >>> sorted(result.items()) + [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] + + Specifying *reducefunc* summarizes the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> reducefunc = sum + >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) + >>> sorted(result.items()) + [('A', 1), ('B', 2), ('C', 3)] + + You may want to filter the input iterable before applying the map/reduce + procedure: + + >>> all_items = range(30) + >>> items = [x for x in all_items if 10 <= x <= 20] # Filter + >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 + >>> categories = map_reduce(items, keyfunc=keyfunc) + >>> sorted(categories.items()) + [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] + >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) + >>> sorted(summaries.items()) + [(0, 90), (1, 75)] + + Note that all items in the iterable are gathered into a list before the + summarization step, which may require significant storage. + + The returned object is a :obj:`collections.defaultdict` with the + ``default_factory`` set to ``None``, such that it behaves like a normal + dictionary. + + """ + valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc + + ret = defaultdict(list) + for item in iterable: + key = keyfunc(item) + value = valuefunc(item) + ret[key].append(value) + + if reducefunc is not None: + for key, value_list in ret.items(): + ret[key] = reducefunc(value_list) + + ret.default_factory = None + return ret + + +def rlocate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``, starting from the right and moving left. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 + [4, 2, 1] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item: + + >>> iterable = iter('abcb') + >>> pred = lambda x: x == 'b' + >>> list(rlocate(iterable, pred)) + [3, 1] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(rlocate(iterable, pred=pred, window_size=3)) + [9, 5, 1] + + Beware, this function won't return anything for infinite iterables. + If *iterable* is reversible, ``rlocate`` will reverse it and search from + the right. Otherwise, it will search from the left and return the results + in reverse order. + + See :func:`locate` to for other example applications. + + """ + if window_size is None: + try: + len_iter = len(iterable) + return ( + len_iter - i - 1 for i in locate(reversed(iterable), pred) + ) + except TypeError: + pass + + return reversed(list(locate(iterable, pred, window_size))) + + +def replace(iterable, pred, substitutes, count=None, window_size=1): + """Yield the items from *iterable*, replacing the items for which *pred* + returns ``True`` with the items from the iterable *substitutes*. + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] + >>> pred = lambda x: x == 0 + >>> substitutes = (2, 3) + >>> list(replace(iterable, pred, substitutes)) + [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] + + If *count* is given, the number of replacements will be limited: + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] + >>> pred = lambda x: x == 0 + >>> substitutes = [None] + >>> list(replace(iterable, pred, substitutes, count=2)) + [1, 1, None, 1, 1, None, 1, 1, 0] + + Use *window_size* to control the number of items passed as arguments to + *pred*. This allows for locating and replacing subsequences. + + >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] + >>> window_size = 3 + >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred + >>> substitutes = [3, 4] # Splice in these items + >>> list(replace(iterable, pred, substitutes, window_size=window_size)) + [3, 4, 5, 3, 4, 5] + + """ + if window_size < 1: + raise ValueError('window_size must be at least 1') + + # Save the substitutes iterable, since it's used more than once + substitutes = tuple(substitutes) + + # Add padding such that the number of windows matches the length of the + # iterable + it = chain(iterable, [_marker] * (window_size - 1)) + windows = windowed(it, window_size) + + n = 0 + for w in windows: + # If the current window matches our predicate (and we haven't hit + # our maximum number of replacements), splice in the substitutes + # and then consume the following windows that overlap with this one. + # For example, if the iterable is (0, 1, 2, 3, 4...) + # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... + # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) + if pred(*w): + if (count is None) or (n < count): + n += 1 + for s in substitutes: + yield s + consume(windows, window_size - 1) + continue + + # If there was no match (or we've reached the replacement limit), + # yield the first item from the window. + if w and (w[0] is not _marker): + yield w[0] diff --git a/libraries/more_itertools/recipes.py b/libraries/more_itertools/recipes.py new file mode 100644 index 00000000..3a7706cb --- /dev/null +++ b/libraries/more_itertools/recipes.py @@ -0,0 +1,565 @@ +"""Imported from the recipes section of the itertools documentation. + +All functions taken from the recipes section of the itertools library docs +[1]_. +Some backward-compatible usability improvements have been made. + +.. [1] http://docs.python.org/library/itertools.html#recipes + +""" +from collections import deque +from itertools import ( + chain, combinations, count, cycle, groupby, islice, repeat, starmap, tee +) +import operator +from random import randrange, sample, choice + +from six import PY2 +from six.moves import filter, filterfalse, map, range, zip, zip_longest + +__all__ = [ + 'accumulate', + 'all_equal', + 'consume', + 'dotproduct', + 'first_true', + 'flatten', + 'grouper', + 'iter_except', + 'ncycles', + 'nth', + 'nth_combination', + 'padnone', + 'pairwise', + 'partition', + 'powerset', + 'prepend', + 'quantify', + 'random_combination_with_replacement', + 'random_combination', + 'random_permutation', + 'random_product', + 'repeatfunc', + 'roundrobin', + 'tabulate', + 'tail', + 'take', + 'unique_everseen', + 'unique_justseen', +] + + +def accumulate(iterable, func=operator.add): + """ + Return an iterator whose items are the accumulated results of a function + (specified by the optional *func* argument) that takes two arguments. + By default, returns accumulated sums with :func:`operator.add`. + + >>> list(accumulate([1, 2, 3, 4, 5])) # Running sum + [1, 3, 6, 10, 15] + >>> list(accumulate([1, 2, 3], func=operator.mul)) # Running product + [1, 2, 6] + >>> list(accumulate([0, 1, -1, 2, 3, 2], func=max)) # Running maximum + [0, 1, 1, 2, 3, 3] + + This function is available in the ``itertools`` module for Python 3.2 and + greater. + + """ + it = iter(iterable) + try: + total = next(it) + except StopIteration: + return + else: + yield total + + for element in it: + total = func(total, element) + yield total + + +def take(n, iterable): + """Return first *n* items of the iterable as a list. + + >>> take(3, range(10)) + [0, 1, 2] + >>> take(5, range(3)) + [0, 1, 2] + + Effectively a short replacement for ``next`` based iterator consumption + when you want more than one item, but less than the whole iterator. + + """ + return list(islice(iterable, n)) + + +def tabulate(function, start=0): + """Return an iterator over the results of ``func(start)``, + ``func(start + 1)``, ``func(start + 2)``... + + *func* should be a function that accepts one integer argument. + + If *start* is not specified it defaults to 0. It will be incremented each + time the iterator is advanced. + + >>> square = lambda x: x ** 2 + >>> iterator = tabulate(square, -3) + >>> take(4, iterator) + [9, 4, 1, 0] + + """ + return map(function, count(start)) + + +def tail(n, iterable): + """Return an iterator over the last *n* items of *iterable*. + + >>> t = tail(3, 'ABCDEFG') + >>> list(t) + ['E', 'F', 'G'] + + """ + return iter(deque(iterable, maxlen=n)) + + +def consume(iterator, n=None): + """Advance *iterable* by *n* steps. If *n* is ``None``, consume it + entirely. + + Efficiently exhausts an iterator without returning values. Defaults to + consuming the whole iterator, but an optional second argument may be + provided to limit consumption. + + >>> i = (x for x in range(10)) + >>> next(i) + 0 + >>> consume(i, 3) + >>> next(i) + 4 + >>> consume(i) + >>> next(i) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + StopIteration + + If the iterator has fewer items remaining than the provided limit, the + whole iterator will be consumed. + + >>> i = (x for x in range(3)) + >>> consume(i, 5) + >>> next(i) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + StopIteration + + """ + # Use functions that consume iterators at C speed. + if n is None: + # feed the entire iterator into a zero-length deque + deque(iterator, maxlen=0) + else: + # advance to the empty slice starting at position n + next(islice(iterator, n, n), None) + + +def nth(iterable, n, default=None): + """Returns the nth item or a default value. + + >>> l = range(10) + >>> nth(l, 3) + 3 + >>> nth(l, 20, "zebra") + 'zebra' + + """ + return next(islice(iterable, n, None), default) + + +def all_equal(iterable): + """ + Returns ``True`` if all the elements are equal to each other. + + >>> all_equal('aaaa') + True + >>> all_equal('aaab') + False + + """ + g = groupby(iterable) + return next(g, True) and not next(g, False) + + +def quantify(iterable, pred=bool): + """Return the how many times the predicate is true. + + >>> quantify([True, False, True]) + 2 + + """ + return sum(map(pred, iterable)) + + +def padnone(iterable): + """Returns the sequence of elements and then returns ``None`` indefinitely. + + >>> take(5, padnone(range(3))) + [0, 1, 2, None, None] + + Useful for emulating the behavior of the built-in :func:`map` function. + + See also :func:`padded`. + + """ + return chain(iterable, repeat(None)) + + +def ncycles(iterable, n): + """Returns the sequence elements *n* times + + >>> list(ncycles(["a", "b"], 3)) + ['a', 'b', 'a', 'b', 'a', 'b'] + + """ + return chain.from_iterable(repeat(tuple(iterable), n)) + + +def dotproduct(vec1, vec2): + """Returns the dot product of the two iterables. + + >>> dotproduct([10, 10], [20, 20]) + 400 + + """ + return sum(map(operator.mul, vec1, vec2)) + + +def flatten(listOfLists): + """Return an iterator flattening one level of nesting in a list of lists. + + >>> list(flatten([[0, 1], [2, 3]])) + [0, 1, 2, 3] + + See also :func:`collapse`, which can flatten multiple levels of nesting. + + """ + return chain.from_iterable(listOfLists) + + +def repeatfunc(func, times=None, *args): + """Call *func* with *args* repeatedly, returning an iterable over the + results. + + If *times* is specified, the iterable will terminate after that many + repetitions: + + >>> from operator import add + >>> times = 4 + >>> args = 3, 5 + >>> list(repeatfunc(add, times, *args)) + [8, 8, 8, 8] + + If *times* is ``None`` the iterable will not terminate: + + >>> from random import randrange + >>> times = None + >>> args = 1, 11 + >>> take(6, repeatfunc(randrange, times, *args)) # doctest:+SKIP + [2, 4, 8, 1, 8, 4] + + """ + if times is None: + return starmap(func, repeat(args)) + return starmap(func, repeat(args, times)) + + +def pairwise(iterable): + """Returns an iterator of paired items, overlapping, from the original + + >>> take(4, pairwise(count())) + [(0, 1), (1, 2), (2, 3), (3, 4)] + + """ + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def grouper(n, iterable, fillvalue=None): + """Collect data into fixed-length chunks or blocks. + + >>> list(grouper(3, 'ABCDEFG', 'x')) + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] + + """ + args = [iter(iterable)] * n + return zip_longest(fillvalue=fillvalue, *args) + + +def roundrobin(*iterables): + """Yields an item from each iterable, alternating between them. + + >>> list(roundrobin('ABC', 'D', 'EF')) + ['A', 'D', 'E', 'B', 'F', 'C'] + + This function produces the same output as :func:`interleave_longest`, but + may perform better for some inputs (in particular when the number of + iterables is small). + + """ + # Recipe credited to George Sakkis + pending = len(iterables) + if PY2: + nexts = cycle(iter(it).next for it in iterables) + else: + nexts = cycle(iter(it).__next__ for it in iterables) + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = cycle(islice(nexts, pending)) + + +def partition(pred, iterable): + """ + Returns a 2-tuple of iterables derived from the input iterable. + The first yields the items that have ``pred(item) == False``. + The second yields the items that have ``pred(item) == True``. + + >>> is_odd = lambda x: x % 2 != 0 + >>> iterable = range(10) + >>> even_items, odd_items = partition(is_odd, iterable) + >>> list(even_items), list(odd_items) + ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) + + """ + # partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 + t1, t2 = tee(iterable) + return filterfalse(pred, t1), filter(pred, t2) + + +def powerset(iterable): + """Yields all possible subsets of the iterable. + + >>> list(powerset([1,2,3])) + [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] + + """ + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) + + +def unique_everseen(iterable, key=None): + """ + Yield unique elements, preserving order. + + >>> list(unique_everseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D'] + >>> list(unique_everseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'D'] + + Sequences with a mix of hashable and unhashable items can be used. + The function will be slower (i.e., `O(n^2)`) for unhashable items. + + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + if key is None: + for element in iterable: + try: + if element not in seenset: + seenset_add(element) + yield element + except TypeError: + if element not in seenlist: + seenlist_add(element) + yield element + else: + for element in iterable: + k = key(element) + try: + if k not in seenset: + seenset_add(k) + yield element + except TypeError: + if k not in seenlist: + seenlist_add(k) + yield element + + +def unique_justseen(iterable, key=None): + """Yields elements in order, ignoring serial duplicates + + >>> list(unique_justseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D', 'A', 'B'] + >>> list(unique_justseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'A', 'D'] + + """ + return map(next, map(operator.itemgetter(1), groupby(iterable, key))) + + +def iter_except(func, exception, first=None): + """Yields results from a function repeatedly until an exception is raised. + + Converts a call-until-exception interface to an iterator interface. + Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel + to end the loop. + + >>> l = [0, 1, 2] + >>> list(iter_except(l.pop, IndexError)) + [2, 1, 0] + + """ + try: + if first is not None: + yield first() + while 1: + yield func() + except exception: + pass + + +def first_true(iterable, default=False, pred=None): + """ + Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item for which + ``pred(item) == True`` . + + >>> first_true(range(10)) + 1 + >>> first_true(range(10), pred=lambda x: x > 5) + 6 + >>> first_true(range(10), default='missing', pred=lambda x: x > 9) + 'missing' + + """ + return next(filter(pred, iterable), default) + + +def random_product(*args, **kwds): + """Draw an item at random from each of the input iterables. + + >>> random_product('abc', range(4), 'XYZ') # doctest:+SKIP + ('c', 3, 'Z') + + If *repeat* is provided as a keyword argument, that many items will be + drawn from each iterable. + + >>> random_product('abcd', range(4), repeat=2) # doctest:+SKIP + ('a', 2, 'd', 3) + + This equivalent to taking a random selection from + ``itertools.product(*args, **kwarg)``. + + """ + pools = [tuple(pool) for pool in args] * kwds.get('repeat', 1) + return tuple(choice(pool) for pool in pools) + + +def random_permutation(iterable, r=None): + """Return a random *r* length permutation of the elements in *iterable*. + + If *r* is not specified or is ``None``, then *r* defaults to the length of + *iterable*. + + >>> random_permutation(range(5)) # doctest:+SKIP + (3, 4, 0, 1, 2) + + This equivalent to taking a random selection from + ``itertools.permutations(iterable, r)``. + + """ + pool = tuple(iterable) + r = len(pool) if r is None else r + return tuple(sample(pool, r)) + + +def random_combination(iterable, r): + """Return a random *r* length subsequence of the elements in *iterable*. + + >>> random_combination(range(5), 3) # doctest:+SKIP + (2, 3, 4) + + This equivalent to taking a random selection from + ``itertools.combinations(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(sample(range(n), r)) + return tuple(pool[i] for i in indices) + + +def random_combination_with_replacement(iterable, r): + """Return a random *r* length subsequence of elements in *iterable*, + allowing individual elements to be repeated. + + >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP + (0, 0, 1, 2, 2) + + This equivalent to taking a random selection from + ``itertools.combinations_with_replacement(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(randrange(n) for i in range(r)) + return tuple(pool[i] for i in indices) + + +def nth_combination(iterable, r, index): + """Equivalent to ``list(combinations(iterable, r))[index]``. + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`nth_combination` computes the subsequence at + sort position *index* directly, without computing the previous + subsequences. + + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = 1 + k = min(r, n - r) + for i in range(1, k + 1): + c = c * (n - k + i) // i + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + while r: + c, n, r = c * r // n, n - 1, r - 1 + while index >= c: + index -= c + c, n = c * (n - r) // n, n - 1 + result.append(pool[-1 - n]) + + return tuple(result) + + +def prepend(value, iterator): + """Yield *value*, followed by the elements in *iterator*. + + >>> value = '0' + >>> iterator = ['1', '2', '3'] + >>> list(prepend(value, iterator)) + ['0', '1', '2', '3'] + + To prepend multiple values, see :func:`itertools.chain`. + + """ + return chain([value], iterator) diff --git a/libraries/more_itertools/tests/__init__.py b/libraries/more_itertools/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libraries/more_itertools/tests/test_more.py b/libraries/more_itertools/tests/test_more.py new file mode 100644 index 00000000..a1b1e431 --- /dev/null +++ b/libraries/more_itertools/tests/test_more.py @@ -0,0 +1,2074 @@ +from __future__ import division, print_function, unicode_literals + +from collections import OrderedDict +from decimal import Decimal +from doctest import DocTestSuite +from fractions import Fraction +from functools import partial, reduce +from heapq import merge +from io import StringIO +from itertools import ( + chain, + count, + groupby, + islice, + permutations, + product, + repeat, +) +from operator import add, mul, itemgetter +from unittest import TestCase + +from six.moves import filter, map, range, zip + +import more_itertools as mi + + +def load_tests(loader, tests, ignore): + # Add the doctests + tests.addTests(DocTestSuite('more_itertools.more')) + return tests + + +class CollateTests(TestCase): + """Unit tests for ``collate()``""" + # Also accidentally tests peekable, though that could use its own tests + + def test_default(self): + """Test with the default `key` function.""" + iterables = [range(4), range(7), range(3, 6)] + self.assertEqual( + sorted(reduce(list.__add__, [list(it) for it in iterables])), + list(mi.collate(*iterables)) + ) + + def test_key(self): + """Test using a custom `key` function.""" + iterables = [range(5, 0, -1), range(4, 0, -1)] + actual = sorted( + reduce(list.__add__, [list(it) for it in iterables]), reverse=True + ) + expected = list(mi.collate(*iterables, key=lambda x: -x)) + self.assertEqual(actual, expected) + + def test_empty(self): + """Be nice if passed an empty list of iterables.""" + self.assertEqual([], list(mi.collate())) + + def test_one(self): + """Work when only 1 iterable is passed.""" + self.assertEqual([0, 1], list(mi.collate(range(2)))) + + def test_reverse(self): + """Test the `reverse` kwarg.""" + iterables = [range(4, 0, -1), range(7, 0, -1), range(3, 6, -1)] + + actual = sorted( + reduce(list.__add__, [list(it) for it in iterables]), reverse=True + ) + expected = list(mi.collate(*iterables, reverse=True)) + self.assertEqual(actual, expected) + + def test_alias(self): + self.assertNotEqual(merge.__doc__, mi.collate.__doc__) + self.assertNotEqual(partial.__doc__, mi.collate.__doc__) + + +class ChunkedTests(TestCase): + """Tests for ``chunked()``""" + + def test_even(self): + """Test when ``n`` divides evenly into the length of the iterable.""" + self.assertEqual( + list(mi.chunked('ABCDEF', 3)), [['A', 'B', 'C'], ['D', 'E', 'F']] + ) + + def test_odd(self): + """Test when ``n`` does not divide evenly into the length of the + iterable. + + """ + self.assertEqual( + list(mi.chunked('ABCDE', 3)), [['A', 'B', 'C'], ['D', 'E']] + ) + + +class FirstTests(TestCase): + """Tests for ``first()``""" + + def test_many(self): + """Test that it works on many-item iterables.""" + # Also try it on a generator expression to make sure it works on + # whatever those return, across Python versions. + self.assertEqual(mi.first(x for x in range(4)), 0) + + def test_one(self): + """Test that it doesn't raise StopIteration prematurely.""" + self.assertEqual(mi.first([3]), 3) + + def test_empty_stop_iteration(self): + """It should raise StopIteration for empty iterables.""" + self.assertRaises(ValueError, lambda: mi.first([])) + + def test_default(self): + """It should return the provided default arg for empty iterables.""" + self.assertEqual(mi.first([], 'boo'), 'boo') + + +class IterOnlyRange: + """User-defined iterable class which only support __iter__. + + It is not specified to inherit ``object``, so indexing on a instance will + raise an ``AttributeError`` rather than ``TypeError`` in Python 2. + + >>> r = IterOnlyRange(5) + >>> r[0] + AttributeError: IterOnlyRange instance has no attribute '__getitem__' + + Note: In Python 3, ``TypeError`` will be raised because ``object`` is + inherited implicitly by default. + + >>> r[0] + TypeError: 'IterOnlyRange' object does not support indexing + """ + def __init__(self, n): + """Set the length of the range.""" + self.n = n + + def __iter__(self): + """Works same as range().""" + return iter(range(self.n)) + + +class LastTests(TestCase): + """Tests for ``last()``""" + + def test_many_nonsliceable(self): + """Test that it works on many-item non-slice-able iterables.""" + # Also try it on a generator expression to make sure it works on + # whatever those return, across Python versions. + self.assertEqual(mi.last(x for x in range(4)), 3) + + def test_one_nonsliceable(self): + """Test that it doesn't raise StopIteration prematurely.""" + self.assertEqual(mi.last(x for x in range(1)), 0) + + def test_empty_stop_iteration_nonsliceable(self): + """It should raise ValueError for empty non-slice-able iterables.""" + self.assertRaises(ValueError, lambda: mi.last(x for x in range(0))) + + def test_default_nonsliceable(self): + """It should return the provided default arg for empty non-slice-able + iterables. + """ + self.assertEqual(mi.last((x for x in range(0)), 'boo'), 'boo') + + def test_many_sliceable(self): + """Test that it works on many-item slice-able iterables.""" + self.assertEqual(mi.last([0, 1, 2, 3]), 3) + + def test_one_sliceable(self): + """Test that it doesn't raise StopIteration prematurely.""" + self.assertEqual(mi.last([3]), 3) + + def test_empty_stop_iteration_sliceable(self): + """It should raise ValueError for empty slice-able iterables.""" + self.assertRaises(ValueError, lambda: mi.last([])) + + def test_default_sliceable(self): + """It should return the provided default arg for empty slice-able + iterables. + """ + self.assertEqual(mi.last([], 'boo'), 'boo') + + def test_dict(self): + """last(dic) and last(dic.keys()) should return same result.""" + dic = {'a': 1, 'b': 2, 'c': 3} + self.assertEqual(mi.last(dic), mi.last(dic.keys())) + + def test_ordereddict(self): + """last(dic) should return the last key.""" + od = OrderedDict() + od['a'] = 1 + od['b'] = 2 + od['c'] = 3 + self.assertEqual(mi.last(od), 'c') + + def test_customrange(self): + """It should work on custom class where [] raises AttributeError.""" + self.assertEqual(mi.last(IterOnlyRange(5)), 4) + + +class PeekableTests(TestCase): + """Tests for ``peekable()`` behavor not incidentally covered by testing + ``collate()`` + + """ + def test_peek_default(self): + """Make sure passing a default into ``peek()`` works.""" + p = mi.peekable([]) + self.assertEqual(p.peek(7), 7) + + def test_truthiness(self): + """Make sure a ``peekable`` tests true iff there are items remaining in + the iterable. + + """ + p = mi.peekable([]) + self.assertFalse(p) + + p = mi.peekable(range(3)) + self.assertTrue(p) + + def test_simple_peeking(self): + """Make sure ``next`` and ``peek`` advance and don't advance the + iterator, respectively. + + """ + p = mi.peekable(range(10)) + self.assertEqual(next(p), 0) + self.assertEqual(p.peek(), 1) + self.assertEqual(next(p), 1) + + def test_indexing(self): + """ + Indexing into the peekable shouldn't advance the iterator. + """ + p = mi.peekable('abcdefghijkl') + + # The 0th index is what ``next()`` will return + self.assertEqual(p[0], 'a') + self.assertEqual(next(p), 'a') + + # Indexing further into the peekable shouldn't advance the itertor + self.assertEqual(p[2], 'd') + self.assertEqual(next(p), 'b') + + # The 0th index moves up with the iterator; the last index follows + self.assertEqual(p[0], 'c') + self.assertEqual(p[9], 'l') + + self.assertEqual(next(p), 'c') + self.assertEqual(p[8], 'l') + + # Negative indexing should work too + self.assertEqual(p[-2], 'k') + self.assertEqual(p[-9], 'd') + self.assertRaises(IndexError, lambda: p[-10]) + + def test_slicing(self): + """Slicing the peekable shouldn't advance the iterator.""" + seq = list('abcdefghijkl') + p = mi.peekable(seq) + + # Slicing the peekable should just be like slicing a re-iterable + self.assertEqual(p[1:4], seq[1:4]) + + # Advancing the iterator moves the slices up also + self.assertEqual(next(p), 'a') + self.assertEqual(p[1:4], seq[1:][1:4]) + + # Implicit starts and stop should work + self.assertEqual(p[:5], seq[1:][:5]) + self.assertEqual(p[:], seq[1:][:]) + + # Indexing past the end should work + self.assertEqual(p[:100], seq[1:][:100]) + + # Steps should work, including negative + self.assertEqual(p[::2], seq[1:][::2]) + self.assertEqual(p[::-1], seq[1:][::-1]) + + def test_slicing_reset(self): + """Test slicing on a fresh iterable each time""" + iterable = ['0', '1', '2', '3', '4', '5'] + indexes = list(range(-4, len(iterable) + 4)) + [None] + steps = [1, 2, 3, 4, -1, -2, -3, 4] + for slice_args in product(indexes, indexes, steps): + it = iter(iterable) + p = mi.peekable(it) + next(p) + index = slice(*slice_args) + actual = p[index] + expected = iterable[1:][index] + self.assertEqual(actual, expected, slice_args) + + def test_slicing_error(self): + iterable = '01234567' + p = mi.peekable(iter(iterable)) + + # Prime the cache + p.peek() + old_cache = list(p._cache) + + # Illegal slice + with self.assertRaises(ValueError): + p[1:-1:0] + + # Neither the cache nor the iteration should be affected + self.assertEqual(old_cache, list(p._cache)) + self.assertEqual(list(p), list(iterable)) + + def test_passthrough(self): + """Iterating a peekable without using ``peek()`` or ``prepend()`` + should just give the underlying iterable's elements (a trivial test but + useful to set a baseline in case something goes wrong)""" + expected = [1, 2, 3, 4, 5] + actual = list(mi.peekable(expected)) + self.assertEqual(actual, expected) + + # prepend() behavior tests + + def test_prepend(self): + """Tests intersperesed ``prepend()`` and ``next()`` calls""" + it = mi.peekable(range(2)) + actual = [] + + # Test prepend() before next() + it.prepend(10) + actual += [next(it), next(it)] + + # Test prepend() between next()s + it.prepend(11) + actual += [next(it), next(it)] + + # Test prepend() after source iterable is consumed + it.prepend(12) + actual += [next(it)] + + expected = [10, 0, 11, 1, 12] + self.assertEqual(actual, expected) + + def test_multi_prepend(self): + """Tests prepending multiple items and getting them in proper order""" + it = mi.peekable(range(5)) + actual = [next(it), next(it)] + it.prepend(10, 11, 12) + it.prepend(20, 21) + actual += list(it) + expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4] + self.assertEqual(actual, expected) + + def test_empty(self): + """Tests prepending in front of an empty iterable""" + it = mi.peekable([]) + it.prepend(10) + actual = list(it) + expected = [10] + self.assertEqual(actual, expected) + + def test_prepend_truthiness(self): + """Tests that ``__bool__()`` or ``__nonzero__()`` works properly + with ``prepend()``""" + it = mi.peekable(range(5)) + self.assertTrue(it) + actual = list(it) + self.assertFalse(it) + it.prepend(10) + self.assertTrue(it) + actual += [next(it)] + self.assertFalse(it) + expected = [0, 1, 2, 3, 4, 10] + self.assertEqual(actual, expected) + + def test_multi_prepend_peek(self): + """Tests prepending multiple elements and getting them in reverse order + while peeking""" + it = mi.peekable(range(5)) + actual = [next(it), next(it)] + self.assertEqual(it.peek(), 2) + it.prepend(10, 11, 12) + self.assertEqual(it.peek(), 10) + it.prepend(20, 21) + self.assertEqual(it.peek(), 20) + actual += list(it) + self.assertFalse(it) + expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4] + self.assertEqual(actual, expected) + + def test_prepend_after_stop(self): + """Test resuming iteration after a previous exhaustion""" + it = mi.peekable(range(3)) + self.assertEqual(list(it), [0, 1, 2]) + self.assertRaises(StopIteration, lambda: next(it)) + it.prepend(10) + self.assertEqual(next(it), 10) + self.assertRaises(StopIteration, lambda: next(it)) + + def test_prepend_slicing(self): + """Tests interaction between prepending and slicing""" + seq = list(range(20)) + p = mi.peekable(seq) + + p.prepend(30, 40, 50) + pseq = [30, 40, 50] + seq # pseq for prepended_seq + + # adapt the specific tests from test_slicing + self.assertEqual(p[0], 30) + self.assertEqual(p[1:8], pseq[1:8]) + self.assertEqual(p[1:], pseq[1:]) + self.assertEqual(p[:5], pseq[:5]) + self.assertEqual(p[:], pseq[:]) + self.assertEqual(p[:100], pseq[:100]) + self.assertEqual(p[::2], pseq[::2]) + self.assertEqual(p[::-1], pseq[::-1]) + + def test_prepend_indexing(self): + """Tests interaction between prepending and indexing""" + seq = list(range(20)) + p = mi.peekable(seq) + + p.prepend(30, 40, 50) + + self.assertEqual(p[0], 30) + self.assertEqual(next(p), 30) + self.assertEqual(p[2], 0) + self.assertEqual(next(p), 40) + self.assertEqual(p[0], 50) + self.assertEqual(p[9], 8) + self.assertEqual(next(p), 50) + self.assertEqual(p[8], 8) + self.assertEqual(p[-2], 18) + self.assertEqual(p[-9], 11) + self.assertRaises(IndexError, lambda: p[-21]) + + def test_prepend_iterable(self): + """Tests prepending from an iterable""" + it = mi.peekable(range(5)) + # Don't directly use the range() object to avoid any range-specific + # optimizations + it.prepend(*(x for x in range(5))) + actual = list(it) + expected = list(chain(range(5), range(5))) + self.assertEqual(actual, expected) + + def test_prepend_many(self): + """Tests that prepending a huge number of elements works""" + it = mi.peekable(range(5)) + # Don't directly use the range() object to avoid any range-specific + # optimizations + it.prepend(*(x for x in range(20000))) + actual = list(it) + expected = list(chain(range(20000), range(5))) + self.assertEqual(actual, expected) + + def test_prepend_reversed(self): + """Tests prepending from a reversed iterable""" + it = mi.peekable(range(3)) + it.prepend(*reversed((10, 11, 12))) + actual = list(it) + expected = [12, 11, 10, 0, 1, 2] + self.assertEqual(actual, expected) + + +class ConsumerTests(TestCase): + """Tests for ``consumer()``""" + + def test_consumer(self): + @mi.consumer + def eater(): + while True: + x = yield # noqa + + e = eater() + e.send('hi') # without @consumer, would raise TypeError + + +class DistinctPermutationsTests(TestCase): + def test_distinct_permutations(self): + """Make sure the output for ``distinct_permutations()`` is the same as + set(permutations(it)). + + """ + iterable = ['z', 'a', 'a', 'q', 'q', 'q', 'y'] + test_output = sorted(mi.distinct_permutations(iterable)) + ref_output = sorted(set(permutations(iterable))) + self.assertEqual(test_output, ref_output) + + def test_other_iterables(self): + """Make sure ``distinct_permutations()`` accepts a different type of + iterables. + + """ + # a generator + iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y']) + test_output = sorted(mi.distinct_permutations(iterable)) + # "reload" it + iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y']) + ref_output = sorted(set(permutations(iterable))) + self.assertEqual(test_output, ref_output) + + # an iterator + iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y']) + test_output = sorted(mi.distinct_permutations(iterable)) + # "reload" it + iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y']) + ref_output = sorted(set(permutations(iterable))) + self.assertEqual(test_output, ref_output) + + +class IlenTests(TestCase): + def test_ilen(self): + """Sanity-checks for ``ilen()``.""" + # Non-empty + self.assertEqual( + mi.ilen(filter(lambda x: x % 10 == 0, range(101))), 11 + ) + + # Empty + self.assertEqual(mi.ilen((x for x in range(0))), 0) + + # Iterable with __len__ + self.assertEqual(mi.ilen(list(range(6))), 6) + + +class WithIterTests(TestCase): + def test_with_iter(self): + s = StringIO('One fish\nTwo fish') + initial_words = [line.split()[0] for line in mi.with_iter(s)] + + # Iterable's items should be faithfully represented + self.assertEqual(initial_words, ['One', 'Two']) + # The file object should be closed + self.assertEqual(s.closed, True) + + +class OneTests(TestCase): + def test_basic(self): + it = iter(['item']) + self.assertEqual(mi.one(it), 'item') + + def test_too_short(self): + it = iter([]) + self.assertRaises(ValueError, lambda: mi.one(it)) + self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError)) + + def test_too_long(self): + it = count() + self.assertRaises(ValueError, lambda: mi.one(it)) # burn 0 and 1 + self.assertEqual(next(it), 2) + self.assertRaises( + OverflowError, lambda: mi.one(it, too_long=OverflowError) + ) + + +class IntersperseTest(TestCase): + """ Tests for intersperse() """ + + def test_even(self): + iterable = (x for x in '01') + self.assertEqual( + list(mi.intersperse(None, iterable)), ['0', None, '1'] + ) + + def test_odd(self): + iterable = (x for x in '012') + self.assertEqual( + list(mi.intersperse(None, iterable)), ['0', None, '1', None, '2'] + ) + + def test_nested(self): + element = ('a', 'b') + iterable = (x for x in '012') + actual = list(mi.intersperse(element, iterable)) + expected = ['0', ('a', 'b'), '1', ('a', 'b'), '2'] + self.assertEqual(actual, expected) + + def test_not_iterable(self): + self.assertRaises(TypeError, lambda: mi.intersperse('x', 1)) + + def test_n(self): + for n, element, expected in [ + (1, '_', ['0', '_', '1', '_', '2', '_', '3', '_', '4', '_', '5']), + (2, '_', ['0', '1', '_', '2', '3', '_', '4', '5']), + (3, '_', ['0', '1', '2', '_', '3', '4', '5']), + (4, '_', ['0', '1', '2', '3', '_', '4', '5']), + (5, '_', ['0', '1', '2', '3', '4', '_', '5']), + (6, '_', ['0', '1', '2', '3', '4', '5']), + (7, '_', ['0', '1', '2', '3', '4', '5']), + (3, ['a', 'b'], ['0', '1', '2', ['a', 'b'], '3', '4', '5']), + ]: + iterable = (x for x in '012345') + actual = list(mi.intersperse(element, iterable, n=n)) + self.assertEqual(actual, expected) + + def test_n_zero(self): + self.assertRaises( + ValueError, lambda: list(mi.intersperse('x', '012', n=0)) + ) + + +class UniqueToEachTests(TestCase): + """Tests for ``unique_to_each()``""" + + def test_all_unique(self): + """When all the input iterables are unique the output should match + the input.""" + iterables = [[1, 2], [3, 4, 5], [6, 7, 8]] + self.assertEqual(mi.unique_to_each(*iterables), iterables) + + def test_duplicates(self): + """When there are duplicates in any of the input iterables that aren't + in the rest, those duplicates should be emitted.""" + iterables = ["mississippi", "missouri"] + self.assertEqual( + mi.unique_to_each(*iterables), [['p', 'p'], ['o', 'u', 'r']] + ) + + def test_mixed(self): + """When the input iterables contain different types the function should + still behave properly""" + iterables = ['x', (i for i in range(3)), [1, 2, 3], tuple()] + self.assertEqual(mi.unique_to_each(*iterables), [['x'], [0], [3], []]) + + +class WindowedTests(TestCase): + """Tests for ``windowed()``""" + + def test_basic(self): + actual = list(mi.windowed([1, 2, 3, 4, 5], 3)) + expected = [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + self.assertEqual(actual, expected) + + def test_large_size(self): + """ + When the window size is larger than the iterable, and no fill value is + given,``None`` should be filled in. + """ + actual = list(mi.windowed([1, 2, 3, 4, 5], 6)) + expected = [(1, 2, 3, 4, 5, None)] + self.assertEqual(actual, expected) + + def test_fillvalue(self): + """ + When sizes don't match evenly, the given fill value should be used. + """ + iterable = [1, 2, 3, 4, 5] + + for n, kwargs, expected in [ + (6, {}, [(1, 2, 3, 4, 5, '!')]), # n > len(iterable) + (3, {'step': 3}, [(1, 2, 3), (4, 5, '!')]), # using ``step`` + ]: + actual = list(mi.windowed(iterable, n, fillvalue='!', **kwargs)) + self.assertEqual(actual, expected) + + def test_zero(self): + """When the window size is zero, an empty tuple should be emitted.""" + actual = list(mi.windowed([1, 2, 3, 4, 5], 0)) + expected = [tuple()] + self.assertEqual(actual, expected) + + def test_negative(self): + """When the window size is negative, ValueError should be raised.""" + with self.assertRaises(ValueError): + list(mi.windowed([1, 2, 3, 4, 5], -1)) + + def test_step(self): + """The window should advance by the number of steps provided""" + iterable = [1, 2, 3, 4, 5, 6, 7] + for n, step, expected in [ + (3, 2, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]), # n > step + (3, 3, [(1, 2, 3), (4, 5, 6), (7, None, None)]), # n == step + (3, 4, [(1, 2, 3), (5, 6, 7)]), # line up nicely + (3, 5, [(1, 2, 3), (6, 7, None)]), # off by one + (3, 6, [(1, 2, 3), (7, None, None)]), # off by two + (3, 7, [(1, 2, 3)]), # step past the end + (7, 8, [(1, 2, 3, 4, 5, 6, 7)]), # step > len(iterable) + ]: + actual = list(mi.windowed(iterable, n, step=step)) + self.assertEqual(actual, expected) + + # Step must be greater than or equal to 1 + with self.assertRaises(ValueError): + list(mi.windowed(iterable, 3, step=0)) + + +class BucketTests(TestCase): + """Tests for ``bucket()``""" + + def test_basic(self): + iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33] + D = mi.bucket(iterable, key=lambda x: 10 * (x // 10)) + + # In-order access + self.assertEqual(list(D[10]), [10, 11, 12]) + + # Out of order access + self.assertEqual(list(D[30]), [30, 31, 33]) + self.assertEqual(list(D[20]), [20, 21, 22, 23]) + + self.assertEqual(list(D[40]), []) # Nothing in here! + + def test_in(self): + iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33] + D = mi.bucket(iterable, key=lambda x: 10 * (x // 10)) + + self.assertTrue(10 in D) + self.assertFalse(40 in D) + self.assertTrue(20 in D) + self.assertFalse(21 in D) + + # Checking in-ness shouldn't advance the iterator + self.assertEqual(next(D[10]), 10) + + def test_validator(self): + iterable = count(0) + key = lambda x: int(str(x)[0]) # First digit of each number + validator = lambda x: 0 < x < 10 # No leading zeros + D = mi.bucket(iterable, key, validator=validator) + self.assertEqual(mi.take(3, D[1]), [1, 10, 11]) + self.assertNotIn(0, D) # Non-valid entries don't return True + self.assertNotIn(0, D._cache) # Don't store non-valid entries + self.assertEqual(list(D[0]), []) + + +class SpyTests(TestCase): + """Tests for ``spy()``""" + + def test_basic(self): + original_iterable = iter('abcdefg') + head, new_iterable = mi.spy(original_iterable) + self.assertEqual(head, ['a']) + self.assertEqual( + list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + ) + + def test_unpacking(self): + original_iterable = iter('abcdefg') + (first, second, third), new_iterable = mi.spy(original_iterable, 3) + self.assertEqual(first, 'a') + self.assertEqual(second, 'b') + self.assertEqual(third, 'c') + self.assertEqual( + list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + ) + + def test_too_many(self): + original_iterable = iter('abc') + head, new_iterable = mi.spy(original_iterable, 4) + self.assertEqual(head, ['a', 'b', 'c']) + self.assertEqual(list(new_iterable), ['a', 'b', 'c']) + + def test_zero(self): + original_iterable = iter('abc') + head, new_iterable = mi.spy(original_iterable, 0) + self.assertEqual(head, []) + self.assertEqual(list(new_iterable), ['a', 'b', 'c']) + + +class InterleaveTests(TestCase): + def test_even(self): + actual = list(mi.interleave([1, 4, 7], [2, 5, 8], [3, 6, 9])) + expected = [1, 2, 3, 4, 5, 6, 7, 8, 9] + self.assertEqual(actual, expected) + + def test_short(self): + actual = list(mi.interleave([1, 4], [2, 5, 7], [3, 6, 8])) + expected = [1, 2, 3, 4, 5, 6] + self.assertEqual(actual, expected) + + def test_mixed_types(self): + it_list = ['a', 'b', 'c', 'd'] + it_str = '12345' + it_inf = count() + actual = list(mi.interleave(it_list, it_str, it_inf)) + expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', 3] + self.assertEqual(actual, expected) + + +class InterleaveLongestTests(TestCase): + def test_even(self): + actual = list(mi.interleave_longest([1, 4, 7], [2, 5, 8], [3, 6, 9])) + expected = [1, 2, 3, 4, 5, 6, 7, 8, 9] + self.assertEqual(actual, expected) + + def test_short(self): + actual = list(mi.interleave_longest([1, 4], [2, 5, 7], [3, 6, 8])) + expected = [1, 2, 3, 4, 5, 6, 7, 8] + self.assertEqual(actual, expected) + + def test_mixed_types(self): + it_list = ['a', 'b', 'c', 'd'] + it_str = '12345' + it_gen = (x for x in range(3)) + actual = list(mi.interleave_longest(it_list, it_str, it_gen)) + expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', '5'] + self.assertEqual(actual, expected) + + +class TestCollapse(TestCase): + """Tests for ``collapse()``""" + + def test_collapse(self): + l = [[1], 2, [[3], 4], [[[5]]]] + self.assertEqual(list(mi.collapse(l)), [1, 2, 3, 4, 5]) + + def test_collapse_to_string(self): + l = [["s1"], "s2", [["s3"], "s4"], [[["s5"]]]] + self.assertEqual(list(mi.collapse(l)), ["s1", "s2", "s3", "s4", "s5"]) + + def test_collapse_flatten(self): + l = [[1], [2], [[3], 4], [[[5]]]] + self.assertEqual(list(mi.collapse(l, levels=1)), list(mi.flatten(l))) + + def test_collapse_to_level(self): + l = [[1], 2, [[3], 4], [[[5]]]] + self.assertEqual(list(mi.collapse(l, levels=2)), [1, 2, 3, 4, [5]]) + self.assertEqual( + list(mi.collapse(mi.collapse(l, levels=1), levels=1)), + list(mi.collapse(l, levels=2)) + ) + + def test_collapse_to_list(self): + l = (1, [2], (3, [4, (5,)], 'ab')) + actual = list(mi.collapse(l, base_type=list)) + expected = [1, [2], 3, [4, (5,)], 'ab'] + self.assertEqual(actual, expected) + + +class SideEffectTests(TestCase): + """Tests for ``side_effect()``""" + + def test_individual(self): + # The function increments the counter for each call + counter = [0] + + def func(arg): + counter[0] += 1 + + result = list(mi.side_effect(func, range(10))) + self.assertEqual(result, list(range(10))) + self.assertEqual(counter[0], 10) + + def test_chunked(self): + # The function increments the counter for each call + counter = [0] + + def func(arg): + counter[0] += 1 + + result = list(mi.side_effect(func, range(10), 2)) + self.assertEqual(result, list(range(10))) + self.assertEqual(counter[0], 5) + + def test_before_after(self): + f = StringIO() + collector = [] + + def func(item): + print(item, file=f) + collector.append(f.getvalue()) + + def it(): + yield u'a' + yield u'b' + raise RuntimeError('kaboom') + + before = lambda: print('HEADER', file=f) + after = f.close + + try: + mi.consume(mi.side_effect(func, it(), before=before, after=after)) + except RuntimeError: + pass + + # The iterable should have been written to the file + self.assertEqual(collector, [u'HEADER\na\n', u'HEADER\na\nb\n']) + + # The file should be closed even though something bad happened + self.assertTrue(f.closed) + + def test_before_fails(self): + f = StringIO() + func = lambda x: print(x, file=f) + + def before(): + raise RuntimeError('ouch') + + try: + mi.consume( + mi.side_effect(func, u'abc', before=before, after=f.close) + ) + except RuntimeError: + pass + + # The file should be closed even though something bad happened in the + # before function + self.assertTrue(f.closed) + + +class SlicedTests(TestCase): + """Tests for ``sliced()``""" + + def test_even(self): + """Test when the length of the sequence is divisible by *n*""" + seq = 'ABCDEFGHI' + self.assertEqual(list(mi.sliced(seq, 3)), ['ABC', 'DEF', 'GHI']) + + def test_odd(self): + """Test when the length of the sequence is not divisible by *n*""" + seq = 'ABCDEFGHI' + self.assertEqual(list(mi.sliced(seq, 4)), ['ABCD', 'EFGH', 'I']) + + def test_not_sliceable(self): + seq = (x for x in 'ABCDEFGHI') + + with self.assertRaises(TypeError): + list(mi.sliced(seq, 3)) + + +class SplitAtTests(TestCase): + """Tests for ``split()``""" + + def comp_with_str_split(self, str_to_split, delim): + pred = lambda c: c == delim + actual = list(map(''.join, mi.split_at(str_to_split, pred))) + expected = str_to_split.split(delim) + self.assertEqual(actual, expected) + + def test_seperators(self): + test_strs = ['', 'abcba', 'aaabbbcccddd', 'e'] + for s, delim in product(test_strs, 'abcd'): + self.comp_with_str_split(s, delim) + + +class SplitBeforeTest(TestCase): + """Tests for ``split_before()``""" + + def test_starts_with_sep(self): + actual = list(mi.split_before('xooxoo', lambda c: c == 'x')) + expected = [['x', 'o', 'o'], ['x', 'o', 'o']] + self.assertEqual(actual, expected) + + def test_ends_with_sep(self): + actual = list(mi.split_before('ooxoox', lambda c: c == 'x')) + expected = [['o', 'o'], ['x', 'o', 'o'], ['x']] + self.assertEqual(actual, expected) + + def test_no_sep(self): + actual = list(mi.split_before('ooo', lambda c: c == 'x')) + expected = [['o', 'o', 'o']] + self.assertEqual(actual, expected) + + +class SplitAfterTest(TestCase): + """Tests for ``split_after()``""" + + def test_starts_with_sep(self): + actual = list(mi.split_after('xooxoo', lambda c: c == 'x')) + expected = [['x'], ['o', 'o', 'x'], ['o', 'o']] + self.assertEqual(actual, expected) + + def test_ends_with_sep(self): + actual = list(mi.split_after('ooxoox', lambda c: c == 'x')) + expected = [['o', 'o', 'x'], ['o', 'o', 'x']] + self.assertEqual(actual, expected) + + def test_no_sep(self): + actual = list(mi.split_after('ooo', lambda c: c == 'x')) + expected = [['o', 'o', 'o']] + self.assertEqual(actual, expected) + + +class PaddedTest(TestCase): + """Tests for ``padded()``""" + + def test_no_n(self): + seq = [1, 2, 3] + + # No fillvalue + self.assertEqual(mi.take(5, mi.padded(seq)), [1, 2, 3, None, None]) + + # With fillvalue + self.assertEqual( + mi.take(5, mi.padded(seq, fillvalue='')), [1, 2, 3, '', ''] + ) + + def test_invalid_n(self): + self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=-1))) + self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=0))) + + def test_valid_n(self): + seq = [1, 2, 3, 4, 5] + + # No need for padding: len(seq) <= n + self.assertEqual(list(mi.padded(seq, n=4)), [1, 2, 3, 4, 5]) + self.assertEqual(list(mi.padded(seq, n=5)), [1, 2, 3, 4, 5]) + + # No fillvalue + self.assertEqual( + list(mi.padded(seq, n=7)), [1, 2, 3, 4, 5, None, None] + ) + + # With fillvalue + self.assertEqual( + list(mi.padded(seq, fillvalue='', n=7)), [1, 2, 3, 4, 5, '', ''] + ) + + def test_next_multiple(self): + seq = [1, 2, 3, 4, 5, 6] + + # No need for padding: len(seq) % n == 0 + self.assertEqual( + list(mi.padded(seq, n=3, next_multiple=True)), [1, 2, 3, 4, 5, 6] + ) + + # Padding needed: len(seq) < n + self.assertEqual( + list(mi.padded(seq, n=8, next_multiple=True)), + [1, 2, 3, 4, 5, 6, None, None] + ) + + # No padding needed: len(seq) == n + self.assertEqual( + list(mi.padded(seq, n=6, next_multiple=True)), [1, 2, 3, 4, 5, 6] + ) + + # Padding needed: len(seq) > n + self.assertEqual( + list(mi.padded(seq, n=4, next_multiple=True)), + [1, 2, 3, 4, 5, 6, None, None] + ) + + # With fillvalue + self.assertEqual( + list(mi.padded(seq, fillvalue='', n=4, next_multiple=True)), + [1, 2, 3, 4, 5, 6, '', ''] + ) + + +class DistributeTest(TestCase): + """Tests for distribute()""" + + def test_invalid_n(self): + self.assertRaises(ValueError, lambda: mi.distribute(-1, [1, 2, 3])) + self.assertRaises(ValueError, lambda: mi.distribute(0, [1, 2, 3])) + + def test_basic(self): + iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + for n, expected in [ + (1, [iterable]), + (2, [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]), + (3, [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]), + (10, [[n] for n in range(1, 10 + 1)]), + ]: + self.assertEqual( + [list(x) for x in mi.distribute(n, iterable)], expected + ) + + def test_large_n(self): + iterable = [1, 2, 3, 4] + self.assertEqual( + [list(x) for x in mi.distribute(6, iterable)], + [[1], [2], [3], [4], [], []] + ) + + +class StaggerTest(TestCase): + """Tests for ``stagger()``""" + + def test_default(self): + iterable = [0, 1, 2, 3] + actual = list(mi.stagger(iterable)) + expected = [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + self.assertEqual(actual, expected) + + def test_offsets(self): + iterable = [0, 1, 2, 3] + for offsets, expected in [ + ((-2, 0, 2), [('', 0, 2), ('', 1, 3)]), + ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3)]), + ((1, 2), [(1, 2), (2, 3)]), + ]: + all_groups = mi.stagger(iterable, offsets=offsets, fillvalue='') + self.assertEqual(list(all_groups), expected) + + def test_longest(self): + iterable = [0, 1, 2, 3] + for offsets, expected in [ + ( + (-1, 0, 1), + [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, ''), (3, '', '')] + ), + ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3), (3, '')]), + ((1, 2), [(1, 2), (2, 3), (3, '')]), + ]: + all_groups = mi.stagger( + iterable, offsets=offsets, fillvalue='', longest=True + ) + self.assertEqual(list(all_groups), expected) + + +class ZipOffsetTest(TestCase): + """Tests for ``zip_offset()``""" + + def test_shortest(self): + a_1 = [0, 1, 2, 3] + a_2 = [0, 1, 2, 3, 4, 5] + a_3 = [0, 1, 2, 3, 4, 5, 6, 7] + actual = list( + mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), fillvalue='') + ) + expected = [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)] + self.assertEqual(actual, expected) + + def test_longest(self): + a_1 = [0, 1, 2, 3] + a_2 = [0, 1, 2, 3, 4, 5] + a_3 = [0, 1, 2, 3, 4, 5, 6, 7] + actual = list( + mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), longest=True) + ) + expected = [ + (None, 0, 1), + (0, 1, 2), + (1, 2, 3), + (2, 3, 4), + (3, 4, 5), + (None, 5, 6), + (None, None, 7), + ] + self.assertEqual(actual, expected) + + def test_mismatch(self): + iterables = [0, 1, 2], [2, 3, 4] + offsets = (-1, 0, 1) + self.assertRaises( + ValueError, + lambda: list(mi.zip_offset(*iterables, offsets=offsets)) + ) + + +class SortTogetherTest(TestCase): + """Tests for sort_together()""" + + def test_key_list(self): + """tests `key_list` including default, iterables include duplicates""" + iterables = [ + ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], + ['May', 'Aug.', 'May', 'June', 'July', 'July'], + [97, 20, 100, 70, 100, 20] + ] + + self.assertEqual( + mi.sort_together(iterables), + [ + ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), + ('June', 'July', 'July', 'May', 'Aug.', 'May'), + (70, 100, 20, 97, 20, 100) + ] + ) + + self.assertEqual( + mi.sort_together(iterables, key_list=(0, 1)), + [ + ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), + ('July', 'July', 'June', 'Aug.', 'May', 'May'), + (100, 20, 70, 20, 97, 100) + ] + ) + + self.assertEqual( + mi.sort_together(iterables, key_list=(0, 1, 2)), + [ + ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), + ('July', 'July', 'June', 'Aug.', 'May', 'May'), + (20, 100, 70, 20, 97, 100) + ] + ) + + self.assertEqual( + mi.sort_together(iterables, key_list=(2,)), + [ + ('GA', 'CT', 'CT', 'GA', 'GA', 'CT'), + ('Aug.', 'July', 'June', 'May', 'May', 'July'), + (20, 20, 70, 97, 100, 100) + ] + ) + + def test_invalid_key_list(self): + """tests `key_list` for indexes not available in `iterables`""" + iterables = [ + ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], + ['May', 'Aug.', 'May', 'June', 'July', 'July'], + [97, 20, 100, 70, 100, 20] + ] + + self.assertRaises( + IndexError, lambda: mi.sort_together(iterables, key_list=(5,)) + ) + + def test_reverse(self): + """tests `reverse` to ensure a reverse sort for `key_list` iterables""" + iterables = [ + ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], + ['May', 'Aug.', 'May', 'June', 'July', 'July'], + [97, 20, 100, 70, 100, 20] + ] + + self.assertEqual( + mi.sort_together(iterables, key_list=(0, 1, 2), reverse=True), + [('GA', 'GA', 'GA', 'CT', 'CT', 'CT'), + ('May', 'May', 'Aug.', 'June', 'July', 'July'), + (100, 97, 20, 70, 100, 20)] + ) + + def test_uneven_iterables(self): + """tests trimming of iterables to the shortest length before sorting""" + iterables = [['GA', 'GA', 'GA', 'CT', 'CT', 'CT', 'MA'], + ['May', 'Aug.', 'May', 'June', 'July', 'July'], + [97, 20, 100, 70, 100, 20, 0]] + + self.assertEqual( + mi.sort_together(iterables), + [ + ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), + ('June', 'July', 'July', 'May', 'Aug.', 'May'), + (70, 100, 20, 97, 20, 100) + ] + ) + + +class DivideTest(TestCase): + """Tests for divide()""" + + def test_invalid_n(self): + self.assertRaises(ValueError, lambda: mi.divide(-1, [1, 2, 3])) + self.assertRaises(ValueError, lambda: mi.divide(0, [1, 2, 3])) + + def test_basic(self): + iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + for n, expected in [ + (1, [iterable]), + (2, [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]), + (3, [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]), + (10, [[n] for n in range(1, 10 + 1)]), + ]: + self.assertEqual( + [list(x) for x in mi.divide(n, iterable)], expected + ) + + def test_large_n(self): + iterable = [1, 2, 3, 4] + self.assertEqual( + [list(x) for x in mi.divide(6, iterable)], + [[1], [2], [3], [4], [], []] + ) + + +class TestAlwaysIterable(TestCase): + """Tests for always_iterable()""" + def test_single(self): + self.assertEqual(list(mi.always_iterable(1)), [1]) + + def test_strings(self): + for obj in ['foo', b'bar', u'baz']: + actual = list(mi.always_iterable(obj)) + expected = [obj] + self.assertEqual(actual, expected) + + def test_base_type(self): + dict_obj = {'a': 1, 'b': 2} + str_obj = '123' + + # Default: dicts are iterable like they normally are + default_actual = list(mi.always_iterable(dict_obj)) + default_expected = list(dict_obj) + self.assertEqual(default_actual, default_expected) + + # Unitary types set: dicts are not iterable + custom_actual = list(mi.always_iterable(dict_obj, base_type=dict)) + custom_expected = [dict_obj] + self.assertEqual(custom_actual, custom_expected) + + # With unitary types set, strings are iterable + str_actual = list(mi.always_iterable(str_obj, base_type=None)) + str_expected = list(str_obj) + self.assertEqual(str_actual, str_expected) + + def test_iterables(self): + self.assertEqual(list(mi.always_iterable([0, 1])), [0, 1]) + self.assertEqual( + list(mi.always_iterable([0, 1], base_type=list)), [[0, 1]] + ) + self.assertEqual( + list(mi.always_iterable(iter('foo'))), ['f', 'o', 'o'] + ) + self.assertEqual(list(mi.always_iterable([])), []) + + def test_none(self): + self.assertEqual(list(mi.always_iterable(None)), []) + + def test_generator(self): + def _gen(): + yield 0 + yield 1 + + self.assertEqual(list(mi.always_iterable(_gen())), [0, 1]) + + +class AdjacentTests(TestCase): + def test_typical(self): + actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10))) + expected = [(True, 0), (True, 1), (False, 2), (False, 3), (True, 4), + (True, 5), (True, 6), (False, 7), (False, 8), (False, 9)] + self.assertEqual(actual, expected) + + def test_empty_iterable(self): + actual = list(mi.adjacent(lambda x: x % 5 == 0, [])) + expected = [] + self.assertEqual(actual, expected) + + def test_length_one(self): + actual = list(mi.adjacent(lambda x: x % 5 == 0, [0])) + expected = [(True, 0)] + self.assertEqual(actual, expected) + + actual = list(mi.adjacent(lambda x: x % 5 == 0, [1])) + expected = [(False, 1)] + self.assertEqual(actual, expected) + + def test_consecutive_true(self): + """Test that when the predicate matches multiple consecutive elements + it doesn't repeat elements in the output""" + actual = list(mi.adjacent(lambda x: x % 5 < 2, range(10))) + expected = [(True, 0), (True, 1), (True, 2), (False, 3), (True, 4), + (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)] + self.assertEqual(actual, expected) + + def test_distance(self): + actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=2)) + expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4), + (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)] + self.assertEqual(actual, expected) + + actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=3)) + expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4), + (True, 5), (True, 6), (True, 7), (True, 8), (False, 9)] + self.assertEqual(actual, expected) + + def test_large_distance(self): + """Test distance larger than the length of the iterable""" + iterable = range(10) + actual = list(mi.adjacent(lambda x: x % 5 == 4, iterable, distance=20)) + expected = list(zip(repeat(True), iterable)) + self.assertEqual(actual, expected) + + actual = list(mi.adjacent(lambda x: False, iterable, distance=20)) + expected = list(zip(repeat(False), iterable)) + self.assertEqual(actual, expected) + + def test_zero_distance(self): + """Test that adjacent() reduces to zip+map when distance is 0""" + iterable = range(1000) + predicate = lambda x: x % 4 == 2 + actual = mi.adjacent(predicate, iterable, 0) + expected = zip(map(predicate, iterable), iterable) + self.assertTrue(all(a == e for a, e in zip(actual, expected))) + + def test_negative_distance(self): + """Test that adjacent() raises an error with negative distance""" + pred = lambda x: x + self.assertRaises( + ValueError, lambda: mi.adjacent(pred, range(1000), -1) + ) + self.assertRaises( + ValueError, lambda: mi.adjacent(pred, range(10), -10) + ) + + def test_grouping(self): + """Test interaction of adjacent() with groupby_transform()""" + iterable = mi.adjacent(lambda x: x % 5 == 0, range(10)) + grouper = mi.groupby_transform(iterable, itemgetter(0), itemgetter(1)) + actual = [(k, list(g)) for k, g in grouper] + expected = [ + (True, [0, 1]), + (False, [2, 3]), + (True, [4, 5, 6]), + (False, [7, 8, 9]), + ] + self.assertEqual(actual, expected) + + def test_call_once(self): + """Test that the predicate is only called once per item.""" + already_seen = set() + iterable = range(10) + + def predicate(item): + self.assertNotIn(item, already_seen) + already_seen.add(item) + return True + + actual = list(mi.adjacent(predicate, iterable)) + expected = [(True, x) for x in iterable] + self.assertEqual(actual, expected) + + +class GroupByTransformTests(TestCase): + def assertAllGroupsEqual(self, groupby1, groupby2): + """Compare two groupby objects for equality, both keys and groups.""" + for a, b in zip(groupby1, groupby2): + key1, group1 = a + key2, group2 = b + self.assertEqual(key1, key2) + self.assertListEqual(list(group1), list(group2)) + self.assertRaises(StopIteration, lambda: next(groupby1)) + self.assertRaises(StopIteration, lambda: next(groupby2)) + + def test_default_funcs(self): + """Test that groupby_transform() with default args mimics groupby()""" + iterable = [(x // 5, x) for x in range(1000)] + actual = mi.groupby_transform(iterable) + expected = groupby(iterable) + self.assertAllGroupsEqual(actual, expected) + + def test_valuefunc(self): + iterable = [(int(x / 5), int(x / 3), x) for x in range(10)] + + # Test the standard usage of grouping one iterable using another's keys + grouper = mi.groupby_transform( + iterable, keyfunc=itemgetter(0), valuefunc=itemgetter(-1) + ) + actual = [(k, list(g)) for k, g in grouper] + expected = [(0, [0, 1, 2, 3, 4]), (1, [5, 6, 7, 8, 9])] + self.assertEqual(actual, expected) + + grouper = mi.groupby_transform( + iterable, keyfunc=itemgetter(1), valuefunc=itemgetter(-1) + ) + actual = [(k, list(g)) for k, g in grouper] + expected = [(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])] + self.assertEqual(actual, expected) + + # and now for something a little different + d = dict(zip(range(10), 'abcdefghij')) + grouper = mi.groupby_transform( + range(10), keyfunc=lambda x: x // 5, valuefunc=d.get + ) + actual = [(k, ''.join(g)) for k, g in grouper] + expected = [(0, 'abcde'), (1, 'fghij')] + self.assertEqual(actual, expected) + + def test_no_valuefunc(self): + iterable = range(1000) + + def key(x): + return x // 5 + + actual = mi.groupby_transform(iterable, key, valuefunc=None) + expected = groupby(iterable, key) + self.assertAllGroupsEqual(actual, expected) + + actual = mi.groupby_transform(iterable, key) # default valuefunc + expected = groupby(iterable, key) + self.assertAllGroupsEqual(actual, expected) + + +class NumericRangeTests(TestCase): + def test_basic(self): + for args, expected in [ + ((4,), [0, 1, 2, 3]), + ((4.0,), [0.0, 1.0, 2.0, 3.0]), + ((1.0, 4), [1.0, 2.0, 3.0]), + ((1, 4.0), [1, 2, 3]), + ((1.0, 5), [1.0, 2.0, 3.0, 4.0]), + ((0, 20, 5), [0, 5, 10, 15]), + ((0, 20, 5.0), [0.0, 5.0, 10.0, 15.0]), + ((0, 10, 3), [0, 3, 6, 9]), + ((0, 10, 3.0), [0.0, 3.0, 6.0, 9.0]), + ((0, -5, -1), [0, -1, -2, -3, -4]), + ((0.0, -5, -1), [0.0, -1.0, -2.0, -3.0, -4.0]), + ((1, 2, Fraction(1, 2)), [Fraction(1, 1), Fraction(3, 2)]), + ((0,), []), + ((0.0,), []), + ((1, 0), []), + ((1.0, 0.0), []), + ((Fraction(2, 1),), [Fraction(0, 1), Fraction(1, 1)]), + ((Decimal('2.0'),), [Decimal('0.0'), Decimal('1.0')]), + ]: + actual = list(mi.numeric_range(*args)) + self.assertEqual(actual, expected) + self.assertTrue( + all(type(a) == type(e) for a, e in zip(actual, expected)) + ) + + def test_arg_count(self): + self.assertRaises(TypeError, lambda: list(mi.numeric_range())) + self.assertRaises( + TypeError, lambda: list(mi.numeric_range(0, 1, 2, 3)) + ) + + def test_zero_step(self): + self.assertRaises( + ValueError, lambda: list(mi.numeric_range(1, 2, 0)) + ) + + +class CountCycleTests(TestCase): + def test_basic(self): + expected = [ + (0, 'a'), (0, 'b'), (0, 'c'), + (1, 'a'), (1, 'b'), (1, 'c'), + (2, 'a'), (2, 'b'), (2, 'c'), + ] + for actual in [ + mi.take(9, mi.count_cycle('abc')), # n=None + list(mi.count_cycle('abc', 3)), # n=3 + ]: + self.assertEqual(actual, expected) + + def test_empty(self): + self.assertEqual(list(mi.count_cycle('')), []) + self.assertEqual(list(mi.count_cycle('', 2)), []) + + def test_negative(self): + self.assertEqual(list(mi.count_cycle('abc', -3)), []) + + +class LocateTests(TestCase): + def test_default_pred(self): + iterable = [0, 1, 1, 0, 1, 0, 0] + actual = list(mi.locate(iterable)) + expected = [1, 2, 4] + self.assertEqual(actual, expected) + + def test_no_matches(self): + iterable = [0, 0, 0] + actual = list(mi.locate(iterable)) + expected = [] + self.assertEqual(actual, expected) + + def test_custom_pred(self): + iterable = ['0', 1, 1, '0', 1, '0', '0'] + pred = lambda x: x == '0' + actual = list(mi.locate(iterable, pred)) + expected = [0, 3, 5, 6] + self.assertEqual(actual, expected) + + def test_window_size(self): + iterable = ['0', 1, 1, '0', 1, '0', '0'] + pred = lambda *args: args == ('0', 1) + actual = list(mi.locate(iterable, pred, window_size=2)) + expected = [0, 3] + self.assertEqual(actual, expected) + + def test_window_size_large(self): + iterable = [1, 2, 3, 4] + pred = lambda a, b, c, d, e: True + actual = list(mi.locate(iterable, pred, window_size=5)) + expected = [0] + self.assertEqual(actual, expected) + + def test_window_size_zero(self): + iterable = [1, 2, 3, 4] + pred = lambda: True + with self.assertRaises(ValueError): + list(mi.locate(iterable, pred, window_size=0)) + + +class StripFunctionTests(TestCase): + def test_hashable(self): + iterable = list('www.example.com') + pred = lambda x: x in set('cmowz.') + + self.assertEqual(list(mi.lstrip(iterable, pred)), list('example.com')) + self.assertEqual(list(mi.rstrip(iterable, pred)), list('www.example')) + self.assertEqual(list(mi.strip(iterable, pred)), list('example')) + + def test_not_hashable(self): + iterable = [ + list('http://'), list('www'), list('.example'), list('.com') + ] + pred = lambda x: x in [list('http://'), list('www'), list('.com')] + + self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[2:]) + self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:3]) + self.assertEqual(list(mi.strip(iterable, pred)), iterable[2: 3]) + + def test_math(self): + iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2] + pred = lambda x: x <= 2 + + self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[3:]) + self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:-3]) + self.assertEqual(list(mi.strip(iterable, pred)), iterable[3:-3]) + + +class IsliceExtendedTests(TestCase): + def test_all(self): + iterable = ['0', '1', '2', '3', '4', '5'] + indexes = list(range(-4, len(iterable) + 4)) + [None] + steps = [1, 2, 3, 4, -1, -2, -3, 4] + for slice_args in product(indexes, indexes, steps): + try: + actual = list(mi.islice_extended(iterable, *slice_args)) + except Exception as e: + self.fail((slice_args, e)) + + expected = iterable[slice(*slice_args)] + self.assertEqual(actual, expected, slice_args) + + def test_zero_step(self): + with self.assertRaises(ValueError): + list(mi.islice_extended([1, 2, 3], 0, 1, 0)) + + +class ConsecutiveGroupsTest(TestCase): + def test_numbers(self): + iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7] + actual = [list(g) for g in mi.consecutive_groups(iterable)] + expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]] + self.assertEqual(actual, expected) + + def test_custom_ordering(self): + iterable = ['1', '10', '11', '20', '21', '22', '30', '31'] + ordering = lambda x: int(x) + actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] + expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']] + self.assertEqual(actual, expected) + + def test_exotic_ordering(self): + iterable = [ + ('a', 'b', 'c', 'd'), + ('a', 'c', 'b', 'd'), + ('a', 'c', 'd', 'b'), + ('a', 'd', 'b', 'c'), + ('d', 'b', 'c', 'a'), + ('d', 'c', 'a', 'b'), + ] + ordering = list(permutations('abcd')).index + actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] + expected = [ + [('a', 'b', 'c', 'd')], + [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')], + [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')], + ] + self.assertEqual(actual, expected) + + +class DifferenceTest(TestCase): + def test_normal(self): + iterable = [10, 20, 30, 40, 50] + actual = list(mi.difference(iterable)) + expected = [10, 10, 10, 10, 10] + self.assertEqual(actual, expected) + + def test_custom(self): + iterable = [10, 20, 30, 40, 50] + actual = list(mi.difference(iterable, add)) + expected = [10, 30, 50, 70, 90] + self.assertEqual(actual, expected) + + def test_roundtrip(self): + original = list(range(100)) + accumulated = mi.accumulate(original) + actual = list(mi.difference(accumulated)) + self.assertEqual(actual, original) + + def test_one(self): + self.assertEqual(list(mi.difference([0])), [0]) + + def test_empty(self): + self.assertEqual(list(mi.difference([])), []) + + +class SeekableTest(TestCase): + def test_exhaustion_reset(self): + iterable = [str(n) for n in range(10)] + + s = mi.seekable(iterable) + self.assertEqual(list(s), iterable) # Normal iteration + self.assertEqual(list(s), []) # Iterable is exhausted + + s.seek(0) + self.assertEqual(list(s), iterable) # Back in action + + def test_partial_reset(self): + iterable = [str(n) for n in range(10)] + + s = mi.seekable(iterable) + self.assertEqual(mi.take(5, s), iterable[:5]) # Normal iteration + + s.seek(1) + self.assertEqual(list(s), iterable[1:]) # Get the rest of the iterable + + def test_forward(self): + iterable = [str(n) for n in range(10)] + + s = mi.seekable(iterable) + self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration + + s.seek(3) # Skip over index 2 + self.assertEqual(list(s), iterable[3:]) # Result is similar to slicing + + s.seek(0) # Back to 0 + self.assertEqual(list(s), iterable) # No difference in result + + def test_past_end(self): + iterable = [str(n) for n in range(10)] + + s = mi.seekable(iterable) + self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration + + s.seek(20) + self.assertEqual(list(s), []) # Iterable is exhausted + + s.seek(0) # Back to 0 + self.assertEqual(list(s), iterable) # No difference in result + + def test_elements(self): + iterable = map(str, count()) + + s = mi.seekable(iterable) + mi.take(10, s) + + elements = s.elements() + self.assertEqual( + [elements[i] for i in range(10)], [str(n) for n in range(10)] + ) + self.assertEqual(len(elements), 10) + + mi.take(10, s) + self.assertEqual(list(elements), [str(n) for n in range(20)]) + + +class SequenceViewTests(TestCase): + def test_init(self): + view = mi.SequenceView((1, 2, 3)) + self.assertEqual(repr(view), "SequenceView((1, 2, 3))") + self.assertRaises(TypeError, lambda: mi.SequenceView({})) + + def test_update(self): + seq = [1, 2, 3] + view = mi.SequenceView(seq) + self.assertEqual(len(view), 3) + self.assertEqual(repr(view), "SequenceView([1, 2, 3])") + + seq.pop() + self.assertEqual(len(view), 2) + self.assertEqual(repr(view), "SequenceView([1, 2])") + + def test_indexing(self): + seq = ('a', 'b', 'c', 'd', 'e', 'f') + view = mi.SequenceView(seq) + for i in range(-len(seq), len(seq)): + self.assertEqual(view[i], seq[i]) + + def test_slicing(self): + seq = ('a', 'b', 'c', 'd', 'e', 'f') + view = mi.SequenceView(seq) + n = len(seq) + indexes = list(range(-n - 1, n + 1)) + [None] + steps = list(range(-n, n + 1)) + steps.remove(0) + for slice_args in product(indexes, indexes, steps): + i = slice(*slice_args) + self.assertEqual(view[i], seq[i]) + + def test_abc_methods(self): + # collections.Sequence should provide all of this functionality + seq = ('a', 'b', 'c', 'd', 'e', 'f', 'f') + view = mi.SequenceView(seq) + + # __contains__ + self.assertIn('b', view) + self.assertNotIn('g', view) + + # __iter__ + self.assertEqual(list(iter(view)), list(seq)) + + # __reversed__ + self.assertEqual(list(reversed(view)), list(reversed(seq))) + + # index + self.assertEqual(view.index('b'), 1) + + # count + self.assertEqual(seq.count('f'), 2) + + +class RunLengthTest(TestCase): + def test_encode(self): + iterable = (int(str(n)[0]) for n in count(800)) + actual = mi.take(4, mi.run_length.encode(iterable)) + expected = [(8, 100), (9, 100), (1, 1000), (2, 1000)] + self.assertEqual(actual, expected) + + def test_decode(self): + iterable = [('d', 4), ('c', 3), ('b', 2), ('a', 1)] + actual = ''.join(mi.run_length.decode(iterable)) + expected = 'ddddcccbba' + self.assertEqual(actual, expected) + + +class ExactlyNTests(TestCase): + """Tests for ``exactly_n()``""" + + def test_true(self): + """Iterable has ``n`` ``True`` elements""" + self.assertTrue(mi.exactly_n([True, False, True], 2)) + self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3)) + self.assertTrue(mi.exactly_n([False, False], 0)) + self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10)) + + def test_false(self): + """Iterable does not have ``n`` ``True`` elements""" + self.assertFalse(mi.exactly_n([True, False, False], 2)) + self.assertFalse(mi.exactly_n([True, True, False], 1)) + self.assertFalse(mi.exactly_n([False], 1)) + self.assertFalse(mi.exactly_n([True], -1)) + self.assertFalse(mi.exactly_n(repeat(True), 100)) + + def test_empty(self): + """Return ``True`` if the iterable is empty and ``n`` is 0""" + self.assertTrue(mi.exactly_n([], 0)) + self.assertFalse(mi.exactly_n([], 1)) + + +class AlwaysReversibleTests(TestCase): + """Tests for ``always_reversible()``""" + + def test_regular_reversed(self): + self.assertEqual(list(reversed(range(10))), + list(mi.always_reversible(range(10)))) + self.assertEqual(list(reversed([1, 2, 3])), + list(mi.always_reversible([1, 2, 3]))) + self.assertEqual(reversed([1, 2, 3]).__class__, + mi.always_reversible([1, 2, 3]).__class__) + + def test_nonseq_reversed(self): + # Create a non-reversible generator from a sequence + with self.assertRaises(TypeError): + reversed(x for x in range(10)) + + self.assertEqual(list(reversed(range(10))), + list(mi.always_reversible(x for x in range(10)))) + self.assertEqual(list(reversed([1, 2, 3])), + list(mi.always_reversible(x for x in [1, 2, 3]))) + self.assertNotEqual(reversed((1, 2)).__class__, + mi.always_reversible(x for x in (1, 2)).__class__) + + +class CircularShiftsTests(TestCase): + def test_empty(self): + # empty iterable -> empty list + self.assertEqual(list(mi.circular_shifts([])), []) + + def test_simple_circular_shifts(self): + # test the a simple iterator case + self.assertEqual( + mi.circular_shifts(range(4)), + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + ) + + def test_duplicates(self): + # test non-distinct entries + self.assertEqual( + mi.circular_shifts([0, 1, 0, 1]), + [(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)] + ) + + +class MakeDecoratorTests(TestCase): + def test_basic(self): + slicer = mi.make_decorator(islice) + + @slicer(1, 10, 2) + def user_function(arg_1, arg_2, kwarg_1=None): + self.assertEqual(arg_1, 'arg_1') + self.assertEqual(arg_2, 'arg_2') + self.assertEqual(kwarg_1, 'kwarg_1') + return map(str, count()) + + it = user_function('arg_1', 'arg_2', kwarg_1='kwarg_1') + actual = list(it) + expected = ['1', '3', '5', '7', '9'] + self.assertEqual(actual, expected) + + def test_result_index(self): + def stringify(*args, **kwargs): + self.assertEqual(args[0], 'arg_0') + iterable = args[1] + self.assertEqual(args[2], 'arg_2') + self.assertEqual(kwargs['kwarg_1'], 'kwarg_1') + return map(str, iterable) + + stringifier = mi.make_decorator(stringify, result_index=1) + + @stringifier('arg_0', 'arg_2', kwarg_1='kwarg_1') + def user_function(n): + return count(n) + + it = user_function(1) + actual = mi.take(5, it) + expected = ['1', '2', '3', '4', '5'] + self.assertEqual(actual, expected) + + def test_wrap_class(self): + seeker = mi.make_decorator(mi.seekable) + + @seeker() + def user_function(n): + return map(str, range(n)) + + it = user_function(5) + self.assertEqual(list(it), ['0', '1', '2', '3', '4']) + + it.seek(0) + self.assertEqual(list(it), ['0', '1', '2', '3', '4']) + + +class MapReduceTests(TestCase): + def test_default(self): + iterable = (str(x) for x in range(5)) + keyfunc = lambda x: int(x) // 2 + actual = sorted(mi.map_reduce(iterable, keyfunc).items()) + expected = [(0, ['0', '1']), (1, ['2', '3']), (2, ['4'])] + self.assertEqual(actual, expected) + + def test_valuefunc(self): + iterable = (str(x) for x in range(5)) + keyfunc = lambda x: int(x) // 2 + valuefunc = int + actual = sorted(mi.map_reduce(iterable, keyfunc, valuefunc).items()) + expected = [(0, [0, 1]), (1, [2, 3]), (2, [4])] + self.assertEqual(actual, expected) + + def test_reducefunc(self): + iterable = (str(x) for x in range(5)) + keyfunc = lambda x: int(x) // 2 + valuefunc = int + reducefunc = lambda value_list: reduce(mul, value_list, 1) + actual = sorted( + mi.map_reduce(iterable, keyfunc, valuefunc, reducefunc).items() + ) + expected = [(0, 0), (1, 6), (2, 4)] + self.assertEqual(actual, expected) + + def test_ret(self): + d = mi.map_reduce([1, 0, 2, 0, 1, 0], bool) + self.assertEqual(d, {False: [0, 0, 0], True: [1, 2, 1]}) + self.assertRaises(KeyError, lambda: d[None].append(1)) + + +class RlocateTests(TestCase): + def test_default_pred(self): + iterable = [0, 1, 1, 0, 1, 0, 0] + for it in (iterable[:], iter(iterable)): + actual = list(mi.rlocate(it)) + expected = [4, 2, 1] + self.assertEqual(actual, expected) + + def test_no_matches(self): + iterable = [0, 0, 0] + for it in (iterable[:], iter(iterable)): + actual = list(mi.rlocate(it)) + expected = [] + self.assertEqual(actual, expected) + + def test_custom_pred(self): + iterable = ['0', 1, 1, '0', 1, '0', '0'] + pred = lambda x: x == '0' + for it in (iterable[:], iter(iterable)): + actual = list(mi.rlocate(it, pred)) + expected = [6, 5, 3, 0] + self.assertEqual(actual, expected) + + def test_efficient_reversal(self): + iterable = range(10 ** 10) # Is efficiently reversible + target = 10 ** 10 - 2 + pred = lambda x: x == target # Find-able from the right + actual = next(mi.rlocate(iterable, pred)) + self.assertEqual(actual, target) + + def test_window_size(self): + iterable = ['0', 1, 1, '0', 1, '0', '0'] + pred = lambda *args: args == ('0', 1) + for it in (iterable, iter(iterable)): + actual = list(mi.rlocate(it, pred, window_size=2)) + expected = [3, 0] + self.assertEqual(actual, expected) + + def test_window_size_large(self): + iterable = [1, 2, 3, 4] + pred = lambda a, b, c, d, e: True + for it in (iterable, iter(iterable)): + actual = list(mi.rlocate(iterable, pred, window_size=5)) + expected = [0] + self.assertEqual(actual, expected) + + def test_window_size_zero(self): + iterable = [1, 2, 3, 4] + pred = lambda: True + for it in (iterable, iter(iterable)): + with self.assertRaises(ValueError): + list(mi.locate(iterable, pred, window_size=0)) + + +class ReplaceTests(TestCase): + def test_basic(self): + iterable = range(10) + pred = lambda x: x % 2 == 0 + substitutes = [] + actual = list(mi.replace(iterable, pred, substitutes)) + expected = [1, 3, 5, 7, 9] + self.assertEqual(actual, expected) + + def test_count(self): + iterable = range(10) + pred = lambda x: x % 2 == 0 + substitutes = [] + actual = list(mi.replace(iterable, pred, substitutes, count=4)) + expected = [1, 3, 5, 7, 8, 9] + self.assertEqual(actual, expected) + + def test_window_size(self): + iterable = range(10) + pred = lambda *args: args == (0, 1, 2) + substitutes = [] + actual = list(mi.replace(iterable, pred, substitutes, window_size=3)) + expected = [3, 4, 5, 6, 7, 8, 9] + self.assertEqual(actual, expected) + + def test_window_size_end(self): + iterable = range(10) + pred = lambda *args: args == (7, 8, 9) + substitutes = [] + actual = list(mi.replace(iterable, pred, substitutes, window_size=3)) + expected = [0, 1, 2, 3, 4, 5, 6] + self.assertEqual(actual, expected) + + def test_window_size_count(self): + iterable = range(10) + pred = lambda *args: (args == (0, 1, 2)) or (args == (7, 8, 9)) + substitutes = [] + actual = list( + mi.replace(iterable, pred, substitutes, count=1, window_size=3) + ) + expected = [3, 4, 5, 6, 7, 8, 9] + self.assertEqual(actual, expected) + + def test_window_size_large(self): + iterable = range(4) + pred = lambda a, b, c, d, e: True + substitutes = [5, 6, 7] + actual = list(mi.replace(iterable, pred, substitutes, window_size=5)) + expected = [5, 6, 7] + self.assertEqual(actual, expected) + + def test_window_size_zero(self): + iterable = range(10) + pred = lambda *args: True + substitutes = [] + with self.assertRaises(ValueError): + list(mi.replace(iterable, pred, substitutes, window_size=0)) + + def test_iterable_substitutes(self): + iterable = range(5) + pred = lambda x: x % 2 == 0 + substitutes = iter('__') + actual = list(mi.replace(iterable, pred, substitutes)) + expected = ['_', '_', 1, '_', '_', 3, '_', '_'] + self.assertEqual(actual, expected) diff --git a/libraries/more_itertools/tests/test_recipes.py b/libraries/more_itertools/tests/test_recipes.py new file mode 100644 index 00000000..98981fe8 --- /dev/null +++ b/libraries/more_itertools/tests/test_recipes.py @@ -0,0 +1,616 @@ +from doctest import DocTestSuite +from unittest import TestCase + +from itertools import combinations +from six.moves import range + +import more_itertools as mi + + +def load_tests(loader, tests, ignore): + # Add the doctests + tests.addTests(DocTestSuite('more_itertools.recipes')) + return tests + + +class AccumulateTests(TestCase): + """Tests for ``accumulate()``""" + + def test_empty(self): + """Test that an empty input returns an empty output""" + self.assertEqual(list(mi.accumulate([])), []) + + def test_default(self): + """Test accumulate with the default function (addition)""" + self.assertEqual(list(mi.accumulate([1, 2, 3])), [1, 3, 6]) + + def test_bogus_function(self): + """Test accumulate with an invalid function""" + with self.assertRaises(TypeError): + list(mi.accumulate([1, 2, 3], func=lambda x: x)) + + def test_custom_function(self): + """Test accumulate with a custom function""" + self.assertEqual( + list(mi.accumulate((1, 2, 3, 2, 1), func=max)), [1, 2, 3, 3, 3] + ) + + +class TakeTests(TestCase): + """Tests for ``take()``""" + + def test_simple_take(self): + """Test basic usage""" + t = mi.take(5, range(10)) + self.assertEqual(t, [0, 1, 2, 3, 4]) + + def test_null_take(self): + """Check the null case""" + t = mi.take(0, range(10)) + self.assertEqual(t, []) + + def test_negative_take(self): + """Make sure taking negative items results in a ValueError""" + self.assertRaises(ValueError, lambda: mi.take(-3, range(10))) + + def test_take_too_much(self): + """Taking more than an iterator has remaining should return what the + iterator has remaining. + + """ + t = mi.take(10, range(5)) + self.assertEqual(t, [0, 1, 2, 3, 4]) + + +class TabulateTests(TestCase): + """Tests for ``tabulate()``""" + + def test_simple_tabulate(self): + """Test the happy path""" + t = mi.tabulate(lambda x: x) + f = tuple([next(t) for _ in range(3)]) + self.assertEqual(f, (0, 1, 2)) + + def test_count(self): + """Ensure tabulate accepts specific count""" + t = mi.tabulate(lambda x: 2 * x, -1) + f = (next(t), next(t), next(t)) + self.assertEqual(f, (-2, 0, 2)) + + +class TailTests(TestCase): + """Tests for ``tail()``""" + + def test_greater(self): + """Length of iterable is greather than requested tail""" + self.assertEqual(list(mi.tail(3, 'ABCDEFG')), ['E', 'F', 'G']) + + def test_equal(self): + """Length of iterable is equal to the requested tail""" + self.assertEqual( + list(mi.tail(7, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G'] + ) + + def test_less(self): + """Length of iterable is less than requested tail""" + self.assertEqual( + list(mi.tail(8, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G'] + ) + + +class ConsumeTests(TestCase): + """Tests for ``consume()``""" + + def test_sanity(self): + """Test basic functionality""" + r = (x for x in range(10)) + mi.consume(r, 3) + self.assertEqual(3, next(r)) + + def test_null_consume(self): + """Check the null case""" + r = (x for x in range(10)) + mi.consume(r, 0) + self.assertEqual(0, next(r)) + + def test_negative_consume(self): + """Check that negative consumsion throws an error""" + r = (x for x in range(10)) + self.assertRaises(ValueError, lambda: mi.consume(r, -1)) + + def test_total_consume(self): + """Check that iterator is totally consumed by default""" + r = (x for x in range(10)) + mi.consume(r) + self.assertRaises(StopIteration, lambda: next(r)) + + +class NthTests(TestCase): + """Tests for ``nth()``""" + + def test_basic(self): + """Make sure the nth item is returned""" + l = range(10) + for i, v in enumerate(l): + self.assertEqual(mi.nth(l, i), v) + + def test_default(self): + """Ensure a default value is returned when nth item not found""" + l = range(3) + self.assertEqual(mi.nth(l, 100, "zebra"), "zebra") + + def test_negative_item_raises(self): + """Ensure asking for a negative item raises an exception""" + self.assertRaises(ValueError, lambda: mi.nth(range(10), -3)) + + +class AllEqualTests(TestCase): + """Tests for ``all_equal()``""" + + def test_true(self): + """Everything is equal""" + self.assertTrue(mi.all_equal('aaaaaa')) + self.assertTrue(mi.all_equal([0, 0, 0, 0])) + + def test_false(self): + """Not everything is equal""" + self.assertFalse(mi.all_equal('aaaaab')) + self.assertFalse(mi.all_equal([0, 0, 0, 1])) + + def test_tricky(self): + """Not everything is identical, but everything is equal""" + items = [1, complex(1, 0), 1.0] + self.assertTrue(mi.all_equal(items)) + + def test_empty(self): + """Return True if the iterable is empty""" + self.assertTrue(mi.all_equal('')) + self.assertTrue(mi.all_equal([])) + + def test_one(self): + """Return True if the iterable is singular""" + self.assertTrue(mi.all_equal('0')) + self.assertTrue(mi.all_equal([0])) + + +class QuantifyTests(TestCase): + """Tests for ``quantify()``""" + + def test_happy_path(self): + """Make sure True count is returned""" + q = [True, False, True] + self.assertEqual(mi.quantify(q), 2) + + def test_custom_predicate(self): + """Ensure non-default predicates return as expected""" + q = range(10) + self.assertEqual(mi.quantify(q, lambda x: x % 2 == 0), 5) + + +class PadnoneTests(TestCase): + """Tests for ``padnone()``""" + + def test_happy_path(self): + """wrapper iterator should return None indefinitely""" + r = range(2) + p = mi.padnone(r) + self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)]) + + +class NcyclesTests(TestCase): + """Tests for ``nyclces()``""" + + def test_happy_path(self): + """cycle a sequence three times""" + r = ["a", "b", "c"] + n = mi.ncycles(r, 3) + self.assertEqual( + ["a", "b", "c", "a", "b", "c", "a", "b", "c"], + list(n) + ) + + def test_null_case(self): + """asking for 0 cycles should return an empty iterator""" + n = mi.ncycles(range(100), 0) + self.assertRaises(StopIteration, lambda: next(n)) + + def test_pathalogical_case(self): + """asking for negative cycles should return an empty iterator""" + n = mi.ncycles(range(100), -10) + self.assertRaises(StopIteration, lambda: next(n)) + + +class DotproductTests(TestCase): + """Tests for ``dotproduct()``'""" + + def test_happy_path(self): + """simple dotproduct example""" + self.assertEqual(400, mi.dotproduct([10, 10], [20, 20])) + + +class FlattenTests(TestCase): + """Tests for ``flatten()``""" + + def test_basic_usage(self): + """ensure list of lists is flattened one level""" + f = [[0, 1, 2], [3, 4, 5]] + self.assertEqual(list(range(6)), list(mi.flatten(f))) + + def test_single_level(self): + """ensure list of lists is flattened only one level""" + f = [[0, [1, 2]], [[3, 4], 5]] + self.assertEqual([0, [1, 2], [3, 4], 5], list(mi.flatten(f))) + + +class RepeatfuncTests(TestCase): + """Tests for ``repeatfunc()``""" + + def test_simple_repeat(self): + """test simple repeated functions""" + r = mi.repeatfunc(lambda: 5) + self.assertEqual([5, 5, 5, 5, 5], [next(r) for _ in range(5)]) + + def test_finite_repeat(self): + """ensure limited repeat when times is provided""" + r = mi.repeatfunc(lambda: 5, times=5) + self.assertEqual([5, 5, 5, 5, 5], list(r)) + + def test_added_arguments(self): + """ensure arguments are applied to the function""" + r = mi.repeatfunc(lambda x: x, 2, 3) + self.assertEqual([3, 3], list(r)) + + def test_null_times(self): + """repeat 0 should return an empty iterator""" + r = mi.repeatfunc(range, 0, 3) + self.assertRaises(StopIteration, lambda: next(r)) + + +class PairwiseTests(TestCase): + """Tests for ``pairwise()``""" + + def test_base_case(self): + """ensure an iterable will return pairwise""" + p = mi.pairwise([1, 2, 3]) + self.assertEqual([(1, 2), (2, 3)], list(p)) + + def test_short_case(self): + """ensure an empty iterator if there's not enough values to pair""" + p = mi.pairwise("a") + self.assertRaises(StopIteration, lambda: next(p)) + + +class GrouperTests(TestCase): + """Tests for ``grouper()``""" + + def test_even(self): + """Test when group size divides evenly into the length of + the iterable. + + """ + self.assertEqual( + list(mi.grouper(3, 'ABCDEF')), [('A', 'B', 'C'), ('D', 'E', 'F')] + ) + + def test_odd(self): + """Test when group size does not divide evenly into the length of the + iterable. + + """ + self.assertEqual( + list(mi.grouper(3, 'ABCDE')), [('A', 'B', 'C'), ('D', 'E', None)] + ) + + def test_fill_value(self): + """Test that the fill value is used to pad the final group""" + self.assertEqual( + list(mi.grouper(3, 'ABCDE', 'x')), + [('A', 'B', 'C'), ('D', 'E', 'x')] + ) + + +class RoundrobinTests(TestCase): + """Tests for ``roundrobin()``""" + + def test_even_groups(self): + """Ensure ordered output from evenly populated iterables""" + self.assertEqual( + list(mi.roundrobin('ABC', [1, 2, 3], range(3))), + ['A', 1, 0, 'B', 2, 1, 'C', 3, 2] + ) + + def test_uneven_groups(self): + """Ensure ordered output from unevenly populated iterables""" + self.assertEqual( + list(mi.roundrobin('ABCD', [1, 2], range(0))), + ['A', 1, 'B', 2, 'C', 'D'] + ) + + +class PartitionTests(TestCase): + """Tests for ``partition()``""" + + def test_bool(self): + """Test when pred() returns a boolean""" + lesser, greater = mi.partition(lambda x: x > 5, range(10)) + self.assertEqual(list(lesser), [0, 1, 2, 3, 4, 5]) + self.assertEqual(list(greater), [6, 7, 8, 9]) + + def test_arbitrary(self): + """Test when pred() returns an integer""" + divisibles, remainders = mi.partition(lambda x: x % 3, range(10)) + self.assertEqual(list(divisibles), [0, 3, 6, 9]) + self.assertEqual(list(remainders), [1, 2, 4, 5, 7, 8]) + + +class PowersetTests(TestCase): + """Tests for ``powerset()``""" + + def test_combinatorics(self): + """Ensure a proper enumeration""" + p = mi.powerset([1, 2, 3]) + self.assertEqual( + list(p), + [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] + ) + + +class UniqueEverseenTests(TestCase): + """Tests for ``unique_everseen()``""" + + def test_everseen(self): + """ensure duplicate elements are ignored""" + u = mi.unique_everseen('AAAABBBBCCDAABBB') + self.assertEqual( + ['A', 'B', 'C', 'D'], + list(u) + ) + + def test_custom_key(self): + """ensure the custom key comparison works""" + u = mi.unique_everseen('aAbACCc', key=str.lower) + self.assertEqual(list('abC'), list(u)) + + def test_unhashable(self): + """ensure things work for unhashable items""" + iterable = ['a', [1, 2, 3], [1, 2, 3], 'a'] + u = mi.unique_everseen(iterable) + self.assertEqual(list(u), ['a', [1, 2, 3]]) + + def test_unhashable_key(self): + """ensure things work for unhashable items with a custom key""" + iterable = ['a', [1, 2, 3], [1, 2, 3], 'a'] + u = mi.unique_everseen(iterable, key=lambda x: x) + self.assertEqual(list(u), ['a', [1, 2, 3]]) + + +class UniqueJustseenTests(TestCase): + """Tests for ``unique_justseen()``""" + + def test_justseen(self): + """ensure only last item is remembered""" + u = mi.unique_justseen('AAAABBBCCDABB') + self.assertEqual(list('ABCDAB'), list(u)) + + def test_custom_key(self): + """ensure the custom key comparison works""" + u = mi.unique_justseen('AABCcAD', str.lower) + self.assertEqual(list('ABCAD'), list(u)) + + +class IterExceptTests(TestCase): + """Tests for ``iter_except()``""" + + def test_exact_exception(self): + """ensure the exact specified exception is caught""" + l = [1, 2, 3] + i = mi.iter_except(l.pop, IndexError) + self.assertEqual(list(i), [3, 2, 1]) + + def test_generic_exception(self): + """ensure the generic exception can be caught""" + l = [1, 2] + i = mi.iter_except(l.pop, Exception) + self.assertEqual(list(i), [2, 1]) + + def test_uncaught_exception_is_raised(self): + """ensure a non-specified exception is raised""" + l = [1, 2, 3] + i = mi.iter_except(l.pop, KeyError) + self.assertRaises(IndexError, lambda: list(i)) + + def test_first(self): + """ensure first is run before the function""" + l = [1, 2, 3] + f = lambda: 25 + i = mi.iter_except(l.pop, IndexError, f) + self.assertEqual(list(i), [25, 3, 2, 1]) + + +class FirstTrueTests(TestCase): + """Tests for ``first_true()``""" + + def test_something_true(self): + """Test with no keywords""" + self.assertEqual(mi.first_true(range(10)), 1) + + def test_nothing_true(self): + """Test default return value.""" + self.assertEqual(mi.first_true([0, 0, 0]), False) + + def test_default(self): + """Test with a default keyword""" + self.assertEqual(mi.first_true([0, 0, 0], default='!'), '!') + + def test_pred(self): + """Test with a custom predicate""" + self.assertEqual( + mi.first_true([2, 4, 6], pred=lambda x: x % 3 == 0), 6 + ) + + +class RandomProductTests(TestCase): + """Tests for ``random_product()`` + + Since random.choice() has different results with the same seed across + python versions 2.x and 3.x, these tests use highly probably events to + create predictable outcomes across platforms. + """ + + def test_simple_lists(self): + """Ensure that one item is chosen from each list in each pair. + Also ensure that each item from each list eventually appears in + the chosen combinations. + + Odds are roughly 1 in 7.1 * 10e16 that one item from either list will + not be chosen after 100 samplings of one item from each list. Just to + be safe, better use a known random seed, too. + + """ + nums = [1, 2, 3] + lets = ['a', 'b', 'c'] + n, m = zip(*[mi.random_product(nums, lets) for _ in range(100)]) + n, m = set(n), set(m) + self.assertEqual(n, set(nums)) + self.assertEqual(m, set(lets)) + self.assertEqual(len(n), len(nums)) + self.assertEqual(len(m), len(lets)) + + def test_list_with_repeat(self): + """ensure multiple items are chosen, and that they appear to be chosen + from one list then the next, in proper order. + + """ + nums = [1, 2, 3] + lets = ['a', 'b', 'c'] + r = list(mi.random_product(nums, lets, repeat=100)) + self.assertEqual(2 * 100, len(r)) + n, m = set(r[::2]), set(r[1::2]) + self.assertEqual(n, set(nums)) + self.assertEqual(m, set(lets)) + self.assertEqual(len(n), len(nums)) + self.assertEqual(len(m), len(lets)) + + +class RandomPermutationTests(TestCase): + """Tests for ``random_permutation()``""" + + def test_full_permutation(self): + """ensure every item from the iterable is returned in a new ordering + + 15 elements have a 1 in 1.3 * 10e12 of appearing in sorted order, so + we fix a seed value just to be sure. + + """ + i = range(15) + r = mi.random_permutation(i) + self.assertEqual(set(i), set(r)) + if i == r: + raise AssertionError("Values were not permuted") + + def test_partial_permutation(self): + """ensure all returned items are from the iterable, that the returned + permutation is of the desired length, and that all items eventually + get returned. + + Sampling 100 permutations of length 5 from a set of 15 leaves a + (2/3)^100 chance that an item will not be chosen. Multiplied by 15 + items, there is a 1 in 2.6e16 chance that at least 1 item will not + show up in the resulting output. Using a random seed will fix that. + + """ + items = range(15) + item_set = set(items) + all_items = set() + for _ in range(100): + permutation = mi.random_permutation(items, 5) + self.assertEqual(len(permutation), 5) + permutation_set = set(permutation) + self.assertLessEqual(permutation_set, item_set) + all_items |= permutation_set + self.assertEqual(all_items, item_set) + + +class RandomCombinationTests(TestCase): + """Tests for ``random_combination()``""" + + def test_psuedorandomness(self): + """ensure different subsets of the iterable get returned over many + samplings of random combinations""" + items = range(15) + all_items = set() + for _ in range(50): + combination = mi.random_combination(items, 5) + all_items |= set(combination) + self.assertEqual(all_items, set(items)) + + def test_no_replacement(self): + """ensure that elements are sampled without replacement""" + items = range(15) + for _ in range(50): + combination = mi.random_combination(items, len(items)) + self.assertEqual(len(combination), len(set(combination))) + self.assertRaises( + ValueError, lambda: mi.random_combination(items, len(items) + 1) + ) + + +class RandomCombinationWithReplacementTests(TestCase): + """Tests for ``random_combination_with_replacement()``""" + + def test_replacement(self): + """ensure that elements are sampled with replacement""" + items = range(5) + combo = mi.random_combination_with_replacement(items, len(items) * 2) + self.assertEqual(2 * len(items), len(combo)) + if len(set(combo)) == len(combo): + raise AssertionError("Combination contained no duplicates") + + def test_pseudorandomness(self): + """ensure different subsets of the iterable get returned over many + samplings of random combinations""" + items = range(15) + all_items = set() + for _ in range(50): + combination = mi.random_combination_with_replacement(items, 5) + all_items |= set(combination) + self.assertEqual(all_items, set(items)) + + +class NthCombinationTests(TestCase): + def test_basic(self): + iterable = 'abcdefg' + r = 4 + for index, expected in enumerate(combinations(iterable, r)): + actual = mi.nth_combination(iterable, r, index) + self.assertEqual(actual, expected) + + def test_long(self): + actual = mi.nth_combination(range(180), 4, 2000000) + expected = (2, 12, 35, 126) + self.assertEqual(actual, expected) + + def test_invalid_r(self): + for r in (-1, 3): + with self.assertRaises(ValueError): + mi.nth_combination([], r, 0) + + def test_invalid_index(self): + with self.assertRaises(IndexError): + mi.nth_combination('abcdefg', 3, -36) + + +class PrependTests(TestCase): + def test_basic(self): + value = 'a' + iterator = iter('bcdefg') + actual = list(mi.prepend(value, iterator)) + expected = list('abcdefg') + self.assertEqual(actual, expected) + + def test_multiple(self): + value = 'ab' + iterator = iter('cdefg') + actual = tuple(mi.prepend(value, iterator)) + expected = ('ab',) + tuple('cdefg') + self.assertEqual(actual, expected) diff --git a/libraries/portend.py b/libraries/portend.py new file mode 100644 index 00000000..4c393806 --- /dev/null +++ b/libraries/portend.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +""" +A simple library for managing the availability of ports. +""" + +from __future__ import print_function, division + +import time +import socket +import argparse +import sys +import itertools +import contextlib +import collections +import platform + +from tempora import timing + + +def client_host(server_host): + """Return the host on which a client can connect to the given listener.""" + if server_host == '0.0.0.0': + # 0.0.0.0 is INADDR_ANY, which should answer on localhost. + return '127.0.0.1' + if server_host in ('::', '::0', '::0.0.0.0'): + # :: is IN6ADDR_ANY, which should answer on localhost. + # ::0 and ::0.0.0.0 are non-canonical but common + # ways to write IN6ADDR_ANY. + return '::1' + return server_host + + +class Checker(object): + def __init__(self, timeout=1.0): + self.timeout = timeout + + def assert_free(self, host, port=None): + """ + Assert that the given addr is free + in that all attempts to connect fail within the timeout + or raise a PortNotFree exception. + + >>> free_port = find_available_local_port() + + >>> Checker().assert_free('localhost', free_port) + >>> Checker().assert_free('127.0.0.1', free_port) + >>> Checker().assert_free('::1', free_port) + + Also accepts an addr tuple + + >>> addr = '::1', free_port, 0, 0 + >>> Checker().assert_free(addr) + + Host might refer to a server bind address like '::', which + should use localhost to perform the check. + + >>> Checker().assert_free('::', free_port) + """ + if port is None and isinstance(host, collections.Sequence): + host, port = host[:2] + if platform.system() == 'Windows': + host = client_host(host) + info = socket.getaddrinfo( + host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, + ) + list(itertools.starmap(self._connect, info)) + + def _connect(self, af, socktype, proto, canonname, sa): + s = socket.socket(af, socktype, proto) + # fail fast with a small timeout + s.settimeout(self.timeout) + + with contextlib.closing(s): + try: + s.connect(sa) + except socket.error: + return + + # the connect succeeded, so the port isn't free + port, host = sa[:2] + tmpl = "Port {port} is in use on {host}." + raise PortNotFree(tmpl.format(**locals())) + + +class Timeout(IOError): + pass + + +class PortNotFree(IOError): + pass + + +def free(host, port, timeout=float('Inf')): + """ + Wait for the specified port to become free (dropping or rejecting + requests). Return when the port is free or raise a Timeout if timeout has + elapsed. + + Timeout may be specified in seconds or as a timedelta. + If timeout is None or ∞, the routine will run indefinitely. + + >>> free('localhost', find_available_local_port()) + """ + if not host: + raise ValueError("Host values of '' or None are not allowed.") + + timer = timing.Timer(timeout) + + while not timer.expired(): + try: + # Expect a free port, so use a small timeout + Checker(timeout=0.1).assert_free(host, port) + return + except PortNotFree: + # Politely wait. + time.sleep(0.1) + + raise Timeout("Port {port} not free on {host}.".format(**locals())) +wait_for_free_port = free + + +def occupied(host, port, timeout=float('Inf')): + """ + Wait for the specified port to become occupied (accepting requests). + Return when the port is occupied or raise a Timeout if timeout has + elapsed. + + Timeout may be specified in seconds or as a timedelta. + If timeout is None or ∞, the routine will run indefinitely. + + >>> occupied('localhost', find_available_local_port(), .1) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + Timeout: Port ... not bound on localhost. + """ + if not host: + raise ValueError("Host values of '' or None are not allowed.") + + timer = timing.Timer(timeout) + + while not timer.expired(): + try: + Checker(timeout=.5).assert_free(host, port) + # Politely wait + time.sleep(0.1) + except PortNotFree: + # port is occupied + return + + raise Timeout("Port {port} not bound on {host}.".format(**locals())) +wait_for_occupied_port = occupied + + +def find_available_local_port(): + """ + Find a free port on localhost. + + >>> 0 < find_available_local_port() < 65536 + True + """ + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + addr = '', 0 + sock.bind(addr) + addr, port = sock.getsockname()[:2] + sock.close() + return port + + +class HostPort(str): + """ + A simple representation of a host/port pair as a string + + >>> hp = HostPort('localhost:32768') + + >>> hp.host + 'localhost' + + >>> hp.port + 32768 + + >>> len(hp) + 15 + """ + + @property + def host(self): + host, sep, port = self.partition(':') + return host + + @property + def port(self): + host, sep, port = self.partition(':') + return int(port) + + +def _main(): + parser = argparse.ArgumentParser() + global_lookup = lambda key: globals()[key] + parser.add_argument('target', metavar='host:port', type=HostPort) + parser.add_argument('func', metavar='state', type=global_lookup) + parser.add_argument('-t', '--timeout', default=None, type=float) + args = parser.parse_args() + try: + args.func(args.target.host, args.target.port, timeout=args.timeout) + except Timeout as timeout: + print(timeout, file=sys.stderr) + raise SystemExit(1) + + +if __name__ == '__main__': + _main() diff --git a/libraries/tempora/__init__.py b/libraries/tempora/__init__.py new file mode 100644 index 00000000..e0cdead0 --- /dev/null +++ b/libraries/tempora/__init__.py @@ -0,0 +1,505 @@ +# -*- coding: UTF-8 -*- + +"Objects and routines pertaining to date and time (tempora)" + +from __future__ import division, unicode_literals + +import datetime +import time +import re +import numbers +import functools + +import six + +__metaclass__ = type + + +class Parser: + """ + Datetime parser: parses a date-time string using multiple possible + formats. + + >>> p = Parser(('%H%M', '%H:%M')) + >>> tuple(p.parse('1319')) + (1900, 1, 1, 13, 19, 0, 0, 1, -1) + >>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y')) + >>> tuple(dateParser.parse('2003-12-20')) + (2003, 12, 20, 0, 0, 0, 5, 354, -1) + >>> tuple(dateParser.parse('16-Dec-1994')) + (1994, 12, 16, 0, 0, 0, 4, 350, -1) + >>> tuple(dateParser.parse('5/19/2003')) + (2003, 5, 19, 0, 0, 0, 0, 139, -1) + >>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y')) + >>> tuple(dtParser.parse('2003-12-20 19:13:26')) + (2003, 12, 20, 19, 13, 26, 5, 354, -1) + >>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004')) + (2004, 1, 20, 16, 19, 33, 1, 20, -1) + + Be forewarned, a ValueError will be raised if more than one format + matches: + + >>> Parser(('%H%M', '%H%M%S')).parse('732') + Traceback (most recent call last): + ... + ValueError: More than one format string matched target 732. + """ + + formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y') + "some common default formats" + + def __init__(self, formats=None): + if formats: + self.formats = formats + + def parse(self, target): + self.target = target + results = tuple(filter(None, map(self._parse, self.formats))) + del self.target + if not results: + tmpl = "No format strings matched the target {target}." + raise ValueError(tmpl.format(**locals())) + if not len(results) == 1: + tmpl = "More than one format string matched target {target}." + raise ValueError(tmpl.format(**locals())) + return results[0] + + def _parse(self, format): + try: + result = time.strptime(self.target, format) + except ValueError: + result = False + return result + + +# some useful constants +osc_per_year = 290091329207984000 +""" +mean vernal equinox year expressed in oscillations of atomic cesium at the +year 2000 (see http://webexhibits.org/calendars/timeline.html for more info). +""" +osc_per_second = 9192631770 +seconds_per_second = 1 +seconds_per_year = 31556940 +seconds_per_minute = 60 +minutes_per_hour = 60 +hours_per_day = 24 +seconds_per_hour = seconds_per_minute * minutes_per_hour +seconds_per_day = seconds_per_hour * hours_per_day +days_per_year = seconds_per_year / seconds_per_day +thirty_days = datetime.timedelta(days=30) +# these values provide useful averages +six_months = datetime.timedelta(days=days_per_year / 2) +seconds_per_month = seconds_per_year / 12 +hours_per_month = hours_per_day * days_per_year / 12 + + +def strftime(fmt, t): + """A class to replace the strftime in datetime package or time module. + Identical to strftime behavior in those modules except supports any + year. + Also supports datetime.datetime times. + Also supports milliseconds using %s + Also supports microseconds using %u""" + if isinstance(t, (time.struct_time, tuple)): + t = datetime.datetime(*t[:6]) + assert isinstance(t, (datetime.datetime, datetime.time, datetime.date)) + try: + year = t.year + if year < 1900: + t = t.replace(year=1900) + except AttributeError: + year = 1900 + subs = ( + ('%Y', '%04d' % year), + ('%y', '%02d' % (year % 100)), + ('%s', '%03d' % (t.microsecond // 1000)), + ('%u', '%03d' % (t.microsecond % 1000)) + ) + + def doSub(s, sub): + return s.replace(*sub) + + def doSubs(s): + return functools.reduce(doSub, subs, s) + + fmt = '%%'.join(map(doSubs, fmt.split('%%'))) + return t.strftime(fmt) + + +def strptime(s, fmt, tzinfo=None): + """ + A function to replace strptime in the time module. Should behave + identically to the strptime function except it returns a datetime.datetime + object instead of a time.struct_time object. + Also takes an optional tzinfo parameter which is a time zone info object. + """ + res = time.strptime(s, fmt) + return datetime.datetime(tzinfo=tzinfo, *res[:6]) + + +class DatetimeConstructor: + """ + >>> cd = DatetimeConstructor.construct_datetime + >>> cd(datetime.datetime(2011,1,1)) + datetime.datetime(2011, 1, 1, 0, 0) + """ + @classmethod + def construct_datetime(cls, *args, **kwargs): + """Construct a datetime.datetime from a number of different time + types found in python and pythonwin""" + if len(args) == 1: + arg = args[0] + method = cls.__get_dt_constructor( + type(arg).__module__, + type(arg).__name__, + ) + result = method(arg) + try: + result = result.replace(tzinfo=kwargs.pop('tzinfo')) + except KeyError: + pass + if kwargs: + first_key = kwargs.keys()[0] + tmpl = ( + "{first_key} is an invalid keyword " + "argument for this function." + ) + raise TypeError(tmpl.format(**locals())) + else: + result = datetime.datetime(*args, **kwargs) + return result + + @classmethod + def __get_dt_constructor(cls, moduleName, name): + try: + method_name = '__dt_from_{moduleName}_{name}__'.format(**locals()) + return getattr(cls, method_name) + except AttributeError: + tmpl = ( + "No way to construct datetime.datetime from " + "{moduleName}.{name}" + ) + raise TypeError(tmpl.format(**locals())) + + @staticmethod + def __dt_from_datetime_datetime__(source): + dtattrs = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo', + ) + attrs = map(lambda a: getattr(source, a), dtattrs) + return datetime.datetime(*attrs) + + @staticmethod + def __dt_from___builtin___time__(pyt): + "Construct a datetime.datetime from a pythonwin time" + fmtString = '%Y-%m-%d %H:%M:%S' + result = strptime(pyt.Format(fmtString), fmtString) + # get milliseconds and microseconds. The only way to do this is + # to use the __float__ attribute of the time, which is in days. + microseconds_per_day = seconds_per_day * 1000000 + microseconds = float(pyt) * microseconds_per_day + microsecond = int(microseconds % 1000000) + result = result.replace(microsecond=microsecond) + return result + + @staticmethod + def __dt_from_timestamp__(timestamp): + return datetime.datetime.utcfromtimestamp(timestamp) + __dt_from___builtin___float__ = __dt_from_timestamp__ + __dt_from___builtin___long__ = __dt_from_timestamp__ + __dt_from___builtin___int__ = __dt_from_timestamp__ + + @staticmethod + def __dt_from_time_struct_time__(s): + return datetime.datetime(*s[:6]) + + +def datetime_mod(dt, period, start=None): + """ + Find the time which is the specified date/time truncated to the time delta + relative to the start date/time. + By default, the start time is midnight of the same day as the specified + date/time. + + >>> datetime_mod(datetime.datetime(2004, 1, 2, 3), + ... datetime.timedelta(days = 1.5), + ... start = datetime.datetime(2004, 1, 1)) + datetime.datetime(2004, 1, 1, 0, 0) + >>> datetime_mod(datetime.datetime(2004, 1, 2, 13), + ... datetime.timedelta(days = 1.5), + ... start = datetime.datetime(2004, 1, 1)) + datetime.datetime(2004, 1, 2, 12, 0) + >>> datetime_mod(datetime.datetime(2004, 1, 2, 13), + ... datetime.timedelta(days = 7), + ... start = datetime.datetime(2004, 1, 1)) + datetime.datetime(2004, 1, 1, 0, 0) + >>> datetime_mod(datetime.datetime(2004, 1, 10, 13), + ... datetime.timedelta(days = 7), + ... start = datetime.datetime(2004, 1, 1)) + datetime.datetime(2004, 1, 8, 0, 0) + """ + if start is None: + # use midnight of the same day + start = datetime.datetime.combine(dt.date(), datetime.time()) + # calculate the difference between the specified time and the start date. + delta = dt - start + + # now aggregate the delta and the period into microseconds + # Use microseconds because that's the highest precision of these time + # pieces. Also, using microseconds ensures perfect precision (no floating + # point errors). + def get_time_delta_microseconds(td): + return (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds + delta, period = map(get_time_delta_microseconds, (delta, period)) + offset = datetime.timedelta(microseconds=delta % period) + # the result is the original specified time minus the offset + result = dt - offset + return result + + +def datetime_round(dt, period, start=None): + """ + Find the nearest even period for the specified date/time. + + >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13), + ... datetime.timedelta(hours = 1)) + datetime.datetime(2004, 11, 13, 8, 0) + >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13), + ... datetime.timedelta(hours = 1)) + datetime.datetime(2004, 11, 13, 9, 0) + >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30), + ... datetime.timedelta(hours = 1)) + datetime.datetime(2004, 11, 13, 9, 0) + """ + result = datetime_mod(dt, period, start) + if abs(dt - result) >= period // 2: + result += period + return result + + +def get_nearest_year_for_day(day): + """ + Returns the nearest year to now inferred from a Julian date. + """ + now = time.gmtime() + result = now.tm_year + # if the day is far greater than today, it must be from last year + if day - now.tm_yday > 365 // 2: + result -= 1 + # if the day is far less than today, it must be for next year. + if now.tm_yday - day > 365 // 2: + result += 1 + return result + + +def gregorian_date(year, julian_day): + """ + Gregorian Date is defined as a year and a julian day (1-based + index into the days of the year). + + >>> gregorian_date(2007, 15) + datetime.date(2007, 1, 15) + """ + result = datetime.date(year, 1, 1) + result += datetime.timedelta(days=julian_day - 1) + return result + + +def get_period_seconds(period): + """ + return the number of seconds in the specified period + + >>> get_period_seconds('day') + 86400 + >>> get_period_seconds(86400) + 86400 + >>> get_period_seconds(datetime.timedelta(hours=24)) + 86400 + >>> get_period_seconds('day + os.system("rm -Rf *")') + Traceback (most recent call last): + ... + ValueError: period not in (second, minute, hour, day, month, year) + """ + if isinstance(period, six.string_types): + try: + name = 'seconds_per_' + period.lower() + result = globals()[name] + except KeyError: + msg = "period not in (second, minute, hour, day, month, year)" + raise ValueError(msg) + elif isinstance(period, numbers.Number): + result = period + elif isinstance(period, datetime.timedelta): + result = period.days * get_period_seconds('day') + period.seconds + else: + raise TypeError('period must be a string or integer') + return result + + +def get_date_format_string(period): + """ + For a given period (e.g. 'month', 'day', or some numeric interval + such as 3600 (in secs)), return the format string that can be + used with strftime to format that time to specify the times + across that interval, but no more detailed. + For example, + + >>> get_date_format_string('month') + '%Y-%m' + >>> get_date_format_string(3600) + '%Y-%m-%d %H' + >>> get_date_format_string('hour') + '%Y-%m-%d %H' + >>> get_date_format_string(None) + Traceback (most recent call last): + ... + TypeError: period must be a string or integer + >>> get_date_format_string('garbage') + Traceback (most recent call last): + ... + ValueError: period not in (second, minute, hour, day, month, year) + """ + # handle the special case of 'month' which doesn't have + # a static interval in seconds + if isinstance(period, six.string_types) and period.lower() == 'month': + return '%Y-%m' + file_period_secs = get_period_seconds(period) + format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S') + seconds_per_second = 1 + intervals = ( + seconds_per_year, + seconds_per_day, + seconds_per_hour, + seconds_per_minute, + seconds_per_second, + ) + mods = list(map(lambda interval: file_period_secs % interval, intervals)) + format_pieces = format_pieces[: mods.index(0) + 1] + return ''.join(format_pieces) + + +def divide_timedelta_float(td, divisor): + """ + Divide a timedelta by a float value + + >>> one_day = datetime.timedelta(days=1) + >>> half_day = datetime.timedelta(days=.5) + >>> divide_timedelta_float(one_day, 2.0) == half_day + True + >>> divide_timedelta_float(one_day, 2) == half_day + True + """ + # td is comprised of days, seconds, microseconds + dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')] + dsm = map(lambda elem: elem / divisor, dsm) + return datetime.timedelta(*dsm) + + +def calculate_prorated_values(): + """ + A utility function to prompt for a rate (a string in units per + unit time), and return that same rate for various time periods. + """ + rate = six.moves.input("Enter the rate (3/hour, 50/month)> ") + res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict() + value = float(res['value']) + value_per_second = value / get_period_seconds(res['period']) + for period in ('minute', 'hour', 'day', 'month', 'year'): + period_value = value_per_second * get_period_seconds(period) + print("per {period}: {period_value}".format(**locals())) + + +def parse_timedelta(str): + """ + Take a string representing a span of time and parse it to a time delta. + Accepts any string of comma-separated numbers each with a unit indicator. + + >>> parse_timedelta('1 day') + datetime.timedelta(days=1) + + >>> parse_timedelta('1 day, 30 seconds') + datetime.timedelta(days=1, seconds=30) + + >>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds') + datetime.timedelta(days=47, seconds=28848, microseconds=15400) + + Supports weeks, months, years + + >>> parse_timedelta('1 week') + datetime.timedelta(days=7) + + >>> parse_timedelta('1 year, 1 month') + datetime.timedelta(days=395, seconds=58685) + + Note that months and years strict intervals, not aligned + to a calendar: + + >>> now = datetime.datetime.now() + >>> later = now + parse_timedelta('1 year') + >>> later.replace(year=now.year) - now + datetime.timedelta(seconds=20940) + """ + deltas = (_parse_timedelta_part(part.strip()) for part in str.split(',')) + return sum(deltas, datetime.timedelta()) + + +def _parse_timedelta_part(part): + match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part) + if not match: + msg = "Unable to parse {part!r} as a time delta".format(**locals()) + raise ValueError(msg) + unit = match.group('unit').lower() + if not unit.endswith('s'): + unit += 's' + value = float(match.group('value')) + if unit == 'months': + unit = 'years' + value = value / 12 + if unit == 'years': + unit = 'days' + value = value * days_per_year + return datetime.timedelta(**{unit: value}) + + +def divide_timedelta(td1, td2): + """ + Get the ratio of two timedeltas + + >>> one_day = datetime.timedelta(days=1) + >>> one_hour = datetime.timedelta(hours=1) + >>> divide_timedelta(one_hour, one_day) == 1 / 24 + True + """ + try: + return td1 / td2 + except TypeError: + # Python 3.2 gets division + # http://bugs.python.org/issue2706 + return td1.total_seconds() / td2.total_seconds() + + +def date_range(start=None, stop=None, step=None): + """ + Much like the built-in function range, but works with dates + + >>> range_items = date_range( + ... datetime.datetime(2005,12,21), + ... datetime.datetime(2005,12,25), + ... ) + >>> my_range = tuple(range_items) + >>> datetime.datetime(2005,12,21) in my_range + True + >>> datetime.datetime(2005,12,22) in my_range + True + >>> datetime.datetime(2005,12,25) in my_range + False + """ + if step is None: + step = datetime.timedelta(days=1) + if start is None: + start = datetime.datetime.now() + while start < stop: + yield start + start += step diff --git a/libraries/tempora/schedule.py b/libraries/tempora/schedule.py new file mode 100644 index 00000000..1ad093b2 --- /dev/null +++ b/libraries/tempora/schedule.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +""" +Classes for calling functions a schedule. +""" + +from __future__ import absolute_import + +import datetime +import numbers +import abc +import bisect + +import pytz + +__metaclass__ = type + + +def now(): + """ + Provide the current timezone-aware datetime. + + A client may override this function to change the default behavior, + such as to use local time or timezone-naïve times. + """ + return datetime.datetime.utcnow().replace(tzinfo=pytz.utc) + + +def from_timestamp(ts): + """ + Convert a numeric timestamp to a timezone-aware datetime. + + A client may override this function to change the default behavior, + such as to use local time or timezone-naïve times. + """ + return datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=pytz.utc) + + +class DelayedCommand(datetime.datetime): + """ + A command to be executed after some delay (seconds or timedelta). + """ + + @classmethod + def from_datetime(cls, other): + return cls( + other.year, other.month, other.day, other.hour, + other.minute, other.second, other.microsecond, + other.tzinfo, + ) + + @classmethod + def after(cls, delay, target): + if not isinstance(delay, datetime.timedelta): + delay = datetime.timedelta(seconds=delay) + due_time = now() + delay + cmd = cls.from_datetime(due_time) + cmd.delay = delay + cmd.target = target + return cmd + + @staticmethod + def _from_timestamp(input): + """ + If input is a real number, interpret it as a Unix timestamp + (seconds sinc Epoch in UTC) and return a timezone-aware + datetime object. Otherwise return input unchanged. + """ + if not isinstance(input, numbers.Real): + return input + return from_timestamp(input) + + @classmethod + def at_time(cls, at, target): + """ + Construct a DelayedCommand to come due at `at`, where `at` may be + a datetime or timestamp. + """ + at = cls._from_timestamp(at) + cmd = cls.from_datetime(at) + cmd.delay = at - now() + cmd.target = target + return cmd + + def due(self): + return now() >= self + + +class PeriodicCommand(DelayedCommand): + """ + Like a delayed command, but expect this command to run every delay + seconds. + """ + def _next_time(self): + """ + Add delay to self, localized + """ + return self._localize(self + self.delay) + + @staticmethod + def _localize(dt): + """ + Rely on pytz.localize to ensure new result honors DST. + """ + try: + tz = dt.tzinfo + return tz.localize(dt.replace(tzinfo=None)) + except AttributeError: + return dt + + def next(self): + cmd = self.__class__.from_datetime(self._next_time()) + cmd.delay = self.delay + cmd.target = self.target + return cmd + + def __setattr__(self, key, value): + if key == 'delay' and not value > datetime.timedelta(): + raise ValueError( + "A PeriodicCommand must have a positive, " + "non-zero delay." + ) + super(PeriodicCommand, self).__setattr__(key, value) + + +class PeriodicCommandFixedDelay(PeriodicCommand): + """ + Like a periodic command, but don't calculate the delay based on + the current time. Instead use a fixed delay following the initial + run. + """ + + @classmethod + def at_time(cls, at, delay, target): + at = cls._from_timestamp(at) + cmd = cls.from_datetime(at) + if isinstance(delay, numbers.Number): + delay = datetime.timedelta(seconds=delay) + cmd.delay = delay + cmd.target = target + return cmd + + @classmethod + def daily_at(cls, at, target): + """ + Schedule a command to run at a specific time each day. + """ + daily = datetime.timedelta(days=1) + # convert when to the next datetime matching this time + when = datetime.datetime.combine(datetime.date.today(), at) + if when < now(): + when += daily + return cls.at_time(cls._localize(when), daily, target) + + +class Scheduler: + """ + A rudimentary abstract scheduler accepting DelayedCommands + and dispatching them on schedule. + """ + def __init__(self): + self.queue = [] + + def add(self, command): + assert isinstance(command, DelayedCommand) + bisect.insort(self.queue, command) + + def run_pending(self): + while self.queue: + command = self.queue[0] + if not command.due(): + break + self.run(command) + if isinstance(command, PeriodicCommand): + self.add(command.next()) + del self.queue[0] + + @abc.abstractmethod + def run(self, command): + """ + Run the command + """ + + +class InvokeScheduler(Scheduler): + """ + Command targets are functions to be invoked on schedule. + """ + def run(self, command): + command.target() + + +class CallbackScheduler(Scheduler): + """ + Command targets are passed to a dispatch callable on schedule. + """ + def __init__(self, dispatch): + super(CallbackScheduler, self).__init__() + self.dispatch = dispatch + + def run(self, command): + self.dispatch(command.target) diff --git a/libraries/tempora/tests/test_schedule.py b/libraries/tempora/tests/test_schedule.py new file mode 100644 index 00000000..38eb8dc9 --- /dev/null +++ b/libraries/tempora/tests/test_schedule.py @@ -0,0 +1,118 @@ +import time +import random +import datetime + +import pytest +import pytz +import freezegun + +from tempora import schedule + +__metaclass__ = type + + +@pytest.fixture +def naive_times(monkeypatch): + monkeypatch.setattr( + 'irc.schedule.from_timestamp', + datetime.datetime.fromtimestamp) + monkeypatch.setattr('irc.schedule.now', datetime.datetime.now) + + +do_nothing = type(None) +try: + do_nothing() +except TypeError: + # Python 2 compat + def do_nothing(): + return None + + +def test_delayed_command_order(): + """ + delayed commands should be sorted by delay time + """ + delays = [random.randint(0, 99) for x in range(5)] + cmds = sorted([ + schedule.DelayedCommand.after(delay, do_nothing) + for delay in delays + ]) + assert [c.delay.seconds for c in cmds] == sorted(delays) + + +def test_periodic_command_delay(): + "A PeriodicCommand must have a positive, non-zero delay." + with pytest.raises(ValueError) as exc_info: + schedule.PeriodicCommand.after(0, None) + assert str(exc_info.value) == test_periodic_command_delay.__doc__ + + +def test_periodic_command_fixed_delay(): + """ + Test that we can construct a periodic command with a fixed initial + delay. + """ + fd = schedule.PeriodicCommandFixedDelay.at_time( + at=schedule.now(), + delay=datetime.timedelta(seconds=2), + target=lambda: None, + ) + assert fd.due() is True + assert fd.next().due() is False + + +class TestCommands: + def test_delayed_command_from_timestamp(self): + """ + Ensure a delayed command can be constructed from a timestamp. + """ + t = time.time() + schedule.DelayedCommand.at_time(t, do_nothing) + + def test_command_at_noon(self): + """ + Create a periodic command that's run at noon every day. + """ + when = datetime.time(12, 0, tzinfo=pytz.utc) + cmd = schedule.PeriodicCommandFixedDelay.daily_at(when, target=None) + assert cmd.due() is False + next_cmd = cmd.next() + daily = datetime.timedelta(days=1) + day_from_now = schedule.now() + daily + two_days_from_now = day_from_now + daily + assert day_from_now < next_cmd < two_days_from_now + + +class TestTimezones: + def test_alternate_timezone_west(self): + target_tz = pytz.timezone('US/Pacific') + target = schedule.now().astimezone(target_tz) + cmd = schedule.DelayedCommand.at_time(target, target=None) + assert cmd.due() + + def test_alternate_timezone_east(self): + target_tz = pytz.timezone('Europe/Amsterdam') + target = schedule.now().astimezone(target_tz) + cmd = schedule.DelayedCommand.at_time(target, target=None) + assert cmd.due() + + def test_daylight_savings(self): + """ + A command at 9am should always be 9am regardless of + a DST boundary. + """ + with freezegun.freeze_time('2018-03-10 08:00:00'): + target_tz = pytz.timezone('US/Eastern') + target_time = datetime.time(9, tzinfo=target_tz) + cmd = schedule.PeriodicCommandFixedDelay.daily_at( + target_time, + target=lambda: None, + ) + + def naive(dt): + return dt.replace(tzinfo=None) + + assert naive(cmd) == datetime.datetime(2018, 3, 10, 9, 0, 0) + next_ = cmd.next() + assert naive(next_) == datetime.datetime(2018, 3, 11, 9, 0, 0) + assert next_ - cmd == datetime.timedelta(hours=23) diff --git a/libraries/tempora/timing.py b/libraries/tempora/timing.py new file mode 100644 index 00000000..03c22454 --- /dev/null +++ b/libraries/tempora/timing.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals, absolute_import + +import datetime +import functools +import numbers +import time + +__metaclass__ = type + + +class Stopwatch: + """ + A simple stopwatch which starts automatically. + + >>> w = Stopwatch() + >>> _1_sec = datetime.timedelta(seconds=1) + >>> w.split() < _1_sec + True + >>> import time + >>> time.sleep(1.0) + >>> w.split() >= _1_sec + True + >>> w.stop() >= _1_sec + True + >>> w.reset() + >>> w.start() + >>> w.split() < _1_sec + True + + It should be possible to launch the Stopwatch in a context: + + >>> with Stopwatch() as watch: + ... assert isinstance(watch.split(), datetime.timedelta) + + In that case, the watch is stopped when the context is exited, + so to read the elapsed time:: + + >>> watch.elapsed + datetime.timedelta(...) + >>> watch.elapsed.seconds + 0 + """ + def __init__(self): + self.reset() + self.start() + + def reset(self): + self.elapsed = datetime.timedelta(0) + if hasattr(self, 'start_time'): + del self.start_time + + def start(self): + self.start_time = datetime.datetime.utcnow() + + def stop(self): + stop_time = datetime.datetime.utcnow() + self.elapsed += stop_time - self.start_time + del self.start_time + return self.elapsed + + def split(self): + local_duration = datetime.datetime.utcnow() - self.start_time + return self.elapsed + local_duration + + # context manager support + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.stop() + + +class IntervalGovernor: + """ + Decorate a function to only allow it to be called once per + min_interval. Otherwise, it returns None. + """ + def __init__(self, min_interval): + if isinstance(min_interval, numbers.Number): + min_interval = datetime.timedelta(seconds=min_interval) + self.min_interval = min_interval + self.last_call = None + + def decorate(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + allow = ( + not self.last_call + or self.last_call.split() > self.min_interval + ) + if allow: + self.last_call = Stopwatch() + return func(*args, **kwargs) + return wrapper + + __call__ = decorate + + +class Timer(Stopwatch): + """ + Watch for a target elapsed time. + + >>> t = Timer(0.1) + >>> t.expired() + False + >>> __import__('time').sleep(0.15) + >>> t.expired() + True + """ + def __init__(self, target=float('Inf')): + self.target = self._accept(target) + super(Timer, self).__init__() + + def _accept(self, target): + "Accept None or ∞ or datetime or numeric for target" + if isinstance(target, datetime.timedelta): + target = target.total_seconds() + + if target is None: + # treat None as infinite target + target = float('Inf') + + return target + + def expired(self): + return self.split().total_seconds() > self.target + + +class BackoffDelay: + """ + Exponential backoff delay. + + Useful for defining delays between retries. Consider for use + with ``jaraco.functools.retry_call`` as the cleanup. + + Default behavior has no effect; a delay or jitter must + be supplied for the call to be non-degenerate. + + >>> bd = BackoffDelay() + >>> bd() + >>> bd() + + The following instance will delay 10ms for the first call, + 20ms for the second, etc. + + >>> bd = BackoffDelay(delay=0.01, factor=2) + >>> bd() + >>> bd() + + Inspect and adjust the state of the delay anytime. + + >>> bd.delay + 0.04 + >>> bd.delay = 0.01 + + Set limit to prevent the delay from exceeding bounds. + + >>> bd = BackoffDelay(delay=0.01, factor=2, limit=0.015) + >>> bd() + >>> bd.delay + 0.015 + + Limit may be a callable taking a number and returning + the limited number. + + >>> at_least_one = lambda n: max(n, 1) + >>> bd = BackoffDelay(delay=0.01, factor=2, limit=at_least_one) + >>> bd() + >>> bd.delay + 1 + + Pass a jitter to add or subtract seconds to the delay. + + >>> bd = BackoffDelay(jitter=0.01) + >>> bd() + >>> bd.delay + 0.01 + + Jitter may be a callable. To supply a non-deterministic jitter + between -0.5 and 0.5, consider: + + >>> import random + >>> jitter=functools.partial(random.uniform, -0.5, 0.5) + >>> bd = BackoffDelay(jitter=jitter) + >>> bd() + >>> 0 <= bd.delay <= 0.5 + True + """ + + delay = 0 + + factor = 1 + "Multiplier applied to delay" + + jitter = 0 + "Number or callable returning extra seconds to add to delay" + + def __init__(self, delay=0, factor=1, limit=float('inf'), jitter=0): + self.delay = delay + self.factor = factor + if isinstance(limit, numbers.Number): + limit_ = limit + + def limit(n): + return max(0, min(limit_, n)) + self.limit = limit + if isinstance(jitter, numbers.Number): + jitter_ = jitter + + def jitter(): + return jitter_ + self.jitter = jitter + + def __call__(self): + time.sleep(self.delay) + self.delay = self.limit(self.delay * self.factor + self.jitter()) diff --git a/libraries/tempora/utc.py b/libraries/tempora/utc.py new file mode 100644 index 00000000..35bfdb06 --- /dev/null +++ b/libraries/tempora/utc.py @@ -0,0 +1,36 @@ +""" +Facilities for common time operations in UTC. + +Inspired by the `utc project <https://pypi.org/project/utc>`_. + +>>> dt = now() +>>> dt == fromtimestamp(dt.timestamp()) +True +>>> dt.tzinfo +datetime.timezone.utc + +>>> from time import time as timestamp +>>> now().timestamp() - timestamp() < 0.1 +True + +>>> datetime(2018, 6, 26, 0).tzinfo +datetime.timezone.utc + +>>> time(0, 0).tzinfo +datetime.timezone.utc +""" + +import datetime as std +import functools + + +__all__ = ['now', 'fromtimestamp', 'datetime', 'time'] + + +now = functools.partial(std.datetime.now, std.timezone.utc) +fromtimestamp = functools.partial( + std.datetime.fromtimestamp, + tz=std.timezone.utc, +) +datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc) +time = functools.partial(std.time, tzinfo=std.timezone.utc) diff --git a/libraries/zc/__init__.py b/libraries/zc/__init__.py new file mode 100644 index 00000000..146c3362 --- /dev/null +++ b/libraries/zc/__init__.py @@ -0,0 +1 @@ +__namespace__ = 'zc' \ No newline at end of file diff --git a/libraries/zc/lockfile/README.txt b/libraries/zc/lockfile/README.txt new file mode 100644 index 00000000..89ef33e9 --- /dev/null +++ b/libraries/zc/lockfile/README.txt @@ -0,0 +1,70 @@ +Lock file support +================= + +The ZODB lock_file module provides support for creating file system +locks. These are locks that are implemented with lock files and +OS-provided locking facilities. To create a lock, instantiate a +LockFile object with a file name: + + >>> import zc.lockfile + >>> lock = zc.lockfile.LockFile('lock') + +If we try to lock the same name, we'll get a lock error: + + >>> import zope.testing.loggingsupport + >>> handler = zope.testing.loggingsupport.InstalledHandler('zc.lockfile') + >>> try: + ... zc.lockfile.LockFile('lock') + ... except zc.lockfile.LockError: + ... print("Can't lock file") + Can't lock file + +.. We don't log failure to acquire. + + >>> for record in handler.records: # doctest: +ELLIPSIS + ... print(record.levelname+' '+record.getMessage()) + +To release the lock, use it's close method: + + >>> lock.close() + +The lock file is not removed. It is left behind: + + >>> import os + >>> os.path.exists('lock') + True + +Of course, now that we've released the lock, we can create it again: + + >>> lock = zc.lockfile.LockFile('lock') + >>> lock.close() + +.. Cleanup + + >>> import os + >>> os.remove('lock') + +Hostname in lock file +===================== + +In a container environment (e.g. Docker), the PID is typically always +identical even if multiple containers are running under the same operating +system instance. + +Clearly, inspecting lock files doesn't then help much in debugging. To identify +the container which created the lock file, we need information about the +container in the lock file. Since Docker uses the container identifier or name +as the hostname, this information can be stored in the lock file in addition to +or instead of the PID. + +Use the ``content_template`` keyword argument to ``LockFile`` to specify a +custom lock file content format: + + >>> lock = zc.lockfile.LockFile('lock', content_template='{pid};{hostname}') + >>> lock.close() + +If you now inspected the lock file, you would see e.g.: + + $ cat lock + 123;myhostname + diff --git a/libraries/zc/lockfile/__init__.py b/libraries/zc/lockfile/__init__.py new file mode 100644 index 00000000..a0ac2ff1 --- /dev/null +++ b/libraries/zc/lockfile/__init__.py @@ -0,0 +1,104 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +import os +import errno +import logging +logger = logging.getLogger("zc.lockfile") + +class LockError(Exception): + """Couldn't get a lock + """ + +try: + import fcntl +except ImportError: + try: + import msvcrt + except ImportError: + def _lock_file(file): + raise TypeError('No file-locking support on this platform') + def _unlock_file(file): + raise TypeError('No file-locking support on this platform') + + else: + # Windows + def _lock_file(file): + # Lock just the first byte + try: + msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1) + except IOError: + raise LockError("Couldn't lock %r" % file.name) + + def _unlock_file(file): + try: + file.seek(0) + msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1) + except IOError: + raise LockError("Couldn't unlock %r" % file.name) + +else: + # Unix + _flags = fcntl.LOCK_EX | fcntl.LOCK_NB + + def _lock_file(file): + try: + fcntl.flock(file.fileno(), _flags) + except IOError: + raise LockError("Couldn't lock %r" % file.name) + + def _unlock_file(file): + fcntl.flock(file.fileno(), fcntl.LOCK_UN) + +class LazyHostName(object): + """Avoid importing socket and calling gethostname() unnecessarily""" + def __str__(self): + import socket + return socket.gethostname() + + +class LockFile: + + _fp = None + + def __init__(self, path, content_template='{pid}'): + self._path = path + try: + # Try to open for writing without truncation: + fp = open(path, 'r+') + except IOError: + # If the file doesn't exist, we'll get an IO error, try a+ + # Note that there may be a race here. Multiple processes + # could fail on the r+ open and open the file a+, but only + # one will get the the lock and write a pid. + fp = open(path, 'a+') + + try: + _lock_file(fp) + except: + fp.close() + raise + + # We got the lock, record info in the file. + self._fp = fp + fp.write(" %s\n" % content_template.format(pid=os.getpid(), + hostname=LazyHostName())) + fp.truncate() + fp.flush() + + def close(self): + if self._fp is not None: + _unlock_file(self._fp) + self._fp.close() + self._fp = None diff --git a/libraries/zc/lockfile/tests.py b/libraries/zc/lockfile/tests.py new file mode 100644 index 00000000..e9fcbff3 --- /dev/null +++ b/libraries/zc/lockfile/tests.py @@ -0,0 +1,193 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +import os, re, sys, unittest, doctest +import zc.lockfile, time, threading +from zope.testing import renormalizing, setupstack +import tempfile +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + +checker = renormalizing.RENormalizing([ + # Python 3 adds module path to error class name. + (re.compile("zc\.lockfile\.LockError:"), + r"LockError:"), + ]) + +def inc(): + while 1: + try: + lock = zc.lockfile.LockFile('f.lock') + except zc.lockfile.LockError: + continue + else: + break + f = open('f', 'r+b') + v = int(f.readline().strip()) + time.sleep(0.01) + v += 1 + f.seek(0) + f.write(('%d\n' % v).encode('ASCII')) + f.close() + lock.close() + +def many_threads_read_and_write(): + r""" + >>> with open('f', 'w+b') as file: + ... _ = file.write(b'0\n') + >>> with open('f.lock', 'w+b') as file: + ... _ = file.write(b'0\n') + + >>> n = 50 + >>> threads = [threading.Thread(target=inc) for i in range(n)] + >>> _ = [thread.start() for thread in threads] + >>> _ = [thread.join() for thread in threads] + >>> with open('f', 'rb') as file: + ... saved = int(file.read().strip()) + >>> saved == n + True + + >>> os.remove('f') + + We should only have one pid in the lock file: + + >>> f = open('f.lock') + >>> len(f.read().strip().split()) + 1 + >>> f.close() + + >>> os.remove('f.lock') + + """ + +def pid_in_lockfile(): + r""" + >>> import os, zc.lockfile + >>> pid = os.getpid() + >>> lock = zc.lockfile.LockFile("f.lock") + >>> f = open("f.lock") + >>> _ = f.seek(1) + >>> f.read().strip() == str(pid) + True + >>> f.close() + + Make sure that locking twice does not overwrite the old pid: + + >>> lock = zc.lockfile.LockFile("f.lock") + Traceback (most recent call last): + ... + LockError: Couldn't lock 'f.lock' + + >>> f = open("f.lock") + >>> _ = f.seek(1) + >>> f.read().strip() == str(pid) + True + >>> f.close() + + >>> lock.close() + """ + + +def hostname_in_lockfile(): + r""" + hostname is correctly written into the lock file when it's included in the + lock file content template + + >>> import zc.lockfile + >>> with patch('socket.gethostname', Mock(return_value='myhostname')): + ... lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}') + >>> f = open("f.lock") + >>> _ = f.seek(1) + >>> f.read().rstrip() + 'myhostname' + >>> f.close() + + Make sure that locking twice does not overwrite the old hostname: + + >>> lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}') + Traceback (most recent call last): + ... + LockError: Couldn't lock 'f.lock' + + >>> f = open("f.lock") + >>> _ = f.seek(1) + >>> f.read().rstrip() + 'myhostname' + >>> f.close() + + >>> lock.close() + """ + + +class TestLogger(object): + def __init__(self): + self.log_entries = [] + + def exception(self, msg, *args): + self.log_entries.append((msg,) + args) + + +class LockFileLogEntryTestCase(unittest.TestCase): + """Tests for logging in case of lock failure""" + def setUp(self): + self.here = os.getcwd() + self.tmp = tempfile.mkdtemp(prefix='zc.lockfile-test-') + os.chdir(self.tmp) + + def tearDown(self): + os.chdir(self.here) + setupstack.rmtree(self.tmp) + + def test_log_formatting(self): + # PID and hostname are parsed and logged from lock file on failure + with patch('os.getpid', Mock(return_value=123)): + with patch('socket.gethostname', Mock(return_value='myhostname')): + lock = zc.lockfile.LockFile('f.lock', + content_template='{pid}/{hostname}') + with open('f.lock') as f: + self.assertEqual(' 123/myhostname\n', f.read()) + + lock.close() + + def test_unlock_and_lock_while_multiprocessing_process_running(self): + import multiprocessing + + lock = zc.lockfile.LockFile('l') + q = multiprocessing.Queue() + p = multiprocessing.Process(target=q.get) + p.daemon = True + p.start() + + # release and re-acquire should work (obviously) + lock.close() + lock = zc.lockfile.LockFile('l') + self.assertTrue(p.is_alive()) + + q.put(0) + lock.close() + p.join() + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocFileSuite( + 'README.txt', checker=checker, + setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown)) + suite.addTest(doctest.DocTestSuite( + setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown, + checker=checker)) + # Add unittest test cases from this module + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(__name__)) + return suite diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 34826af0..7f692846 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -2,17 +2,14 @@ ################################################################################################# -import SimpleHTTPServer -import BaseHTTPServer import logging -import httplib import threading -import urlparse -import urllib import xbmc import xbmcvfs +import cherrypy + ################################################################################################# PORT = 57578 @@ -21,181 +18,59 @@ LOG = logging.getLogger("EMBY."+__name__) ################################################################################################# +class Root(object): + + @cherrypy.expose + def default(self, *args, **kwargs): + + try: + if not kwargs.get('Id').isdigit(): + raise IndexError("Incorrect Id format: %s" % kwargs.get('Id')) + + LOG.info("Webservice called with params: %s", kwargs) + + return ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" + % (kwargs.get('Id'), kwargs.get('KodiId'), kwargs.get('Name'), kwargs.get('transcode') or False)) + + except IndexError as error: + LOG.error(error) + + raise cherrypy.HTTPError(404, error) + + except Exception as error: + LOG.exception(error) + + raise cherrypy.HTTPError(500, "Exception occurred: %s" % error) + class WebService(threading.Thread): - ''' Run a webservice to trigger playback. - Inspired from script.skin.helper.service by marcelveldt. - ''' - stop_thread = False + root = None def __init__(self): + + self.root = Root() + cherrypy.config.update({ + 'engine.autoreload.on' : False, + 'log.screen': False, + 'engine.timeout_monitor.frequency': 5, + 'server.shutdown_timeout': 1, + }) threading.Thread.__init__(self) + def run(self): + + LOG.info("--->[ webservice/%s ]", PORT) + conf = { + 'global': { + 'server.socket_host': '0.0.0.0', + 'server.socket_port': PORT + }, '/': {} + } + cherrypy.quickstart(self.root, '/', conf) + def stop(self): - ''' Called when the thread needs to stop - ''' - try: - conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT) - conn.request("QUIT", "/") - conn.getresponse() - self.stop_thread = True - except Exception as error: - LOG.exception(error) + cherrypy.engine.exit() + self.join(0) - def run(self): - - ''' Called to start the webservice. - ''' - LOG.info("--->[ webservice/%s ]", PORT) - - try: - server = StoppableHttpServer(('127.0.0.1', PORT), StoppableHttpRequestHandler) - server.serve_forever() - except Exception as error: - - if '10053' not in error: # ignore host diconnected errors - LOG.exception(error) - - LOG.info("---<[ webservice ]") - - -class Request(object): - - ''' Attributes from urlsplit that this class also sets - ''' - uri_attrs = ('scheme', 'netloc', 'path', 'query', 'fragment') - - def __init__(self, uri, headers, rfile=None): - - self.uri = uri - self.headers = headers - parsed = urlparse.urlsplit(uri) - - for i, attr in enumerate(self.uri_attrs): - setattr(self, attr, parsed[i]) - - try: - body_len = int(self.headers.get('Content-length', 0)) - except ValueError: - body_len = 0 - - self.body = rfile.read(body_len) if body_len and rfile else None - - -class StoppableHttpServer(BaseHTTPServer.HTTPServer): - - ''' Http server that reacts to self.stop flag. - ''' - def serve_forever(self): - - ''' Handle one request at a time until stopped. - ''' - self.stop = False - - while not self.stop: - - self.handle_request() - xbmc.sleep(100) - - -class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - - ''' http request handler with QUIT stopping the server - ''' - raw_requestline = "" - - def __init__(self, request, client_address, server): - try: - SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, request, client_address, server) - except Exception: - pass - - def log_message(self, format, *args): - - ''' Mute the webservice requests. - ''' - pass - - def do_QUIT(self): - - ''' send 200 OK response, and set server.stop to True - ''' - self.send_response(200) - self.end_headers() - self.server.stop = True - - def parse_request(self): - - ''' Modify here to workaround unencoded requests. - ''' - retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self) - self.request = Request(self.path, self.headers, self.rfile) - - return retval - - def do_HEAD(self): - - ''' Called on HEAD requests - ''' - self.handle_request(True) - - return - - def get_params(self): - - ''' Get the params - ''' - try: - path = self.path[1:] - - if '?' in path: - path = path.split('?', 1)[1] - - params = dict(urlparse.parse_qsl(path)) - except Exception: - params = {} - - return params - - def handle_request(self, headers_only=False): - - ''' Send headers and reponse - ''' - try: - params = self.get_params() - - if not params.get('Id').isdigit(): - raise IndexError("Incorrect Id format: %s" % params.get('Id')) - - LOG.info("Webservice called with params: %s", params) - - path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" - % (params.get('Id'), params.get('KodiId'), params.get('Name'), params.get('transcode') or False)) - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.send_header('Content-Length', len(path)) - self.end_headers() - - if not headers_only: - self.wfile.write(path) - except IndexError as error: - - LOG.error(error) - self.send_error(403) - - except Exception as error: - - LOG.exception(error) - self.send_error(500, "Exception occurred: %s" % error) - - return - - def do_GET(self): - - ''' Called on GET requests - ''' - self.handle_request() - - return + del self.root From c367deed4a7f918eb99a0bb2c5f7ab355dbc1aec Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Wed, 30 Jan 2019 06:43:24 -0600 Subject: [PATCH 07/12] Update translation Dutch --- .../language/resource.language.nl_nl/strings.po | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index ce8e5eb5..724b877c 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -3,14 +3,14 @@ # Addon id: plugin.video.emby # Addon Provider: angelblue05 # Translators: -# inSmithereens, 2019 +# 63ac71fcbd0581bb567b1f0d798c7970, 2019 # msgid "" msgstr "" "Project-Id-Version: Emby for Kodi\n" "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2018-09-07 20:10+0000\n" -"Last-Translator: inSmithereens, 2019\n" +"Last-Translator: 63ac71fcbd0581bb567b1f0d798c7970, 2019\n" "Language-Team: Dutch (https://www.transifex.com/emby-for-kodi/teams/91090/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -227,7 +227,7 @@ msgid "Verify connection" msgstr "Verbinding controleren" msgctxt "#30504" -msgid "Use altername device name" +msgid "Use alternate device name" msgstr "Alternatieve apparaat naam gebruiken" msgctxt "#30506" @@ -1056,8 +1056,15 @@ msgstr "Bibliotheken beheren" msgctxt "#33195" msgid "Enable Emby for Kodi" -msgstr "" +msgstr "Emby for Kodi inschakelen" msgctxt "#33196" msgid "Advanced options" +msgstr "Geavanceerde opties" + +msgctxt "#33197" +msgid "" +"A sync is already running, please wait until it completes and try again." msgstr "" +"Synchronisatie is al in voortgang, wacht tot deze compleet is en probeer het" +" nog eens." From 0e30a28dcb86f3457c91377a430ff2c470ce8ef5 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Thu, 31 Jan 2019 04:06:46 -0600 Subject: [PATCH 08/12] Fix webservice being stalled This time, it should work. Logging was causing the stall?! --- libraries/backports/__init__.py | 1 - libraries/backports/functools_lru_cache.py | 184 -- libraries/cheroot/__init__.py | 6 - libraries/cheroot/__main__.py | 6 - libraries/cheroot/_compat.py | 66 - libraries/cheroot/cli.py | 233 -- libraries/cheroot/errors.py | 58 - libraries/cheroot/makefile.py | 387 --- libraries/cheroot/server.py | 2001 --------------- libraries/cheroot/ssl/__init__.py | 51 - libraries/cheroot/ssl/builtin.py | 162 -- libraries/cheroot/ssl/pyopenssl.py | 267 -- libraries/cheroot/test/__init__.py | 1 - libraries/cheroot/test/conftest.py | 27 - libraries/cheroot/test/helper.py | 169 -- libraries/cheroot/test/test.pem | 38 - libraries/cheroot/test/test__compat.py | 49 - libraries/cheroot/test/test_conn.py | 897 ------- libraries/cheroot/test/test_core.py | 405 --- libraries/cheroot/test/test_server.py | 193 -- libraries/cheroot/test/webtest.py | 581 ----- libraries/cheroot/testing.py | 144 -- libraries/cheroot/workers/__init__.py | 1 - libraries/cheroot/workers/threadpool.py | 271 -- libraries/cheroot/wsgi.py | 423 ---- libraries/cherrypy/__init__.py | 362 --- libraries/cherrypy/__main__.py | 5 - libraries/cherrypy/_cpchecker.py | 325 --- libraries/cherrypy/_cpcompat.py | 162 -- libraries/cherrypy/_cpconfig.py | 300 --- libraries/cherrypy/_cpdispatch.py | 686 ----- libraries/cherrypy/_cperror.py | 619 ----- libraries/cherrypy/_cplogging.py | 482 ---- libraries/cherrypy/_cpmodpy.py | 356 --- libraries/cherrypy/_cpnative_server.py | 160 -- libraries/cherrypy/_cpreqbody.py | 1000 -------- libraries/cherrypy/_cprequest.py | 930 ------- libraries/cherrypy/_cpserver.py | 252 -- libraries/cherrypy/_cptools.py | 509 ---- libraries/cherrypy/_cptree.py | 313 --- libraries/cherrypy/_cpwsgi.py | 467 ---- libraries/cherrypy/_cpwsgi_server.py | 110 - libraries/cherrypy/_helper.py | 344 --- libraries/cherrypy/daemon.py | 107 - libraries/cherrypy/favicon.ico | Bin 1406 -> 0 bytes libraries/cherrypy/lib/__init__.py | 96 - libraries/cherrypy/lib/auth_basic.py | 120 - libraries/cherrypy/lib/auth_digest.py | 464 ---- libraries/cherrypy/lib/caching.py | 482 ---- libraries/cherrypy/lib/covercp.py | 391 --- libraries/cherrypy/lib/cpstats.py | 696 ------ libraries/cherrypy/lib/cptools.py | 640 ----- libraries/cherrypy/lib/encoding.py | 436 ---- libraries/cherrypy/lib/gctools.py | 218 -- libraries/cherrypy/lib/httputil.py | 581 ----- libraries/cherrypy/lib/jsontools.py | 88 - libraries/cherrypy/lib/locking.py | 47 - libraries/cherrypy/lib/profiler.py | 221 -- libraries/cherrypy/lib/reprconf.py | 514 ---- libraries/cherrypy/lib/sessions.py | 919 ------- libraries/cherrypy/lib/static.py | 390 --- libraries/cherrypy/lib/xmlrpcutil.py | 61 - libraries/cherrypy/process/__init__.py | 17 - libraries/cherrypy/process/plugins.py | 752 ------ libraries/cherrypy/process/servers.py | 416 ---- libraries/cherrypy/process/win32.py | 183 -- libraries/cherrypy/process/wspbus.py | 590 ----- libraries/cherrypy/scaffold/__init__.py | 63 - libraries/cherrypy/scaffold/apache-fcgi.conf | 22 - libraries/cherrypy/scaffold/example.conf | 3 - libraries/cherrypy/scaffold/site.conf | 14 - .../static/made_with_cherrypy_small.png | Bin 6347 -> 0 bytes libraries/cherrypy/test/__init__.py | 24 - libraries/cherrypy/test/_test_decorators.py | 39 - libraries/cherrypy/test/_test_states_demo.py | 69 - libraries/cherrypy/test/benchmark.py | 425 ---- libraries/cherrypy/test/checkerdemo.py | 49 - libraries/cherrypy/test/fastcgi.conf | 18 - libraries/cherrypy/test/fcgi.conf | 14 - libraries/cherrypy/test/helper.py | 542 ---- libraries/cherrypy/test/logtest.py | 228 -- libraries/cherrypy/test/modfastcgi.py | 136 - libraries/cherrypy/test/modfcgid.py | 124 - libraries/cherrypy/test/modpy.py | 164 -- libraries/cherrypy/test/modwsgi.py | 154 -- libraries/cherrypy/test/sessiondemo.py | 161 -- libraries/cherrypy/test/static/404.html | 5 - libraries/cherrypy/test/static/dirback.jpg | Bin 16585 -> 0 bytes libraries/cherrypy/test/static/index.html | 1 - libraries/cherrypy/test/style.css | 1 - libraries/cherrypy/test/test.pem | 38 - libraries/cherrypy/test/test_auth_basic.py | 135 - libraries/cherrypy/test/test_auth_digest.py | 134 - libraries/cherrypy/test/test_bus.py | 274 -- libraries/cherrypy/test/test_caching.py | 392 --- libraries/cherrypy/test/test_compat.py | 34 - libraries/cherrypy/test/test_config.py | 303 --- libraries/cherrypy/test/test_config_server.py | 126 - libraries/cherrypy/test/test_conn.py | 873 ------- libraries/cherrypy/test/test_core.py | 823 ------ .../test/test_dynamicobjectmapping.py | 424 ---- libraries/cherrypy/test/test_encoding.py | 426 ---- libraries/cherrypy/test/test_etags.py | 84 - libraries/cherrypy/test/test_http.py | 307 --- libraries/cherrypy/test/test_httputil.py | 80 - libraries/cherrypy/test/test_iterator.py | 196 -- libraries/cherrypy/test/test_json.py | 102 - libraries/cherrypy/test/test_logging.py | 209 -- libraries/cherrypy/test/test_mime.py | 134 - libraries/cherrypy/test/test_misc_tools.py | 210 -- libraries/cherrypy/test/test_native.py | 35 - libraries/cherrypy/test/test_objectmapping.py | 430 ---- libraries/cherrypy/test/test_params.py | 61 - libraries/cherrypy/test/test_plugins.py | 14 - libraries/cherrypy/test/test_proxy.py | 154 -- libraries/cherrypy/test/test_refleaks.py | 66 - libraries/cherrypy/test/test_request_obj.py | 932 ------- libraries/cherrypy/test/test_routes.py | 80 - libraries/cherrypy/test/test_session.py | 512 ---- .../cherrypy/test/test_sessionauthenticate.py | 61 - libraries/cherrypy/test/test_states.py | 473 ---- libraries/cherrypy/test/test_static.py | 434 ---- libraries/cherrypy/test/test_tools.py | 468 ---- libraries/cherrypy/test/test_tutorials.py | 210 -- libraries/cherrypy/test/test_virtualhost.py | 113 - libraries/cherrypy/test/test_wsgi_ns.py | 93 - .../cherrypy/test/test_wsgi_unix_socket.py | 93 - libraries/cherrypy/test/test_wsgi_vhost.py | 35 - libraries/cherrypy/test/test_wsgiapps.py | 120 - libraries/cherrypy/test/test_xmlrpc.py | 183 -- libraries/cherrypy/test/webtest.py | 11 - libraries/cherrypy/tutorial/README.rst | 16 - libraries/cherrypy/tutorial/__init__.py | 3 - libraries/cherrypy/tutorial/custom_error.html | 14 - libraries/cherrypy/tutorial/pdf_file.pdf | Bin 85698 -> 0 bytes .../cherrypy/tutorial/tut01_helloworld.py | 34 - .../cherrypy/tutorial/tut02_expose_methods.py | 32 - .../cherrypy/tutorial/tut03_get_and_post.py | 51 - .../cherrypy/tutorial/tut04_complex_site.py | 103 - .../tutorial/tut05_derived_objects.py | 80 - .../cherrypy/tutorial/tut06_default_method.py | 61 - libraries/cherrypy/tutorial/tut07_sessions.py | 41 - .../tutorial/tut08_generators_and_yield.py | 44 - libraries/cherrypy/tutorial/tut09_files.py | 105 - .../cherrypy/tutorial/tut10_http_errors.py | 84 - libraries/cherrypy/tutorial/tutorial.conf | 4 - libraries/contextlib2.py | 436 ---- libraries/more_itertools/__init__.py | 2 - libraries/more_itertools/more.py | 2211 ----------------- libraries/more_itertools/recipes.py | 565 ----- libraries/more_itertools/tests/__init__.py | 0 libraries/more_itertools/tests/test_more.py | 2074 ---------------- .../more_itertools/tests/test_recipes.py | 616 ----- libraries/portend.py | 212 -- libraries/tempora/__init__.py | 505 ---- libraries/tempora/schedule.py | 202 -- libraries/tempora/tests/test_schedule.py | 118 - libraries/tempora/timing.py | 219 -- libraries/tempora/utc.py | 36 - libraries/zc/__init__.py | 1 - libraries/zc/lockfile/README.txt | 70 - libraries/zc/lockfile/__init__.py | 104 - libraries/zc/lockfile/tests.py | 193 -- resources/lib/webservice.py | 167 +- 164 files changed, 116 insertions(+), 42857 deletions(-) delete mode 100644 libraries/backports/__init__.py delete mode 100644 libraries/backports/functools_lru_cache.py delete mode 100644 libraries/cheroot/__init__.py delete mode 100644 libraries/cheroot/__main__.py delete mode 100644 libraries/cheroot/_compat.py delete mode 100644 libraries/cheroot/cli.py delete mode 100644 libraries/cheroot/errors.py delete mode 100644 libraries/cheroot/makefile.py delete mode 100644 libraries/cheroot/server.py delete mode 100644 libraries/cheroot/ssl/__init__.py delete mode 100644 libraries/cheroot/ssl/builtin.py delete mode 100644 libraries/cheroot/ssl/pyopenssl.py delete mode 100644 libraries/cheroot/test/__init__.py delete mode 100644 libraries/cheroot/test/conftest.py delete mode 100644 libraries/cheroot/test/helper.py delete mode 100644 libraries/cheroot/test/test.pem delete mode 100644 libraries/cheroot/test/test__compat.py delete mode 100644 libraries/cheroot/test/test_conn.py delete mode 100644 libraries/cheroot/test/test_core.py delete mode 100644 libraries/cheroot/test/test_server.py delete mode 100644 libraries/cheroot/test/webtest.py delete mode 100644 libraries/cheroot/testing.py delete mode 100644 libraries/cheroot/workers/__init__.py delete mode 100644 libraries/cheroot/workers/threadpool.py delete mode 100644 libraries/cheroot/wsgi.py delete mode 100644 libraries/cherrypy/__init__.py delete mode 100644 libraries/cherrypy/__main__.py delete mode 100644 libraries/cherrypy/_cpchecker.py delete mode 100644 libraries/cherrypy/_cpcompat.py delete mode 100644 libraries/cherrypy/_cpconfig.py delete mode 100644 libraries/cherrypy/_cpdispatch.py delete mode 100644 libraries/cherrypy/_cperror.py delete mode 100644 libraries/cherrypy/_cplogging.py delete mode 100644 libraries/cherrypy/_cpmodpy.py delete mode 100644 libraries/cherrypy/_cpnative_server.py delete mode 100644 libraries/cherrypy/_cpreqbody.py delete mode 100644 libraries/cherrypy/_cprequest.py delete mode 100644 libraries/cherrypy/_cpserver.py delete mode 100644 libraries/cherrypy/_cptools.py delete mode 100644 libraries/cherrypy/_cptree.py delete mode 100644 libraries/cherrypy/_cpwsgi.py delete mode 100644 libraries/cherrypy/_cpwsgi_server.py delete mode 100644 libraries/cherrypy/_helper.py delete mode 100644 libraries/cherrypy/daemon.py delete mode 100644 libraries/cherrypy/favicon.ico delete mode 100644 libraries/cherrypy/lib/__init__.py delete mode 100644 libraries/cherrypy/lib/auth_basic.py delete mode 100644 libraries/cherrypy/lib/auth_digest.py delete mode 100644 libraries/cherrypy/lib/caching.py delete mode 100644 libraries/cherrypy/lib/covercp.py delete mode 100644 libraries/cherrypy/lib/cpstats.py delete mode 100644 libraries/cherrypy/lib/cptools.py delete mode 100644 libraries/cherrypy/lib/encoding.py delete mode 100644 libraries/cherrypy/lib/gctools.py delete mode 100644 libraries/cherrypy/lib/httputil.py delete mode 100644 libraries/cherrypy/lib/jsontools.py delete mode 100644 libraries/cherrypy/lib/locking.py delete mode 100644 libraries/cherrypy/lib/profiler.py delete mode 100644 libraries/cherrypy/lib/reprconf.py delete mode 100644 libraries/cherrypy/lib/sessions.py delete mode 100644 libraries/cherrypy/lib/static.py delete mode 100644 libraries/cherrypy/lib/xmlrpcutil.py delete mode 100644 libraries/cherrypy/process/__init__.py delete mode 100644 libraries/cherrypy/process/plugins.py delete mode 100644 libraries/cherrypy/process/servers.py delete mode 100644 libraries/cherrypy/process/win32.py delete mode 100644 libraries/cherrypy/process/wspbus.py delete mode 100644 libraries/cherrypy/scaffold/__init__.py delete mode 100644 libraries/cherrypy/scaffold/apache-fcgi.conf delete mode 100644 libraries/cherrypy/scaffold/example.conf delete mode 100644 libraries/cherrypy/scaffold/site.conf delete mode 100644 libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png delete mode 100644 libraries/cherrypy/test/__init__.py delete mode 100644 libraries/cherrypy/test/_test_decorators.py delete mode 100644 libraries/cherrypy/test/_test_states_demo.py delete mode 100644 libraries/cherrypy/test/benchmark.py delete mode 100644 libraries/cherrypy/test/checkerdemo.py delete mode 100644 libraries/cherrypy/test/fastcgi.conf delete mode 100644 libraries/cherrypy/test/fcgi.conf delete mode 100644 libraries/cherrypy/test/helper.py delete mode 100644 libraries/cherrypy/test/logtest.py delete mode 100644 libraries/cherrypy/test/modfastcgi.py delete mode 100644 libraries/cherrypy/test/modfcgid.py delete mode 100644 libraries/cherrypy/test/modpy.py delete mode 100644 libraries/cherrypy/test/modwsgi.py delete mode 100644 libraries/cherrypy/test/sessiondemo.py delete mode 100644 libraries/cherrypy/test/static/404.html delete mode 100644 libraries/cherrypy/test/static/dirback.jpg delete mode 100644 libraries/cherrypy/test/static/index.html delete mode 100644 libraries/cherrypy/test/style.css delete mode 100644 libraries/cherrypy/test/test.pem delete mode 100644 libraries/cherrypy/test/test_auth_basic.py delete mode 100644 libraries/cherrypy/test/test_auth_digest.py delete mode 100644 libraries/cherrypy/test/test_bus.py delete mode 100644 libraries/cherrypy/test/test_caching.py delete mode 100644 libraries/cherrypy/test/test_compat.py delete mode 100644 libraries/cherrypy/test/test_config.py delete mode 100644 libraries/cherrypy/test/test_config_server.py delete mode 100644 libraries/cherrypy/test/test_conn.py delete mode 100644 libraries/cherrypy/test/test_core.py delete mode 100644 libraries/cherrypy/test/test_dynamicobjectmapping.py delete mode 100644 libraries/cherrypy/test/test_encoding.py delete mode 100644 libraries/cherrypy/test/test_etags.py delete mode 100644 libraries/cherrypy/test/test_http.py delete mode 100644 libraries/cherrypy/test/test_httputil.py delete mode 100644 libraries/cherrypy/test/test_iterator.py delete mode 100644 libraries/cherrypy/test/test_json.py delete mode 100644 libraries/cherrypy/test/test_logging.py delete mode 100644 libraries/cherrypy/test/test_mime.py delete mode 100644 libraries/cherrypy/test/test_misc_tools.py delete mode 100644 libraries/cherrypy/test/test_native.py delete mode 100644 libraries/cherrypy/test/test_objectmapping.py delete mode 100644 libraries/cherrypy/test/test_params.py delete mode 100644 libraries/cherrypy/test/test_plugins.py delete mode 100644 libraries/cherrypy/test/test_proxy.py delete mode 100644 libraries/cherrypy/test/test_refleaks.py delete mode 100644 libraries/cherrypy/test/test_request_obj.py delete mode 100644 libraries/cherrypy/test/test_routes.py delete mode 100644 libraries/cherrypy/test/test_session.py delete mode 100644 libraries/cherrypy/test/test_sessionauthenticate.py delete mode 100644 libraries/cherrypy/test/test_states.py delete mode 100644 libraries/cherrypy/test/test_static.py delete mode 100644 libraries/cherrypy/test/test_tools.py delete mode 100644 libraries/cherrypy/test/test_tutorials.py delete mode 100644 libraries/cherrypy/test/test_virtualhost.py delete mode 100644 libraries/cherrypy/test/test_wsgi_ns.py delete mode 100644 libraries/cherrypy/test/test_wsgi_unix_socket.py delete mode 100644 libraries/cherrypy/test/test_wsgi_vhost.py delete mode 100644 libraries/cherrypy/test/test_wsgiapps.py delete mode 100644 libraries/cherrypy/test/test_xmlrpc.py delete mode 100644 libraries/cherrypy/test/webtest.py delete mode 100644 libraries/cherrypy/tutorial/README.rst delete mode 100644 libraries/cherrypy/tutorial/__init__.py delete mode 100644 libraries/cherrypy/tutorial/custom_error.html delete mode 100644 libraries/cherrypy/tutorial/pdf_file.pdf delete mode 100644 libraries/cherrypy/tutorial/tut01_helloworld.py delete mode 100644 libraries/cherrypy/tutorial/tut02_expose_methods.py delete mode 100644 libraries/cherrypy/tutorial/tut03_get_and_post.py delete mode 100644 libraries/cherrypy/tutorial/tut04_complex_site.py delete mode 100644 libraries/cherrypy/tutorial/tut05_derived_objects.py delete mode 100644 libraries/cherrypy/tutorial/tut06_default_method.py delete mode 100644 libraries/cherrypy/tutorial/tut07_sessions.py delete mode 100644 libraries/cherrypy/tutorial/tut08_generators_and_yield.py delete mode 100644 libraries/cherrypy/tutorial/tut09_files.py delete mode 100644 libraries/cherrypy/tutorial/tut10_http_errors.py delete mode 100644 libraries/cherrypy/tutorial/tutorial.conf delete mode 100644 libraries/contextlib2.py delete mode 100644 libraries/more_itertools/__init__.py delete mode 100644 libraries/more_itertools/more.py delete mode 100644 libraries/more_itertools/recipes.py delete mode 100644 libraries/more_itertools/tests/__init__.py delete mode 100644 libraries/more_itertools/tests/test_more.py delete mode 100644 libraries/more_itertools/tests/test_recipes.py delete mode 100644 libraries/portend.py delete mode 100644 libraries/tempora/__init__.py delete mode 100644 libraries/tempora/schedule.py delete mode 100644 libraries/tempora/tests/test_schedule.py delete mode 100644 libraries/tempora/timing.py delete mode 100644 libraries/tempora/utc.py delete mode 100644 libraries/zc/__init__.py delete mode 100644 libraries/zc/lockfile/README.txt delete mode 100644 libraries/zc/lockfile/__init__.py delete mode 100644 libraries/zc/lockfile/tests.py diff --git a/libraries/backports/__init__.py b/libraries/backports/__init__.py deleted file mode 100644 index 69e3be50..00000000 --- a/libraries/backports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/libraries/backports/functools_lru_cache.py b/libraries/backports/functools_lru_cache.py deleted file mode 100644 index 707c6c76..00000000 --- a/libraries/backports/functools_lru_cache.py +++ /dev/null @@ -1,184 +0,0 @@ -from __future__ import absolute_import - -import functools -from collections import namedtuple -from threading import RLock - -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) - - -@functools.wraps(functools.update_wrapper) -def update_wrapper(wrapper, - wrapped, - assigned = functools.WRAPPER_ASSIGNMENTS, - updated = functools.WRAPPER_UPDATES): - """ - Patch two bugs in functools.update_wrapper. - """ - # workaround for http://bugs.python.org/issue3445 - assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr)) - wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated) - # workaround for https://bugs.python.org/issue17482 - wrapper.__wrapped__ = wrapped - return wrapper - - -class _HashedSeq(list): - __slots__ = 'hashvalue' - - def __init__(self, tup, hash=hash): - self[:] = tup - self.hashvalue = hash(tup) - - def __hash__(self): - return self.hashvalue - - -def _make_key(args, kwds, typed, - kwd_mark=(object(),), - fasttypes=set([int, str, frozenset, type(None)]), - sorted=sorted, tuple=tuple, type=type, len=len): - 'Make a cache key from optionally typed positional and keyword arguments' - key = args - if kwds: - sorted_items = sorted(kwds.items()) - key += kwd_mark - for item in sorted_items: - key += item - if typed: - key += tuple(type(v) for v in args) - if kwds: - key += tuple(type(v) for k, v in sorted_items) - elif len(key) == 1 and type(key[0]) in fasttypes: - return key[0] - return _HashedSeq(key) - - -def lru_cache(maxsize=100, typed=False): - """Least-recently-used cache decorator. - - If *maxsize* is set to None, the LRU features are disabled and the cache - can grow without bound. - - If *typed* is True, arguments of different types will be cached separately. - For example, f(3.0) and f(3) will be treated as distinct calls with - distinct results. - - Arguments to the cached function must be hashable. - - View the cache statistics named tuple (hits, misses, maxsize, currsize) with - f.cache_info(). Clear the cache and statistics with f.cache_clear(). - Access the underlying function with f.__wrapped__. - - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - - """ - - # Users should only access the lru_cache through its public API: - # cache_info, cache_clear, and f.__wrapped__ - # The internals of the lru_cache are encapsulated for thread safety and - # to allow the implementation to change (including a possible C version). - - def decorating_function(user_function): - - cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields - make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields - - if maxsize == 0: - - def wrapper(*args, **kwds): - # no caching, just do a statistics update after a successful call - result = user_function(*args, **kwds) - stats[MISSES] += 1 - return result - - elif maxsize is None: - - def wrapper(*args, **kwds): - # simple caching without ordering or size limit - key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel - if result is not root: - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - cache[key] = result - stats[MISSES] += 1 - return result - - else: - - def wrapper(*args, **kwds): - # size limited caching that tracks accesses by recency - key = make_key(args, kwds, typed) if kwds or typed else args - with lock: - link = cache_get(key) - if link is not None: - # record recent use of the key by moving it to the front of the list - root, = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - with lock: - root, = nonlocal_root - if key in cache: - # getting here means that this same key was added to the - # cache while the lock was released. since the link - # update is already done, we need only return the - # computed result and update the count of misses. - pass - elif _len(cache) >= maxsize: - # use the old root to store the new key and result - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - # empty the oldest link and make it the new root - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - root[KEY] = root[RESULT] = None - # now update the cache dictionary for the new links - del cache[oldkey] - cache[key] = oldroot - else: - # put result in a new link at the front of the list - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - return result - - def cache_info(): - """Report cache statistics""" - with lock: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) - - def cache_clear(): - """Clear the cache and cache statistics""" - with lock: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return update_wrapper(wrapper, user_function) - - return decorating_function diff --git a/libraries/cheroot/__init__.py b/libraries/cheroot/__init__.py deleted file mode 100644 index a313660e..00000000 --- a/libraries/cheroot/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""High-performance, pure-Python HTTP server used by CherryPy.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -__version__ = '6.4.0' diff --git a/libraries/cheroot/__main__.py b/libraries/cheroot/__main__.py deleted file mode 100644 index d2e27c10..00000000 --- a/libraries/cheroot/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Stub for accessing the Cheroot CLI tool.""" - -from .cli import main - -if __name__ == '__main__': - main() diff --git a/libraries/cheroot/_compat.py b/libraries/cheroot/_compat.py deleted file mode 100644 index e98f91f9..00000000 --- a/libraries/cheroot/_compat.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Compatibility code for using Cheroot with various versions of Python.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import re - -import six - -if six.PY3: - def ntob(n, encoding='ISO-8859-1'): - """Return the native string as bytes in the given encoding.""" - assert_native(n) - # In Python 3, the native string type is unicode - return n.encode(encoding) - - def ntou(n, encoding='ISO-8859-1'): - """Return the native string as unicode with the given encoding.""" - assert_native(n) - # In Python 3, the native string type is unicode - return n - - def bton(b, encoding='ISO-8859-1'): - """Return the byte string as native string in the given encoding.""" - return b.decode(encoding) -else: - # Python 2 - def ntob(n, encoding='ISO-8859-1'): - """Return the native string as bytes in the given encoding.""" - assert_native(n) - # In Python 2, the native string type is bytes. Assume it's already - # in the given encoding, which for ISO-8859-1 is almost always what - # was intended. - return n - - def ntou(n, encoding='ISO-8859-1'): - """Return the native string as unicode with the given encoding.""" - assert_native(n) - # In Python 2, the native string type is bytes. - # First, check for the special encoding 'escape'. The test suite uses - # this to signal that it wants to pass a string with embedded \uXXXX - # escapes, but without having to prefix it with u'' for Python 2, - # but no prefix for Python 3. - if encoding == 'escape': - return six.u( - re.sub(r'\\u([0-9a-zA-Z]{4})', - lambda m: six.unichr(int(m.group(1), 16)), - n.decode('ISO-8859-1'))) - # Assume it's already in the given encoding, which for ISO-8859-1 - # is almost always what was intended. - return n.decode(encoding) - - def bton(b, encoding='ISO-8859-1'): - """Return the byte string as native string in the given encoding.""" - return b - - -def assert_native(n): - """Check whether the input is of nativ ``str`` type. - - Raises: - TypeError: in case of failed check - - """ - if not isinstance(n, str): - raise TypeError('n must be a native str (got %s)' % type(n).__name__) diff --git a/libraries/cheroot/cli.py b/libraries/cheroot/cli.py deleted file mode 100644 index 6d59fb5c..00000000 --- a/libraries/cheroot/cli.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Command line tool for starting a Cheroot WSGI/HTTP server instance. - -Basic usage:: - - # Start a server on 127.0.0.1:8000 with the default settings - # for the WSGI app myapp/wsgi.py:application() - cheroot myapp.wsgi - - # Start a server on 0.0.0.0:9000 with 8 threads - # for the WSGI app myapp/wsgi.py:main_app() - cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8 - - # Start a server for the cheroot.server.Gateway subclass - # myapp/gateway.py:HTTPGateway - cheroot myapp.gateway:HTTPGateway - - # Start a server on the UNIX socket /var/spool/myapp.sock - cheroot myapp.wsgi --bind /var/spool/myapp.sock - - # Start a server on the abstract UNIX socket CherootServer - cheroot myapp.wsgi --bind @CherootServer -""" - -import argparse -from importlib import import_module -import os -import sys -import contextlib - -import six - -from . import server -from . import wsgi - - -__metaclass__ = type - - -class BindLocation: - """A class for storing the bind location for a Cheroot instance.""" - - -class TCPSocket(BindLocation): - """TCPSocket.""" - - def __init__(self, address, port): - """Initialize. - - Args: - address (str): Host name or IP address - port (int): TCP port number - """ - self.bind_addr = address, port - - -class UnixSocket(BindLocation): - """UnixSocket.""" - - def __init__(self, path): - """Initialize.""" - self.bind_addr = path - - -class AbstractSocket(BindLocation): - """AbstractSocket.""" - - def __init__(self, addr): - """Initialize.""" - self.bind_addr = '\0{}'.format(self.abstract_socket) - - -class Application: - """Application.""" - - @classmethod - def resolve(cls, full_path): - """Read WSGI app/Gateway path string and import application module.""" - mod_path, _, app_path = full_path.partition(':') - app = getattr(import_module(mod_path), app_path or 'application') - - with contextlib.suppress(TypeError): - if issubclass(app, server.Gateway): - return GatewayYo(app) - - return cls(app) - - def __init__(self, wsgi_app): - """Initialize.""" - if not callable(wsgi_app): - raise TypeError( - 'Application must be a callable object or ' - 'cheroot.server.Gateway subclass' - ) - self.wsgi_app = wsgi_app - - def server_args(self, parsed_args): - """Return keyword args for Server class.""" - args = { - arg: value - for arg, value in vars(parsed_args).items() - if not arg.startswith('_') and value is not None - } - args.update(vars(self)) - return args - - def server(self, parsed_args): - """Server.""" - return wsgi.Server(**self.server_args(parsed_args)) - - -class GatewayYo: - """Gateway.""" - - def __init__(self, gateway): - """Init.""" - self.gateway = gateway - - def server(self, parsed_args): - """Server.""" - server_args = vars(self) - server_args['bind_addr'] = parsed_args['bind_addr'] - if parsed_args.max is not None: - server_args['maxthreads'] = parsed_args.max - if parsed_args.numthreads is not None: - server_args['minthreads'] = parsed_args.numthreads - return server.HTTPServer(**server_args) - - -def parse_wsgi_bind_location(bind_addr_string): - """Convert bind address string to a BindLocation.""" - # try and match for an IP/hostname and port - match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string)) - try: - addr = match.hostname - port = match.port - if addr is not None or port is not None: - return TCPSocket(addr, port) - except ValueError: - pass - - # else, assume a UNIX socket path - # if the string begins with an @ symbol, use an abstract socket - if bind_addr_string.startswith('@'): - return AbstractSocket(bind_addr_string[1:]) - return UnixSocket(path=bind_addr_string) - - -def parse_wsgi_bind_addr(bind_addr_string): - """Convert bind address string to bind address parameter.""" - return parse_wsgi_bind_location(bind_addr_string).bind_addr - - -_arg_spec = { - '_wsgi_app': dict( - metavar='APP_MODULE', - type=Application.resolve, - help='WSGI application callable or cheroot.server.Gateway subclass', - ), - '--bind': dict( - metavar='ADDRESS', - dest='bind_addr', - type=parse_wsgi_bind_addr, - default='[::1]:8000', - help='Network interface to listen on (default: [::1]:8000)', - ), - '--chdir': dict( - metavar='PATH', - type=os.chdir, - help='Set the working directory', - ), - '--server-name': dict( - dest='server_name', - type=str, - help='Web server name to be advertised via Server HTTP header', - ), - '--threads': dict( - metavar='INT', - dest='numthreads', - type=int, - help='Minimum number of worker threads', - ), - '--max-threads': dict( - metavar='INT', - dest='max', - type=int, - help='Maximum number of worker threads', - ), - '--timeout': dict( - metavar='INT', - dest='timeout', - type=int, - help='Timeout in seconds for accepted connections', - ), - '--shutdown-timeout': dict( - metavar='INT', - dest='shutdown_timeout', - type=int, - help='Time in seconds to wait for worker threads to cleanly exit', - ), - '--request-queue-size': dict( - metavar='INT', - dest='request_queue_size', - type=int, - help='Maximum number of queued connections', - ), - '--accepted-queue-size': dict( - metavar='INT', - dest='accepted_queue_size', - type=int, - help='Maximum number of active requests in queue', - ), - '--accepted-queue-timeout': dict( - metavar='INT', - dest='accepted_queue_timeout', - type=int, - help='Timeout in seconds for putting requests into queue', - ), -} - - -def main(): - """Create a new Cheroot instance with arguments from the command line.""" - parser = argparse.ArgumentParser( - description='Start an instance of the Cheroot WSGI/HTTP server.') - for arg, spec in _arg_spec.items(): - parser.add_argument(arg, **spec) - raw_args = parser.parse_args() - - # ensure cwd in sys.path - '' in sys.path or sys.path.insert(0, '') - - # create a server based on the arguments provided - raw_args._wsgi_app.server(raw_args).safe_start() diff --git a/libraries/cheroot/errors.py b/libraries/cheroot/errors.py deleted file mode 100644 index 82412b42..00000000 --- a/libraries/cheroot/errors.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Collection of exceptions raised and/or processed by Cheroot.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import errno -import sys - - -class MaxSizeExceeded(Exception): - """Exception raised when a client sends more data then acceptable within limit. - - Depends on ``request.body.maxbytes`` config option if used within CherryPy - """ - - -class NoSSLError(Exception): - """Exception raised when a client speaks HTTP to an HTTPS socket.""" - - -class FatalSSLAlert(Exception): - """Exception raised when the SSL implementation signals a fatal alert.""" - - -def plat_specific_errors(*errnames): - """Return error numbers for all errors in errnames on this platform. - - The 'errno' module contains different global constants depending on - the specific platform (OS). This function will return the list of - numeric values for a given list of potential names. - """ - errno_names = dir(errno) - nums = [getattr(errno, k) for k in errnames if k in errno_names] - # de-dupe the list - return list(dict.fromkeys(nums).keys()) - - -socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR') - -socket_errors_to_ignore = plat_specific_errors( - 'EPIPE', - 'EBADF', 'WSAEBADF', - 'ENOTSOCK', 'WSAENOTSOCK', - 'ETIMEDOUT', 'WSAETIMEDOUT', - 'ECONNREFUSED', 'WSAECONNREFUSED', - 'ECONNRESET', 'WSAECONNRESET', - 'ECONNABORTED', 'WSAECONNABORTED', - 'ENETRESET', 'WSAENETRESET', - 'EHOSTDOWN', 'EHOSTUNREACH', -) -socket_errors_to_ignore.append('timed out') -socket_errors_to_ignore.append('The read operation timed out') -socket_errors_nonblocking = plat_specific_errors( - 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') - -if sys.platform == 'darwin': - socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE')) - socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE')) diff --git a/libraries/cheroot/makefile.py b/libraries/cheroot/makefile.py deleted file mode 100644 index a76f2eda..00000000 --- a/libraries/cheroot/makefile.py +++ /dev/null @@ -1,387 +0,0 @@ -"""Socket file object.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import socket - -try: - # prefer slower Python-based io module - import _pyio as io -except ImportError: - # Python 2.6 - import io - -import six - -from . import errors - - -class BufferedWriter(io.BufferedWriter): - """Faux file object attached to a socket object.""" - - def write(self, b): - """Write bytes to buffer.""" - self._checkClosed() - if isinstance(b, str): - raise TypeError("can't write str to binary stream") - - with self._write_lock: - self._write_buf.extend(b) - self._flush_unlocked() - return len(b) - - def _flush_unlocked(self): - self._checkClosed('flush of closed file') - while self._write_buf: - try: - # ssl sockets only except 'bytes', not bytearrays - # so perhaps we should conditionally wrap this for perf? - n = self.raw.write(bytes(self._write_buf)) - except io.BlockingIOError as e: - n = e.characters_written - del self._write_buf[:n] - - -def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): - """File object attached to a socket object.""" - if 'r' in mode: - return io.BufferedReader(socket.SocketIO(sock, mode), bufsize) - else: - return BufferedWriter(socket.SocketIO(sock, mode), bufsize) - - -class MakeFile_PY2(getattr(socket, '_fileobject', object)): - """Faux file object attached to a socket object.""" - - def __init__(self, *args, **kwargs): - """Initialize faux file object.""" - self.bytes_read = 0 - self.bytes_written = 0 - socket._fileobject.__init__(self, *args, **kwargs) - - def write(self, data): - """Sendall for non-blocking sockets.""" - while data: - try: - bytes_sent = self.send(data) - data = data[bytes_sent:] - except socket.error as e: - if e.args[0] not in errors.socket_errors_nonblocking: - raise - - def send(self, data): - """Send some part of message to the socket.""" - bytes_sent = self._sock.send(data) - self.bytes_written += bytes_sent - return bytes_sent - - def flush(self): - """Write all data from buffer to socket and reset write buffer.""" - if self._wbuf: - buffer = ''.join(self._wbuf) - self._wbuf = [] - self.write(buffer) - - def recv(self, size): - """Receive message of a size from the socket.""" - while True: - try: - data = self._sock.recv(size) - self.bytes_read += len(data) - return data - except socket.error as e: - what = ( - e.args[0] not in errors.socket_errors_nonblocking - and e.args[0] not in errors.socket_error_eintr - ) - if what: - raise - - class FauxSocket: - """Faux socket with the minimal interface required by pypy.""" - - def _reuse(self): - pass - - _fileobject_uses_str_type = six.PY2 and isinstance( - socket._fileobject(FauxSocket())._rbuf, six.string_types) - - # FauxSocket is no longer needed - del FauxSocket - - if not _fileobject_uses_str_type: - def read(self, size=-1): - """Read data from the socket to buffer.""" - # Use max, disallow tiny reads in a loop as they are very - # inefficient. - # We never leave read() with any leftover data from a new recv() - # call in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned - # by recv() minimizes memory usage and fragmentation that occurs - # when rbufsize is large compared to the typical return value of - # recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - if size < 0: - # Read until EOF - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(rbufsize) - if not data: - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and - # return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return rv - - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - data = self.recv(left) - if not data: - break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, 'recv(%d) returned %d bytes' % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - # assert buf_len == buf.tell() - return buf.getvalue() - - def readline(self, size=-1): - """Read line from the socket to buffer.""" - buf = self._rbuf - buf.seek(0, 2) # seek end - if buf.tell() > 0: - # check if we already have it in our buffer - buf.seek(0) - bline = buf.readline(size) - if bline.endswith('\n') or len(bline) == size: - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return bline - del bline - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - buf.seek(0) - buffers = [buf.read()] - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - data = None - recv = self.recv - while data != '\n': - data = recv(1) - if not data: - break - buffers.append(data) - return ''.join(buffers) - - buf.seek(0, 2) # seek end - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(self._rbufsize) - if not data: - break - nl = data.find('\n') - if nl >= 0: - nl += 1 - buf.write(data[:nl]) - self._rbuf.write(data[nl:]) - del data - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or \n or EOF seen, whichever comes - # first - buf.seek(0, 2) # seek end - buf_len = buf.tell() - if buf_len >= size: - buf.seek(0) - rv = buf.read(size) - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return rv - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(self._rbufsize) - if not data: - break - left = size - buf_len - # did we just receive a newline? - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - # save the excess data to _rbuf - self._rbuf.write(data[nl:]) - if buf_len: - buf.write(data[:nl]) - break - else: - # Shortcut. Avoid data copy through buf when - # returning a substring of our first recv(). - return data[:nl] - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid data copy through buf when - # returning exactly all of our first recv(). - return data - if n >= left: - buf.write(data[:left]) - self._rbuf.write(data[left:]) - break - buf.write(data) - buf_len += n - # assert buf_len == buf.tell() - return buf.getvalue() - else: - def read(self, size=-1): - """Read data from the socket to buffer.""" - if size < 0: - # Read until EOF - buffers = [self._rbuf] - self._rbuf = '' - if self._rbufsize <= 1: - recv_size = self.default_bufsize - else: - recv_size = self._rbufsize - - while True: - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - return ''.join(buffers) - else: - # Read until size bytes or EOF seen, whichever comes first - data = self._rbuf - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - left = size - buf_len - recv_size = max(self._rbufsize, left) - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return ''.join(buffers) - - def readline(self, size=-1): - """Read line from the socket to buffer.""" - data = self._rbuf - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - assert data == '' - buffers = [] - while data != '\n': - data = self.recv(1) - if not data: - break - buffers.append(data) - return ''.join(buffers) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - return ''.join(buffers) - else: - # Read until size bytes or \n or EOF seen, whichever comes - # first - nl = data.find('\n', 0, size) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - left = size - buf_len - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return ''.join(buffers) - - -MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3 diff --git a/libraries/cheroot/server.py b/libraries/cheroot/server.py deleted file mode 100644 index 44070490..00000000 --- a/libraries/cheroot/server.py +++ /dev/null @@ -1,2001 +0,0 @@ -""" -A high-speed, production ready, thread pooled, generic HTTP server. - -For those of you wanting to understand internals of this module, here's the -basic call flow. The server's listening thread runs a very tight loop, -sticking incoming connections onto a Queue:: - - server = HTTPServer(...) - server.start() - -> while True: - tick() - # This blocks until a request comes in: - child = socket.accept() - conn = HTTPConnection(child, ...) - server.requests.put(conn) - -Worker threads are kept in a pool and poll the Queue, popping off and then -handling each connection in turn. Each connection can consist of an arbitrary -number of requests and their responses, so we run a nested loop:: - - while True: - conn = server.requests.get() - conn.communicate() - -> while True: - req = HTTPRequest(...) - req.parse_request() - -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" - req.rfile.readline() - read_headers(req.rfile, req.inheaders) - req.respond() - -> response = app(...) - try: - for chunk in response: - if chunk: - req.write(chunk) - finally: - if hasattr(response, "close"): - response.close() - if req.close_connection: - return - -For running a server you can invoke :func:`start() <HTTPServer.start()>` (it -will run the server forever) or use invoking :func:`prepare() -<HTTPServer.prepare()>` and :func:`serve() <HTTPServer.serve()>` like this:: - - server = HTTPServer(...) - server.prepare() - try: - threading.Thread(target=server.serve).start() - - # waiting/detecting some appropriate stop condition here - ... - - finally: - server.stop() - -And now for a trivial doctest to exercise the test suite - ->>> 'HTTPServer' in globals() -True - -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import os -import io -import re -import email.utils -import socket -import sys -import time -import traceback as traceback_ -import logging -import platform -import xbmc - -try: - from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache - -import six -from six.moves import queue -from six.moves import urllib - -from . import errors, __version__ -from ._compat import bton, ntou -from .workers import threadpool -from .makefile import MakeFile - - -__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer', - 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', - 'Gateway', 'get_ssl_adapter_class') - -""" -Special KODI case: -Android does not have support for grp and pwd -But Python has issues reporting that this is running on Android (it shows as Linux2). -We're instead using xbmc library to detect that. -""" -IS_WINDOWS = platform.system() == 'Windows' -IS_ANDROID = xbmc.getCondVisibility('system.platform.linux') and xbmc.getCondVisibility('system.platform.android') - -if not (IS_WINDOWS or IS_ANDROID): - import grp - import pwd - import struct - - -if IS_WINDOWS and hasattr(socket, 'AF_INET6'): - if not hasattr(socket, 'IPPROTO_IPV6'): - socket.IPPROTO_IPV6 = 41 - if not hasattr(socket, 'IPV6_V6ONLY'): - socket.IPV6_V6ONLY = 27 - - -if not hasattr(socket, 'SO_PEERCRED'): - """ - NOTE: the value for SO_PEERCRED can be architecture specific, in - which case the getsockopt() will hopefully fail. The arch - specific value could be derived from platform.processor() - """ - socket.SO_PEERCRED = 17 - - -LF = b'\n' -CRLF = b'\r\n' -TAB = b'\t' -SPACE = b' ' -COLON = b':' -SEMICOLON = b';' -EMPTY = b'' -ASTERISK = b'*' -FORWARD_SLASH = b'/' -QUOTED_SLASH = b'%2F' -QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH) - - -comma_separated_headers = [ - b'Accept', b'Accept-Charset', b'Accept-Encoding', - b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control', - b'Connection', b'Content-Encoding', b'Content-Language', b'Expect', - b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE', - b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning', - b'WWW-Authenticate', -] - - -if not hasattr(logging, 'statistics'): - logging.statistics = {} - - -class HeaderReader: - """Object for reading headers from an HTTP request. - - Interface and default implementation. - """ - - def __call__(self, rfile, hdict=None): - """ - Read headers from the given stream into the given header dict. - - If hdict is None, a new header dict is created. Returns the populated - header dict. - - Headers which are repeated are folded together using a comma if their - specification so dictates. - - This function raises ValueError when the read bytes violate the HTTP - spec. - You should probably return "400 Bad Request" if this happens. - """ - if hdict is None: - hdict = {} - - while True: - line = rfile.readline() - if not line: - # No more data--illegal end of headers - raise ValueError('Illegal end of headers.') - - if line == CRLF: - # Normal end of headers - break - if not line.endswith(CRLF): - raise ValueError('HTTP requires CRLF terminators') - - if line[0] in (SPACE, TAB): - # It's a continuation line. - v = line.strip() - else: - try: - k, v = line.split(COLON, 1) - except ValueError: - raise ValueError('Illegal header line.') - v = v.strip() - k = self._transform_key(k) - hname = k - - if not self._allow_header(k): - continue - - if k in comma_separated_headers: - existing = hdict.get(hname) - if existing: - v = b', '.join((existing, v)) - hdict[hname] = v - - return hdict - - def _allow_header(self, key_name): - return True - - def _transform_key(self, key_name): - # TODO: what about TE and WWW-Authenticate? - return key_name.strip().title() - - -class DropUnderscoreHeaderReader(HeaderReader): - """Custom HeaderReader to exclude any headers with underscores in them.""" - - def _allow_header(self, key_name): - orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name) - return orig and '_' not in key_name - - -class SizeCheckWrapper: - """Wraps a file-like object, raising MaxSizeExceeded if too large.""" - - def __init__(self, rfile, maxlen): - """Initialize SizeCheckWrapper instance. - - Args: - rfile (file): file of a limited size - maxlen (int): maximum length of the file being read - """ - self.rfile = rfile - self.maxlen = maxlen - self.bytes_read = 0 - - def _check_length(self): - if self.maxlen and self.bytes_read > self.maxlen: - raise errors.MaxSizeExceeded() - - def read(self, size=None): - """Read a chunk from rfile buffer and return it. - - Args: - size (int): amount of data to read - - Returns: - bytes: Chunk from rfile, limited by size if specified. - - """ - data = self.rfile.read(size) - self.bytes_read += len(data) - self._check_length() - return data - - def readline(self, size=None): - """Read a single line from rfile buffer and return it. - - Args: - size (int): minimum amount of data to read - - Returns: - bytes: One line from rfile. - - """ - if size is not None: - data = self.rfile.readline(size) - self.bytes_read += len(data) - self._check_length() - return data - - # User didn't specify a size ... - # We read the line in chunks to make sure it's not a 100MB line ! - res = [] - while True: - data = self.rfile.readline(256) - self.bytes_read += len(data) - self._check_length() - res.append(data) - # See https://github.com/cherrypy/cherrypy/issues/421 - if len(data) < 256 or data[-1:] == LF: - return EMPTY.join(res) - - def readlines(self, sizehint=0): - """Read all lines from rfile buffer and return them. - - Args: - sizehint (int): hint of minimum amount of data to read - - Returns: - list[bytes]: Lines of bytes read from rfile. - - """ - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline(sizehint) - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline(sizehint) - return lines - - def close(self): - """Release resources allocated for rfile.""" - self.rfile.close() - - def __iter__(self): - """Return file iterator.""" - return self - - def __next__(self): - """Generate next file chunk.""" - data = next(self.rfile) - self.bytes_read += len(data) - self._check_length() - return data - - next = __next__ - - -class KnownLengthRFile: - """Wraps a file-like object, returning an empty string when exhausted.""" - - def __init__(self, rfile, content_length): - """Initialize KnownLengthRFile instance. - - Args: - rfile (file): file of a known size - content_length (int): length of the file being read - - """ - self.rfile = rfile - self.remaining = content_length - - def read(self, size=None): - """Read a chunk from rfile buffer and return it. - - Args: - size (int): amount of data to read - - Returns: - bytes: Chunk from rfile, limited by size if specified. - - """ - if self.remaining == 0: - return b'' - if size is None: - size = self.remaining - else: - size = min(size, self.remaining) - - data = self.rfile.read(size) - self.remaining -= len(data) - return data - - def readline(self, size=None): - """Read a single line from rfile buffer and return it. - - Args: - size (int): minimum amount of data to read - - Returns: - bytes: One line from rfile. - - """ - if self.remaining == 0: - return b'' - if size is None: - size = self.remaining - else: - size = min(size, self.remaining) - - data = self.rfile.readline(size) - self.remaining -= len(data) - return data - - def readlines(self, sizehint=0): - """Read all lines from rfile buffer and return them. - - Args: - sizehint (int): hint of minimum amount of data to read - - Returns: - list[bytes]: Lines of bytes read from rfile. - - """ - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline(sizehint) - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline(sizehint) - return lines - - def close(self): - """Release resources allocated for rfile.""" - self.rfile.close() - - def __iter__(self): - """Return file iterator.""" - return self - - def __next__(self): - """Generate next file chunk.""" - data = next(self.rfile) - self.remaining -= len(data) - return data - - next = __next__ - - -class ChunkedRFile: - """Wraps a file-like object, returning an empty string when exhausted. - - This class is intended to provide a conforming wsgi.input value for - request entities that have been encoded with the 'chunked' transfer - encoding. - """ - - def __init__(self, rfile, maxlen, bufsize=8192): - """Initialize ChunkedRFile instance. - - Args: - rfile (file): file encoded with the 'chunked' transfer encoding - maxlen (int): maximum length of the file being read - bufsize (int): size of the buffer used to read the file - """ - self.rfile = rfile - self.maxlen = maxlen - self.bytes_read = 0 - self.buffer = EMPTY - self.bufsize = bufsize - self.closed = False - - def _fetch(self): - if self.closed: - return - - line = self.rfile.readline() - self.bytes_read += len(line) - - if self.maxlen and self.bytes_read > self.maxlen: - raise errors.MaxSizeExceeded( - 'Request Entity Too Large', self.maxlen) - - line = line.strip().split(SEMICOLON, 1) - - try: - chunk_size = line.pop(0) - chunk_size = int(chunk_size, 16) - except ValueError: - raise ValueError('Bad chunked transfer size: ' + repr(chunk_size)) - - if chunk_size <= 0: - self.closed = True - return - -# if line: chunk_extension = line[0] - - if self.maxlen and self.bytes_read + chunk_size > self.maxlen: - raise IOError('Request Entity Too Large') - - chunk = self.rfile.read(chunk_size) - self.bytes_read += len(chunk) - self.buffer += chunk - - crlf = self.rfile.read(2) - if crlf != CRLF: - raise ValueError( - "Bad chunked transfer coding (expected '\\r\\n', " - 'got ' + repr(crlf) + ')') - - def read(self, size=None): - """Read a chunk from rfile buffer and return it. - - Args: - size (int): amount of data to read - - Returns: - bytes: Chunk from rfile, limited by size if specified. - - """ - data = EMPTY - - if size == 0: - return data - - while True: - if size and len(data) >= size: - return data - - if not self.buffer: - self._fetch() - if not self.buffer: - # EOF - return data - - if size: - remaining = size - len(data) - data += self.buffer[:remaining] - self.buffer = self.buffer[remaining:] - else: - data += self.buffer - self.buffer = EMPTY - - def readline(self, size=None): - """Read a single line from rfile buffer and return it. - - Args: - size (int): minimum amount of data to read - - Returns: - bytes: One line from rfile. - - """ - data = EMPTY - - if size == 0: - return data - - while True: - if size and len(data) >= size: - return data - - if not self.buffer: - self._fetch() - if not self.buffer: - # EOF - return data - - newline_pos = self.buffer.find(LF) - if size: - if newline_pos == -1: - remaining = size - len(data) - data += self.buffer[:remaining] - self.buffer = self.buffer[remaining:] - else: - remaining = min(size - len(data), newline_pos) - data += self.buffer[:remaining] - self.buffer = self.buffer[remaining:] - else: - if newline_pos == -1: - data += self.buffer - self.buffer = EMPTY - else: - data += self.buffer[:newline_pos] - self.buffer = self.buffer[newline_pos:] - - def readlines(self, sizehint=0): - """Read all lines from rfile buffer and return them. - - Args: - sizehint (int): hint of minimum amount of data to read - - Returns: - list[bytes]: Lines of bytes read from rfile. - - """ - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline(sizehint) - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline(sizehint) - return lines - - def read_trailer_lines(self): - """Read HTTP headers and yield them. - - Returns: - Generator: yields CRLF separated lines. - - """ - if not self.closed: - raise ValueError( - 'Cannot read trailers until the request body has been read.') - - while True: - line = self.rfile.readline() - if not line: - # No more data--illegal end of headers - raise ValueError('Illegal end of headers.') - - self.bytes_read += len(line) - if self.maxlen and self.bytes_read > self.maxlen: - raise IOError('Request Entity Too Large') - - if line == CRLF: - # Normal end of headers - break - if not line.endswith(CRLF): - raise ValueError('HTTP requires CRLF terminators') - - yield line - - def close(self): - """Release resources allocated for rfile.""" - self.rfile.close() - - -class HTTPRequest: - """An HTTP Request (and response). - - A single HTTP connection may consist of multiple request/response pairs. - """ - - server = None - """The HTTPServer object which is receiving this request.""" - - conn = None - """The HTTPConnection object on which this request connected.""" - - inheaders = {} - """A dict of request headers.""" - - outheaders = [] - """A list of header tuples to write in the response.""" - - ready = False - """When True, the request has been parsed and is ready to begin generating - the response. When False, signals the calling Connection that the response - should not be generated and the connection should close.""" - - close_connection = False - """Signals the calling Connection that the request should close. This does - not imply an error! The client and/or server may each request that the - connection be closed.""" - - chunked_write = False - """If True, output will be encoded with the "chunked" transfer-coding. - - This value is set automatically inside send_headers.""" - - header_reader = HeaderReader() - """ - A HeaderReader instance or compatible reader. - """ - - def __init__(self, server, conn, proxy_mode=False, strict_mode=True): - """Initialize HTTP request container instance. - - Args: - server (HTTPServer): web server object receiving this request - conn (HTTPConnection): HTTP connection object for this request - proxy_mode (bool): whether this HTTPServer should behave as a PROXY - server for certain requests - strict_mode (bool): whether we should return a 400 Bad Request when - we encounter a request that a HTTP compliant client should not be - making - """ - self.server = server - self.conn = conn - - self.ready = False - self.started_request = False - self.scheme = b'http' - if self.server.ssl_adapter is not None: - self.scheme = b'https' - # Use the lowest-common protocol in case read_request_line errors. - self.response_protocol = 'HTTP/1.0' - self.inheaders = {} - - self.status = '' - self.outheaders = [] - self.sent_headers = False - self.close_connection = self.__class__.close_connection - self.chunked_read = False - self.chunked_write = self.__class__.chunked_write - self.proxy_mode = proxy_mode - self.strict_mode = strict_mode - - def parse_request(self): - """Parse the next HTTP request start-line and message-headers.""" - self.rfile = SizeCheckWrapper(self.conn.rfile, - self.server.max_request_header_size) - try: - success = self.read_request_line() - except errors.MaxSizeExceeded: - self.simple_response( - '414 Request-URI Too Long', - 'The Request-URI sent with the request exceeds the maximum ' - 'allowed bytes.') - return - else: - if not success: - return - - try: - success = self.read_request_headers() - except errors.MaxSizeExceeded: - self.simple_response( - '413 Request Entity Too Large', - 'The headers sent with the request exceed the maximum ' - 'allowed bytes.') - return - else: - if not success: - return - - self.ready = True - - def read_request_line(self): - """Read and parse first line of the HTTP request. - - Returns: - bool: True if the request line is valid or False if it's malformed. - - """ - # HTTP/1.1 connections are persistent by default. If a client - # requests a page, then idles (leaves the connection open), - # then rfile.readline() will raise socket.error("timed out"). - # Note that it does this based on the value given to settimeout(), - # and doesn't need the client to request or acknowledge the close - # (although your TCP stack might suffer for it: cf Apache's history - # with FIN_WAIT_2). - request_line = self.rfile.readline() - - # Set started_request to True so communicate() knows to send 408 - # from here on out. - self.started_request = True - if not request_line: - return False - - if request_line == CRLF: - # RFC 2616 sec 4.1: "...if the server is reading the protocol - # stream at the beginning of a message and receives a CRLF - # first, it should ignore the CRLF." - # But only ignore one leading line! else we enable a DoS. - request_line = self.rfile.readline() - if not request_line: - return False - - if not request_line.endswith(CRLF): - self.simple_response( - '400 Bad Request', 'HTTP requires CRLF terminators') - return False - - try: - method, uri, req_protocol = request_line.strip().split(SPACE, 2) - if not req_protocol.startswith(b'HTTP/'): - self.simple_response( - '400 Bad Request', 'Malformed Request-Line: bad protocol' - ) - return False - rp = req_protocol[5:].split(b'.', 1) - rp = tuple(map(int, rp)) # Minor.Major must be threat as integers - if rp > (1, 1): - self.simple_response( - '505 HTTP Version Not Supported', 'Cannot fulfill request' - ) - return False - except (ValueError, IndexError): - self.simple_response('400 Bad Request', 'Malformed Request-Line') - return False - - self.uri = uri - self.method = method.upper() - - if self.strict_mode and method != self.method: - resp = ( - 'Malformed method name: According to RFC 2616 ' - '(section 5.1.1) and its successors ' - 'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) ' - 'method names are case-sensitive and uppercase.' - ) - self.simple_response('400 Bad Request', resp) - return False - - try: - if six.PY2: # FIXME: Figure out better way to do this - # Ref: https://stackoverflow.com/a/196392/595220 (like this?) - """This is a dummy check for unicode in URI.""" - ntou(bton(uri, 'ascii'), 'ascii') - scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) - except UnicodeError: - self.simple_response('400 Bad Request', 'Malformed Request-URI') - return False - - if self.method == b'OPTIONS': - # TODO: cover this branch with tests - path = (uri - # https://tools.ietf.org/html/rfc7230#section-5.3.4 - if self.proxy_mode or uri == ASTERISK - else path) - elif self.method == b'CONNECT': - # TODO: cover this branch with tests - if not self.proxy_mode: - self.simple_response('405 Method Not Allowed') - return False - - # `urlsplit()` above parses "example.com:3128" as path part of URI. - # this is a workaround, which makes it detect netloc correctly - uri_split = urllib.parse.urlsplit(b'//' + uri) - _scheme, _authority, _path, _qs, _fragment = uri_split - _port = EMPTY - try: - _port = uri_split.port - except ValueError: - pass - - # FIXME: use third-party validation to make checks against RFC - # the validation doesn't take into account, that urllib parses - # invalid URIs without raising errors - # https://tools.ietf.org/html/rfc7230#section-5.3.3 - invalid_path = ( - _authority != uri - or not _port - or any((_scheme, _path, _qs, _fragment)) - ) - if invalid_path: - self.simple_response('400 Bad Request', - 'Invalid path in Request-URI: request-' - 'target must match authority-form.') - return False - - authority = path = _authority - scheme = qs = fragment = EMPTY - else: - uri_is_absolute_form = (scheme or authority) - - disallowed_absolute = ( - self.strict_mode - and not self.proxy_mode - and uri_is_absolute_form - ) - if disallowed_absolute: - # https://tools.ietf.org/html/rfc7230#section-5.3.2 - # (absolute form) - """Absolute URI is only allowed within proxies.""" - self.simple_response( - '400 Bad Request', - 'Absolute URI not allowed if server is not a proxy.', - ) - return False - - invalid_path = ( - self.strict_mode - and not uri.startswith(FORWARD_SLASH) - and not uri_is_absolute_form - ) - if invalid_path: - # https://tools.ietf.org/html/rfc7230#section-5.3.1 - # (origin_form) and - """Path should start with a forward slash.""" - resp = ( - 'Invalid path in Request-URI: request-target must contain ' - 'origin-form which starts with absolute-path (URI ' - 'starting with a slash "/").' - ) - self.simple_response('400 Bad Request', resp) - return False - - if fragment: - self.simple_response('400 Bad Request', - 'Illegal #fragment in Request-URI.') - return False - - if path is None: - # FIXME: It looks like this case cannot happen - self.simple_response('400 Bad Request', - 'Invalid path in Request-URI.') - return False - - # Unquote the path+params (e.g. "/this%20path" -> "/this path"). - # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 - # - # But note that "...a URI must be separated into its components - # before the escaped characters within those components can be - # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 - # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not - # "/this/path". - try: - # TODO: Figure out whether exception can really happen here. - # It looks like it's caught on urlsplit() call above. - atoms = [ - urllib.parse.unquote_to_bytes(x) - for x in QUOTED_SLASH_REGEX.split(path) - ] - except ValueError as ex: - self.simple_response('400 Bad Request', ex.args[0]) - return False - path = QUOTED_SLASH.join(atoms) - - if not path.startswith(FORWARD_SLASH): - path = FORWARD_SLASH + path - - if scheme is not EMPTY: - self.scheme = scheme - self.authority = authority - self.path = path - - # Note that, like wsgiref and most other HTTP servers, - # we "% HEX HEX"-unquote the path but not the query string. - self.qs = qs - - # Compare request and server HTTP protocol versions, in case our - # server does not support the requested protocol. Limit our output - # to min(req, server). We want the following output: - # request server actual written supported response - # protocol protocol response protocol feature set - # a 1.0 1.0 1.0 1.0 - # b 1.0 1.1 1.1 1.0 - # c 1.1 1.0 1.0 1.0 - # d 1.1 1.1 1.1 1.1 - # Notice that, in (b), the response will be "HTTP/1.1" even though - # the client only understands 1.0. RFC 2616 10.5.6 says we should - # only return 505 if the _major_ version is different. - sp = int(self.server.protocol[5]), int(self.server.protocol[7]) - - if sp[0] != rp[0]: - self.simple_response('505 HTTP Version Not Supported') - return False - - self.request_protocol = req_protocol - self.response_protocol = 'HTTP/%s.%s' % min(rp, sp) - - return True - - def read_request_headers(self): - """Read self.rfile into self.inheaders. Return success.""" - # then all the http headers - try: - self.header_reader(self.rfile, self.inheaders) - except ValueError as ex: - self.simple_response('400 Bad Request', ex.args[0]) - return False - - mrbs = self.server.max_request_body_size - - try: - cl = int(self.inheaders.get(b'Content-Length', 0)) - except ValueError: - self.simple_response( - '400 Bad Request', - 'Malformed Content-Length Header.') - return False - - if mrbs and cl > mrbs: - self.simple_response( - '413 Request Entity Too Large', - 'The entity sent with the request exceeds the maximum ' - 'allowed bytes.') - return False - - # Persistent connection support - if self.response_protocol == 'HTTP/1.1': - # Both server and client are HTTP/1.1 - if self.inheaders.get(b'Connection', b'') == b'close': - self.close_connection = True - else: - # Either the server or client (or both) are HTTP/1.0 - if self.inheaders.get(b'Connection', b'') != b'Keep-Alive': - self.close_connection = True - - # Transfer-Encoding support - te = None - if self.response_protocol == 'HTTP/1.1': - te = self.inheaders.get(b'Transfer-Encoding') - if te: - te = [x.strip().lower() for x in te.split(b',') if x.strip()] - - self.chunked_read = False - - if te: - for enc in te: - if enc == b'chunked': - self.chunked_read = True - else: - # Note that, even if we see "chunked", we must reject - # if there is an extension we don't recognize. - self.simple_response('501 Unimplemented') - self.close_connection = True - return False - - # From PEP 333: - # "Servers and gateways that implement HTTP 1.1 must provide - # transparent support for HTTP 1.1's "expect/continue" mechanism. - # This may be done in any of several ways: - # 1. Respond to requests containing an Expect: 100-continue request - # with an immediate "100 Continue" response, and proceed normally. - # 2. Proceed with the request normally, but provide the application - # with a wsgi.input stream that will send the "100 Continue" - # response if/when the application first attempts to read from - # the input stream. The read request must then remain blocked - # until the client responds. - # 3. Wait until the client decides that the server does not support - # expect/continue, and sends the request body on its own. - # (This is suboptimal, and is not recommended.) - # - # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, - # but it seems like it would be a big slowdown for such a rare case. - if self.inheaders.get(b'Expect', b'') == b'100-continue': - # Don't use simple_response here, because it emits headers - # we don't want. See - # https://github.com/cherrypy/cherrypy/issues/951 - msg = self.server.protocol.encode('ascii') - msg += b' 100 Continue\r\n\r\n' - try: - self.conn.wfile.write(msg) - except socket.error as ex: - if ex.args[0] not in errors.socket_errors_to_ignore: - raise - return True - - def respond(self): - """Call the gateway and write its iterable output.""" - mrbs = self.server.max_request_body_size - if self.chunked_read: - self.rfile = ChunkedRFile(self.conn.rfile, mrbs) - else: - cl = int(self.inheaders.get(b'Content-Length', 0)) - if mrbs and mrbs < cl: - if not self.sent_headers: - self.simple_response( - '413 Request Entity Too Large', - 'The entity sent with the request exceeds the ' - 'maximum allowed bytes.') - return - self.rfile = KnownLengthRFile(self.conn.rfile, cl) - - self.server.gateway(self).respond() - self.ready and self.ensure_headers_sent() - - if self.chunked_write: - self.conn.wfile.write(b'0\r\n\r\n') - - def simple_response(self, status, msg=''): - """Write a simple response back to the client.""" - status = str(status) - proto_status = '%s %s\r\n' % (self.server.protocol, status) - content_length = 'Content-Length: %s\r\n' % len(msg) - content_type = 'Content-Type: text/plain\r\n' - buf = [ - proto_status.encode('ISO-8859-1'), - content_length.encode('ISO-8859-1'), - content_type.encode('ISO-8859-1'), - ] - - if status[:3] in ('413', '414'): - # Request Entity Too Large / Request-URI Too Long - self.close_connection = True - if self.response_protocol == 'HTTP/1.1': - # This will not be true for 414, since read_request_line - # usually raises 414 before reading the whole line, and we - # therefore cannot know the proper response_protocol. - buf.append(b'Connection: close\r\n') - else: - # HTTP/1.0 had no 413/414 status nor Connection header. - # Emit 400 instead and trust the message body is enough. - status = '400 Bad Request' - - buf.append(CRLF) - if msg: - if isinstance(msg, six.text_type): - msg = msg.encode('ISO-8859-1') - buf.append(msg) - - try: - self.conn.wfile.write(EMPTY.join(buf)) - except socket.error as ex: - if ex.args[0] not in errors.socket_errors_to_ignore: - raise - - def ensure_headers_sent(self): - """Ensure headers are sent to the client if not already sent.""" - if not self.sent_headers: - self.sent_headers = True - self.send_headers() - - def write(self, chunk): - """Write unbuffered data to the client.""" - if self.chunked_write and chunk: - chunk_size_hex = hex(len(chunk))[2:].encode('ascii') - buf = [chunk_size_hex, CRLF, chunk, CRLF] - self.conn.wfile.write(EMPTY.join(buf)) - else: - self.conn.wfile.write(chunk) - - def send_headers(self): - """Assert, process, and send the HTTP response message-headers. - - You must set self.status, and self.outheaders before calling this. - """ - hkeys = [key.lower() for key, value in self.outheaders] - status = int(self.status[:3]) - - if status == 413: - # Request Entity Too Large. Close conn to avoid garbage. - self.close_connection = True - elif b'content-length' not in hkeys: - # "All 1xx (informational), 204 (no content), - # and 304 (not modified) responses MUST NOT - # include a message-body." So no point chunking. - if status < 200 or status in (204, 205, 304): - pass - else: - needs_chunked = ( - self.response_protocol == 'HTTP/1.1' - and self.method != b'HEAD' - ) - if needs_chunked: - # Use the chunked transfer-coding - self.chunked_write = True - self.outheaders.append((b'Transfer-Encoding', b'chunked')) - else: - # Closing the conn is the only way to determine len. - self.close_connection = True - - if b'connection' not in hkeys: - if self.response_protocol == 'HTTP/1.1': - # Both server and client are HTTP/1.1 or better - if self.close_connection: - self.outheaders.append((b'Connection', b'close')) - else: - # Server and/or client are HTTP/1.0 - if not self.close_connection: - self.outheaders.append((b'Connection', b'Keep-Alive')) - - if (not self.close_connection) and (not self.chunked_read): - # Read any remaining request body data on the socket. - # "If an origin server receives a request that does not include an - # Expect request-header field with the "100-continue" expectation, - # the request includes a request body, and the server responds - # with a final status code before reading the entire request body - # from the transport connection, then the server SHOULD NOT close - # the transport connection until it has read the entire request, - # or until the client closes the connection. Otherwise, the client - # might not reliably receive the response message. However, this - # requirement is not be construed as preventing a server from - # defending itself against denial-of-service attacks, or from - # badly broken client implementations." - remaining = getattr(self.rfile, 'remaining', 0) - if remaining > 0: - self.rfile.read(remaining) - - if b'date' not in hkeys: - self.outheaders.append(( - b'Date', - email.utils.formatdate(usegmt=True).encode('ISO-8859-1'), - )) - - if b'server' not in hkeys: - self.outheaders.append(( - b'Server', - self.server.server_name.encode('ISO-8859-1'), - )) - - proto = self.server.protocol.encode('ascii') - buf = [proto + SPACE + self.status + CRLF] - for k, v in self.outheaders: - buf.append(k + COLON + SPACE + v + CRLF) - buf.append(CRLF) - self.conn.wfile.write(EMPTY.join(buf)) - - -class HTTPConnection: - """An HTTP connection (active socket).""" - - remote_addr = None - remote_port = None - ssl_env = None - rbufsize = io.DEFAULT_BUFFER_SIZE - wbufsize = io.DEFAULT_BUFFER_SIZE - RequestHandlerClass = HTTPRequest - peercreds_enabled = False - peercreds_resolve_enabled = False - - def __init__(self, server, sock, makefile=MakeFile): - """Initialize HTTPConnection instance. - - Args: - server (HTTPServer): web server object receiving this request - socket (socket._socketobject): the raw socket object (usually - TCP) for this connection - makefile (file): a fileobject class for reading from the socket - """ - self.server = server - self.socket = sock - self.rfile = makefile(sock, 'rb', self.rbufsize) - self.wfile = makefile(sock, 'wb', self.wbufsize) - self.requests_seen = 0 - - self.peercreds_enabled = self.server.peercreds_enabled - self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled - - # LRU cached methods: - # Ref: https://stackoverflow.com/a/14946506/595220 - self.resolve_peer_creds = ( - lru_cache(maxsize=1)(self.resolve_peer_creds) - ) - self.get_peer_creds = ( - lru_cache(maxsize=1)(self.get_peer_creds) - ) - - def communicate(self): - """Read each request and respond appropriately.""" - request_seen = False - try: - while True: - # (re)set req to None so that if something goes wrong in - # the RequestHandlerClass constructor, the error doesn't - # get written to the previous request. - req = None - req = self.RequestHandlerClass(self.server, self) - - # This order of operations should guarantee correct pipelining. - req.parse_request() - if self.server.stats['Enabled']: - self.requests_seen += 1 - if not req.ready: - # Something went wrong in the parsing (and the server has - # probably already made a simple_response). Return and - # let the conn close. - return - - request_seen = True - req.respond() - if req.close_connection: - return - except socket.error as ex: - errnum = ex.args[0] - # sadly SSL sockets return a different (longer) time out string - timeout_errs = 'timed out', 'The read operation timed out' - if errnum in timeout_errs: - # Don't error if we're between requests; only error - # if 1) no request has been started at all, or 2) we're - # in the middle of a request. - # See https://github.com/cherrypy/cherrypy/issues/853 - if (not request_seen) or (req and req.started_request): - self._conditional_error(req, '408 Request Timeout') - elif errnum not in errors.socket_errors_to_ignore: - self.server.error_log('socket.error %s' % repr(errnum), - level=logging.WARNING, traceback=True) - self._conditional_error(req, '500 Internal Server Error') - except (KeyboardInterrupt, SystemExit): - raise - except errors.FatalSSLAlert: - pass - except errors.NoSSLError: - self._handle_no_ssl(req) - except Exception as ex: - self.server.error_log( - repr(ex), level=logging.ERROR, traceback=True) - self._conditional_error(req, '500 Internal Server Error') - - linger = False - - def _handle_no_ssl(self, req): - if not req or req.sent_headers: - return - # Unwrap wfile - self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize) - msg = ( - 'The client sent a plain HTTP request, but ' - 'this server only speaks HTTPS on this port.' - ) - req.simple_response('400 Bad Request', msg) - self.linger = True - - def _conditional_error(self, req, response): - """Respond with an error. - - Don't bother writing if a response - has already started being written. - """ - if not req or req.sent_headers: - return - - try: - req.simple_response(response) - except errors.FatalSSLAlert: - pass - except errors.NoSSLError: - self._handle_no_ssl(req) - - def close(self): - """Close the socket underlying this connection.""" - self.rfile.close() - - if not self.linger: - self._close_kernel_socket() - self.socket.close() - else: - # On the other hand, sometimes we want to hang around for a bit - # to make sure the client has a chance to read our entire - # response. Skipping the close() calls here delays the FIN - # packet until the socket object is garbage-collected later. - # Someday, perhaps, we'll do the full lingering_close that - # Apache does, but not today. - pass - - def get_peer_creds(self): # LRU cached on per-instance basis, see __init__ - """Return the PID/UID/GID tuple of the peer socket for UNIX sockets. - - This function uses SO_PEERCRED to query the UNIX PID, UID, GID - of the peer, which is only available if the bind address is - a UNIX domain socket. - - Raises: - NotImplementedError: in case of unsupported socket type - RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled - - """ - PEERCRED_STRUCT_DEF = '3i' - - if IS_WINDOWS or self.socket.family != socket.AF_UNIX: - raise NotImplementedError( - 'SO_PEERCRED is only supported in Linux kernel and WSL' - ) - elif not self.peercreds_enabled: - raise RuntimeError( - 'Peer creds lookup is disabled within this server' - ) - - try: - peer_creds = self.socket.getsockopt( - socket.SOL_SOCKET, socket.SO_PEERCRED, - struct.calcsize(PEERCRED_STRUCT_DEF) - ) - except socket.error as socket_err: - """Non-Linux kernels don't support SO_PEERCRED. - - Refs: - http://welz.org.za/notes/on-peer-cred.html - https://github.com/daveti/tcpSockHack - msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025 - """ - six.raise_from( # 3.6+: raise RuntimeError from socket_err - RuntimeError, - socket_err, - ) - else: - pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds) - return pid, uid, gid - - @property - def peer_pid(self): - """Return the id of the connected peer process.""" - pid, _, _ = self.get_peer_creds() - return pid - - @property - def peer_uid(self): - """Return the user id of the connected peer process.""" - _, uid, _ = self.get_peer_creds() - return uid - - @property - def peer_gid(self): - """Return the group id of the connected peer process.""" - _, _, gid = self.get_peer_creds() - return gid - - def resolve_peer_creds(self): # LRU cached on per-instance basis - """Return the username and group tuple of the peercreds if available. - - Raises: - NotImplementedError: in case of unsupported OS - RuntimeError: in case of UID/GID lookup unsupported or disabled - - """ - if (IS_WINDOWS or IS_ANDROID): - raise NotImplementedError( - 'UID/GID lookup can only be done under UNIX-like OS' - ) - elif not self.peercreds_resolve_enabled: - raise RuntimeError( - 'UID/GID lookup is disabled within this server' - ) - - user = pwd.getpwuid(self.peer_uid).pw_name # [0] - group = grp.getgrgid(self.peer_gid).gr_name # [0] - - return user, group - - @property - def peer_user(self): - """Return the username of the connected peer process.""" - user, _ = self.resolve_peer_creds() - return user - - @property - def peer_group(self): - """Return the group of the connected peer process.""" - _, group = self.resolve_peer_creds() - return group - - def _close_kernel_socket(self): - """Close kernel socket in outdated Python versions. - - On old Python versions, - Python's socket module does NOT call close on the kernel - socket when you call socket.close(). We do so manually here - because we want this server to send a FIN TCP segment - immediately. Note this must be called *before* calling - socket.close(), because the latter drops its reference to - the kernel socket. - """ - if six.PY2 and hasattr(self.socket, '_sock'): - self.socket._sock.close() - - -def prevent_socket_inheritance(sock): - """Stub inheritance prevention. - - Dummy function, since neither fcntl nor ctypes are available. - """ - pass - - -class HTTPServer: - """An HTTP server.""" - - _bind_addr = '127.0.0.1' - _interrupt = None - - gateway = None - """A Gateway instance.""" - - minthreads = None - """The minimum number of worker threads to create (default 10).""" - - maxthreads = None - """The maximum number of worker threads to create. - - (default -1 = no limit)""" - - server_name = None - """The name of the server; defaults to ``self.version``.""" - - protocol = 'HTTP/1.1' - """The version string to write in the Status-Line of all HTTP responses. - - For example, "HTTP/1.1" is the default. This also limits the supported - features used in the response.""" - - request_queue_size = 5 - """The 'backlog' arg to socket.listen(); max queued connections. - - (default 5).""" - - shutdown_timeout = 5 - """The total time to wait for worker threads to cleanly exit. - - Specified in seconds.""" - - timeout = 10 - """The timeout in seconds for accepted connections (default 10).""" - - version = 'Cheroot/' + __version__ - """A version string for the HTTPServer.""" - - software = None - """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. - - If None, this defaults to ``'%s Server' % self.version``. - """ - - ready = False - """Internal flag which indicating the socket is accepting connections.""" - - max_request_header_size = 0 - """The maximum size, in bytes, for request headers, or 0 for no limit.""" - - max_request_body_size = 0 - """The maximum size, in bytes, for request bodies, or 0 for no limit.""" - - nodelay = True - """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" - - ConnectionClass = HTTPConnection - """The class to use for handling HTTP connections.""" - - ssl_adapter = None - """An instance of ssl.Adapter (or a subclass). - - You must have the corresponding SSL driver library installed. - """ - - peercreds_enabled = False - """If True, peer cred lookup can be performed via UNIX domain socket.""" - - peercreds_resolve_enabled = False - """If True, username/group will be looked up in the OS from peercreds.""" - - def __init__( - self, bind_addr, gateway, - minthreads=10, maxthreads=-1, server_name=None, - peercreds_enabled=False, peercreds_resolve_enabled=False, - ): - """Initialize HTTPServer instance. - - Args: - bind_addr (tuple): network interface to listen to - gateway (Gateway): gateway for processing HTTP requests - minthreads (int): minimum number of threads for HTTP thread pool - maxthreads (int): maximum number of threads for HTTP thread pool - server_name (str): web server name to be advertised via Server - HTTP header - """ - self.bind_addr = bind_addr - self.gateway = gateway - - self.requests = threadpool.ThreadPool( - self, min=minthreads or 1, max=maxthreads) - - if not server_name: - server_name = self.version - self.server_name = server_name - self.peercreds_enabled = peercreds_enabled - self.peercreds_resolve_enabled = ( - peercreds_resolve_enabled and peercreds_enabled - ) - self.clear_stats() - - def clear_stats(self): - """Reset server stat counters..""" - self._start_time = None - self._run_time = 0 - self.stats = { - 'Enabled': False, - 'Bind Address': lambda s: repr(self.bind_addr), - 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), - 'Accepts': 0, - 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), - 'Queue': lambda s: getattr(self.requests, 'qsize', None), - 'Threads': lambda s: len(getattr(self.requests, '_threads', [])), - 'Threads Idle': lambda s: getattr(self.requests, 'idle', None), - 'Socket Errors': 0, - 'Requests': lambda s: (not s['Enabled']) and -1 or sum( - [w['Requests'](w) for w in s['Worker Threads'].values()], 0), - 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum( - [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0), - 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum( - [w['Bytes Written'](w) for w in s['Worker Threads'].values()], - 0), - 'Work Time': lambda s: (not s['Enabled']) and -1 or sum( - [w['Work Time'](w) for w in s['Worker Threads'].values()], 0), - 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( - [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) - for w in s['Worker Threads'].values()], 0), - 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( - [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) - for w in s['Worker Threads'].values()], 0), - 'Worker Threads': {}, - } - logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats - - def runtime(self): - """Return server uptime.""" - if self._start_time is None: - return self._run_time - else: - return self._run_time + (time.time() - self._start_time) - - def __str__(self): - """Render Server instance representing bind address.""" - return '%s.%s(%r)' % (self.__module__, self.__class__.__name__, - self.bind_addr) - - @property - def bind_addr(self): - """Return the interface on which to listen for connections. - - For TCP sockets, a (host, port) tuple. Host values may be any IPv4 - or IPv6 address, or any valid hostname. The string 'localhost' is a - synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). - The string '0.0.0.0' is a special IPv4 entry meaning "any active - interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for - IPv6. The empty string or None are not allowed. - - For UNIX sockets, supply the filename as a string. - - Systemd socket activation is automatic and doesn't require tempering - with this variable. - """ - return self._bind_addr - - @bind_addr.setter - def bind_addr(self, value): - """Set the interface on which to listen for connections.""" - if isinstance(value, tuple) and value[0] in ('', None): - # Despite the socket module docs, using '' does not - # allow AI_PASSIVE to work. Passing None instead - # returns '0.0.0.0' like we want. In other words: - # host AI_PASSIVE result - # '' Y 192.168.x.y - # '' N 192.168.x.y - # None Y 0.0.0.0 - # None N 127.0.0.1 - # But since you can get the same effect with an explicit - # '0.0.0.0', we deny both the empty string and None as values. - raise ValueError("Host values of '' or None are not allowed. " - "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " - 'to listen on all active interfaces.') - self._bind_addr = value - - def safe_start(self): - """Run the server forever, and stop it cleanly on exit.""" - try: - self.start() - except (KeyboardInterrupt, IOError): - # The time.sleep call might raise - # "IOError: [Errno 4] Interrupted function call" on KBInt. - self.error_log('Keyboard Interrupt: shutting down') - self.stop() - raise - except SystemExit: - self.error_log('SystemExit raised: shutting down') - self.stop() - raise - - def prepare(self): - """Prepare server to serving requests. - - It binds a socket's port, setups the socket to ``listen()`` and does - other preparing things. - """ - self._interrupt = None - - if self.software is None: - self.software = '%s Server' % self.version - - # Select the appropriate socket - self.socket = None - if os.getenv('LISTEN_PID', None): - # systemd socket activation - self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) - elif isinstance(self.bind_addr, six.string_types): - # AF_UNIX socket - - # So we can reuse the socket... - try: - os.unlink(self.bind_addr) - except Exception: - pass - - # So everyone can access the socket... - try: - os.chmod(self.bind_addr, 0o777) - except Exception: - pass - - info = [ - (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)] - else: - # AF_INET or AF_INET6 socket - # Get the correct address family for our host (allows IPv6 - # addresses) - host, port = self.bind_addr - try: - info = socket.getaddrinfo( - host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM, 0, socket.AI_PASSIVE) - except socket.gaierror: - sock_type = socket.AF_INET - bind_addr = self.bind_addr - - if ':' in host: - sock_type = socket.AF_INET6 - bind_addr = bind_addr + (0, 0) - - info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)] - - if not self.socket: - msg = 'No socket could be created' - for res in info: - af, socktype, proto, canonname, sa = res - try: - self.bind(af, socktype, proto) - break - except socket.error as serr: - msg = '%s -- (%s: %s)' % (msg, sa, serr) - if self.socket: - self.socket.close() - self.socket = None - - if not self.socket: - raise socket.error(msg) - - # Timeout so KeyboardInterrupt can be caught on Win32 - self.socket.settimeout(1) - self.socket.listen(self.request_queue_size) - - # Create worker threads - self.requests.start() - - self.ready = True - self._start_time = time.time() - - def serve(self): - """Serve requests, after invoking :func:`prepare()`.""" - while self.ready: - try: - self.tick() - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - self.error_log('Error in HTTPServer.tick', level=logging.ERROR, - traceback=True) - - if self.interrupt: - while self.interrupt is True: - # Wait for self.stop() to complete. See _set_interrupt. - time.sleep(0.1) - if self.interrupt: - raise self.interrupt - - def start(self): - """Run the server forever. - - It is shortcut for invoking :func:`prepare()` then :func:`serve()`. - """ - # We don't have to trap KeyboardInterrupt or SystemExit here, - # because cherrypy.server already does so, calling self.stop() for us. - # If you're using this server with another framework, you should - # trap those exceptions in whatever code block calls start(). - self.prepare() - self.serve() - - def error_log(self, msg='', level=20, traceback=False): - """Write error message to log. - - Args: - msg (str): error message - level (int): logging level - traceback (bool): add traceback to output or not - """ - # Override this in subclasses as desired - sys.stderr.write(msg + '\n') - sys.stderr.flush() - if traceback: - tblines = traceback_.format_exc() - sys.stderr.write(tblines) - sys.stderr.flush() - - def bind(self, family, type, proto=0): - """Create (or recreate) the actual socket object.""" - self.socket = socket.socket(family, type, proto) - prevent_socket_inheritance(self.socket) - if not IS_WINDOWS: - # Windows has different semantics for SO_REUSEADDR, - # so don't set it. - # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if self.nodelay and not isinstance(self.bind_addr, str): - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - if self.ssl_adapter is not None: - self.socket = self.ssl_adapter.bind(self.socket) - - host, port = self.bind_addr[:2] - - # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), - # activate dual-stack. See - # https://github.com/cherrypy/cherrypy/issues/871. - listening_ipv6 = ( - hasattr(socket, 'AF_INET6') - and family == socket.AF_INET6 - and host in ('::', '::0', '::0.0.0.0') - ) - if listening_ipv6: - try: - self.socket.setsockopt( - socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - except (AttributeError, socket.error): - # Apparently, the socket option is not available in - # this machine's TCP stack - pass - - self.socket.bind(self.bind_addr) - # TODO: keep requested bind_addr separate real bound_addr (port is - # different in case of ephemeral port 0) - self.bind_addr = self.socket.getsockname() - if family in ( - # Windows doesn't have socket.AF_UNIX, so not using it in check - socket.AF_INET, - socket.AF_INET6, - ): - """UNIX domain sockets are strings or bytes. - - In case of bytes with a leading null-byte it's an abstract socket. - """ - self.bind_addr = self.bind_addr[:2] - - def tick(self): - """Accept a new connection and put it on the Queue.""" - try: - s, addr = self.socket.accept() - if self.stats['Enabled']: - self.stats['Accepts'] += 1 - if not self.ready: - return - - prevent_socket_inheritance(s) - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - mf = MakeFile - ssl_env = {} - # if ssl cert and key are set, we try to be a secure HTTP server - if self.ssl_adapter is not None: - try: - s, ssl_env = self.ssl_adapter.wrap(s) - except errors.NoSSLError: - msg = ('The client sent a plain HTTP request, but ' - 'this server only speaks HTTPS on this port.') - buf = ['%s 400 Bad Request\r\n' % self.protocol, - 'Content-Length: %s\r\n' % len(msg), - 'Content-Type: text/plain\r\n\r\n', - msg] - - sock_to_make = s if six.PY3 else s._sock - wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) - try: - wfile.write(''.join(buf).encode('ISO-8859-1')) - except socket.error as ex: - if ex.args[0] not in errors.socket_errors_to_ignore: - raise - return - if not s: - return - mf = self.ssl_adapter.makefile - # Re-apply our timeout since we may have a new socket object - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - conn = self.ConnectionClass(self, s, mf) - - if not isinstance(self.bind_addr, six.string_types): - # optional values - # Until we do DNS lookups, omit REMOTE_HOST - if addr is None: # sometimes this can happen - # figure out if AF_INET or AF_INET6. - if len(s.getsockname()) == 2: - # AF_INET - addr = ('0.0.0.0', 0) - else: - # AF_INET6 - addr = ('::', 0) - conn.remote_addr = addr[0] - conn.remote_port = addr[1] - - conn.ssl_env = ssl_env - - try: - self.requests.put(conn) - except queue.Full: - # Just drop the conn. TODO: write 503 back? - conn.close() - return - except socket.timeout: - # The only reason for the timeout in start() is so we can - # notice keyboard interrupts on Win32, which don't interrupt - # accept() by default - return - except socket.error as ex: - if self.stats['Enabled']: - self.stats['Socket Errors'] += 1 - if ex.args[0] in errors.socket_error_eintr: - # I *think* this is right. EINTR should occur when a signal - # is received during the accept() call; all docs say retry - # the call, and I *think* I'm reading it right that Python - # will then go ahead and poll for and handle the signal - # elsewhere. See - # https://github.com/cherrypy/cherrypy/issues/707. - return - if ex.args[0] in errors.socket_errors_nonblocking: - # Just try again. See - # https://github.com/cherrypy/cherrypy/issues/479. - return - if ex.args[0] in errors.socket_errors_to_ignore: - # Our socket was closed. - # See https://github.com/cherrypy/cherrypy/issues/686. - return - raise - - @property - def interrupt(self): - """Flag interrupt of the server.""" - return self._interrupt - - @interrupt.setter - def interrupt(self, interrupt): - """Perform the shutdown of this server and save the exception.""" - self._interrupt = True - self.stop() - self._interrupt = interrupt - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - self.ready = False - if self._start_time is not None: - self._run_time += (time.time() - self._start_time) - self._start_time = None - - sock = getattr(self, 'socket', None) - if sock: - if not isinstance(self.bind_addr, six.string_types): - # Touch our own socket to make accept() return immediately. - try: - host, port = sock.getsockname()[:2] - except socket.error as ex: - if ex.args[0] not in errors.socket_errors_to_ignore: - # Changed to use error code and not message - # See - # https://github.com/cherrypy/cherrypy/issues/860. - raise - else: - # Note that we're explicitly NOT using AI_PASSIVE, - # here, because we want an actual IP to touch. - # localhost won't work if we've bound to a public IP, - # but it will if we bound to '0.0.0.0' (INADDR_ANY). - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None - try: - s = socket.socket(af, socktype, proto) - # See - # https://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - if hasattr(sock, 'close'): - sock.close() - self.socket = None - - self.requests.stop(self.shutdown_timeout) - - -class Gateway: - """Base class to interface HTTPServer with other systems, such as WSGI.""" - - def __init__(self, req): - """Initialize Gateway instance with request. - - Args: - req (HTTPRequest): current HTTP request - """ - self.req = req - - def respond(self): - """Process the current request. Must be overridden in a subclass.""" - raise NotImplementedError - - -# These may either be ssl.Adapter subclasses or the string names -# of such classes (in which case they will be lazily loaded). -ssl_adapters = { - 'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter', - 'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter', -} - - -def get_ssl_adapter_class(name='builtin'): - """Return an SSL adapter class for the given name.""" - adapter = ssl_adapters[name.lower()] - if isinstance(adapter, six.string_types): - last_dot = adapter.rfind('.') - attr_name = adapter[last_dot + 1:] - mod_path = adapter[:last_dot] - - try: - mod = sys.modules[mod_path] - if mod is None: - raise KeyError() - except KeyError: - # The last [''] is important. - mod = __import__(mod_path, globals(), locals(), ['']) - - # Let an AttributeError propagate outward. - try: - adapter = getattr(mod, attr_name) - except AttributeError: - raise AttributeError("'%s' object has no attribute '%s'" - % (mod_path, attr_name)) - - return adapter diff --git a/libraries/cheroot/ssl/__init__.py b/libraries/cheroot/ssl/__init__.py deleted file mode 100644 index ec1a0d90..00000000 --- a/libraries/cheroot/ssl/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Implementation of the SSL adapter base interface.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -from abc import ABCMeta, abstractmethod - -from six import add_metaclass - - -@add_metaclass(ABCMeta) -class Adapter: - """Base class for SSL driver library adapters. - - Required methods: - - * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` - * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> - socket file object`` - """ - - @abstractmethod - def __init__( - self, certificate, private_key, certificate_chain=None, - ciphers=None): - """Set up certificates, private key ciphers and reset context.""" - self.certificate = certificate - self.private_key = private_key - self.certificate_chain = certificate_chain - self.ciphers = ciphers - self.context = None - - @abstractmethod - def bind(self, sock): - """Wrap and return the given socket.""" - return sock - - @abstractmethod - def wrap(self, sock): - """Wrap and return the given socket, plus WSGI environ entries.""" - raise NotImplementedError - - @abstractmethod - def get_environ(self): - """Return WSGI environ entries to be merged into each request.""" - raise NotImplementedError - - @abstractmethod - def makefile(self, sock, mode='r', bufsize=-1): - """Return socket file object.""" - raise NotImplementedError diff --git a/libraries/cheroot/ssl/builtin.py b/libraries/cheroot/ssl/builtin.py deleted file mode 100644 index a19f7eef..00000000 --- a/libraries/cheroot/ssl/builtin.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -A library for integrating Python's builtin ``ssl`` library with Cheroot. - -The ssl module must be importable for SSL functionality. - -To use this module, set ``HTTPServer.ssl_adapter`` to an instance of -``BuiltinSSLAdapter``. -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -try: - import ssl -except ImportError: - ssl = None - -try: - from _pyio import DEFAULT_BUFFER_SIZE -except ImportError: - try: - from io import DEFAULT_BUFFER_SIZE - except ImportError: - DEFAULT_BUFFER_SIZE = -1 - -import six - -from . import Adapter -from .. import errors -from ..makefile import MakeFile - - -if six.PY3: - generic_socket_error = OSError -else: - import socket - generic_socket_error = socket.error - del socket - - -def _assert_ssl_exc_contains(exc, *msgs): - """Check whether SSL exception contains either of messages provided.""" - if len(msgs) < 1: - raise TypeError( - '_assert_ssl_exc_contains() requires ' - 'at least one message to be passed.' - ) - err_msg_lower = exc.args[1].lower() - return any(m.lower() in err_msg_lower for m in msgs) - - -class BuiltinSSLAdapter(Adapter): - """A wrapper for integrating Python's builtin ssl module with Cheroot.""" - - certificate = None - """The filename of the server SSL certificate.""" - - private_key = None - """The filename of the server's private key file.""" - - certificate_chain = None - """The filename of the certificate chain file.""" - - context = None - """The ssl.SSLContext that will be used to wrap sockets.""" - - ciphers = None - """The ciphers list of SSL.""" - - def __init__( - self, certificate, private_key, certificate_chain=None, - ciphers=None): - """Set up context in addition to base class properties if available.""" - if ssl is None: - raise ImportError('You must install the ssl module to use HTTPS.') - - super(BuiltinSSLAdapter, self).__init__( - certificate, private_key, certificate_chain, ciphers) - - self.context = ssl.create_default_context( - purpose=ssl.Purpose.CLIENT_AUTH, - cafile=certificate_chain - ) - self.context.load_cert_chain(certificate, private_key) - if self.ciphers is not None: - self.context.set_ciphers(ciphers) - - def bind(self, sock): - """Wrap and return the given socket.""" - return super(BuiltinSSLAdapter, self).bind(sock) - - def wrap(self, sock): - """Wrap and return the given socket, plus WSGI environ entries.""" - EMPTY_RESULT = None, {} - try: - s = self.context.wrap_socket( - sock, do_handshake_on_connect=True, server_side=True, - ) - except ssl.SSLError as ex: - if ex.errno == ssl.SSL_ERROR_EOF: - # This is almost certainly due to the cherrypy engine - # 'pinging' the socket to assert it's connectable; - # the 'ping' isn't SSL. - return EMPTY_RESULT - elif ex.errno == ssl.SSL_ERROR_SSL: - if _assert_ssl_exc_contains(ex, 'http request'): - # The client is speaking HTTP to an HTTPS server. - raise errors.NoSSLError - - # Check if it's one of the known errors - # Errors that are caught by PyOpenSSL, but thrown by - # built-in ssl - _block_errors = ( - 'unknown protocol', 'unknown ca', 'unknown_ca', - 'unknown error', - 'https proxy request', 'inappropriate fallback', - 'wrong version number', - 'no shared cipher', 'certificate unknown', - 'ccs received early', - ) - if _assert_ssl_exc_contains(ex, *_block_errors): - # Accepted error, let's pass - return EMPTY_RESULT - elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'): - # This error is thrown by builtin SSL after a timeout - # when client is speaking HTTP to an HTTPS server. - # The connection can safely be dropped. - return EMPTY_RESULT - raise - except generic_socket_error as exc: - """It is unclear why exactly this happens. - - It's reproducible only under Python 2 with openssl>1.0 and stdlib - ``ssl`` wrapper, and only with CherryPy. - So it looks like some healthcheck tries to connect to this socket - during startup (from the same process). - - - Ref: https://github.com/cherrypy/cherrypy/issues/1618 - """ - if six.PY2 and exc.args == (0, 'Error'): - return EMPTY_RESULT - raise - return s, self.get_environ(s) - - # TODO: fill this out more with mod ssl env - def get_environ(self, sock): - """Create WSGI environ entries to be merged into each request.""" - cipher = sock.cipher() - ssl_environ = { - 'wsgi.url_scheme': 'https', - 'HTTPS': 'on', - 'SSL_PROTOCOL': cipher[1], - 'SSL_CIPHER': cipher[0] - # SSL_VERSION_INTERFACE string The mod_ssl program version - # SSL_VERSION_LIBRARY string The OpenSSL program version - } - return ssl_environ - - def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): - """Return socket file object.""" - return MakeFile(sock, mode, bufsize) diff --git a/libraries/cheroot/ssl/pyopenssl.py b/libraries/cheroot/ssl/pyopenssl.py deleted file mode 100644 index 2185f851..00000000 --- a/libraries/cheroot/ssl/pyopenssl.py +++ /dev/null @@ -1,267 +0,0 @@ -""" -A library for integrating pyOpenSSL with Cheroot. - -The OpenSSL module must be importable for SSL functionality. -You can obtain it from `here <https://launchpad.net/pyopenssl>`_. - -To use this module, set HTTPServer.ssl_adapter to an instance of -ssl.Adapter. There are two ways to use SSL: - -Method One ----------- - - * ``ssl_adapter.context``: an instance of SSL.Context. - -If this is not None, it is assumed to be an SSL.Context instance, -and will be passed to SSL.Connection on bind(). The developer is -responsible for forming a valid Context object. This approach is -to be preferred for more flexibility, e.g. if the cert and key are -streams instead of files, or need decryption, or SSL.SSLv3_METHOD -is desired instead of the default SSL.SSLv23_METHOD, etc. Consult -the pyOpenSSL documentation for complete options. - -Method Two (shortcut) ---------------------- - - * ``ssl_adapter.certificate``: the filename of the server SSL certificate. - * ``ssl_adapter.private_key``: the filename of the server's private key file. - -Both are None by default. If ssl_adapter.context is None, but .private_key -and .certificate are both given and valid, they will be read, and the -context will be automatically created from them. -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import socket -import threading -import time - -try: - from OpenSSL import SSL - from OpenSSL import crypto -except ImportError: - SSL = None - -from . import Adapter -from .. import errors, server as cheroot_server -from ..makefile import MakeFile - - -class SSL_fileobject(MakeFile): - """SSL file object attached to a socket object.""" - - ssl_timeout = 3 - ssl_retry = .01 - - def _safe_call(self, is_reader, call, *args, **kwargs): - """Wrap the given call with SSL error-trapping. - - is_reader: if False EOF errors will be raised. If True, EOF errors - will return "" (to emulate normal sockets). - """ - start = time.time() - while True: - try: - return call(*args, **kwargs) - except SSL.WantReadError: - # Sleep and try again. This is dangerous, because it means - # the rest of the stack has no way of differentiating - # between a "new handshake" error and "client dropped". - # Note this isn't an endless loop: there's a timeout below. - time.sleep(self.ssl_retry) - except SSL.WantWriteError: - time.sleep(self.ssl_retry) - except SSL.SysCallError as e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return '' - - errnum = e.args[0] - if is_reader and errnum in errors.socket_errors_to_ignore: - return '' - raise socket.error(errnum) - except SSL.Error as e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return '' - - thirdarg = None - try: - thirdarg = e.args[0][0][2] - except IndexError: - pass - - if thirdarg == 'http request': - # The client is talking HTTP to an HTTPS server. - raise errors.NoSSLError() - - raise errors.FatalSSLAlert(*e.args) - - if time.time() - start > self.ssl_timeout: - raise socket.timeout('timed out') - - def recv(self, size): - """Receive message of a size from the socket.""" - return self._safe_call(True, super(SSL_fileobject, self).recv, size) - - def sendall(self, *args, **kwargs): - """Send whole message to the socket.""" - return self._safe_call(False, super(SSL_fileobject, self).sendall, - *args, **kwargs) - - def send(self, *args, **kwargs): - """Send some part of message to the socket.""" - return self._safe_call(False, super(SSL_fileobject, self).send, - *args, **kwargs) - - -class SSLConnection: - """A thread-safe wrapper for an SSL.Connection. - - ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. - """ - - def __init__(self, *args): - """Initialize SSLConnection instance.""" - self._ssl_conn = SSL.Connection(*args) - self._lock = threading.RLock() - - for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', - 'renegotiate', 'bind', 'listen', 'connect', 'accept', - 'setblocking', 'fileno', 'close', 'get_cipher_list', - 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', - 'makefile', 'get_app_data', 'set_app_data', 'state_string', - 'sock_shutdown', 'get_peer_certificate', 'want_read', - 'want_write', 'set_connect_state', 'set_accept_state', - 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): - exec("""def %s(self, *args): - self._lock.acquire() - try: - return self._ssl_conn.%s(*args) - finally: - self._lock.release() -""" % (f, f)) - - def shutdown(self, *args): - """Shutdown the SSL connection. - - Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args. - """ - self._lock.acquire() - try: - return self._ssl_conn.shutdown() - finally: - self._lock.release() - - -class pyOpenSSLAdapter(Adapter): - """A wrapper for integrating pyOpenSSL with Cheroot.""" - - certificate = None - """The filename of the server SSL certificate.""" - - private_key = None - """The filename of the server's private key file.""" - - certificate_chain = None - """Optional. The filename of CA's intermediate certificate bundle. - - This is needed for cheaper "chained root" SSL certificates, and should be - left as None if not required.""" - - context = None - """An instance of SSL.Context.""" - - ciphers = None - """The ciphers list of SSL.""" - - def __init__( - self, certificate, private_key, certificate_chain=None, - ciphers=None): - """Initialize OpenSSL Adapter instance.""" - if SSL is None: - raise ImportError('You must install pyOpenSSL to use HTTPS.') - - super(pyOpenSSLAdapter, self).__init__( - certificate, private_key, certificate_chain, ciphers) - - self._environ = None - - def bind(self, sock): - """Wrap and return the given socket.""" - if self.context is None: - self.context = self.get_context() - conn = SSLConnection(self.context, sock) - self._environ = self.get_environ() - return conn - - def wrap(self, sock): - """Wrap and return the given socket, plus WSGI environ entries.""" - return sock, self._environ.copy() - - def get_context(self): - """Return an SSL.Context from self attributes.""" - # See https://code.activestate.com/recipes/442473/ - c = SSL.Context(SSL.SSLv23_METHOD) - c.use_privatekey_file(self.private_key) - if self.certificate_chain: - c.load_verify_locations(self.certificate_chain) - c.use_certificate_file(self.certificate) - return c - - def get_environ(self): - """Return WSGI environ entries to be merged into each request.""" - ssl_environ = { - 'HTTPS': 'on', - # pyOpenSSL doesn't provide access to any of these AFAICT - # 'SSL_PROTOCOL': 'SSLv2', - # SSL_CIPHER string The cipher specification name - # SSL_VERSION_INTERFACE string The mod_ssl program version - # SSL_VERSION_LIBRARY string The OpenSSL program version - } - - if self.certificate: - # Server certificate attributes - cert = open(self.certificate, 'rb').read() - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - ssl_environ.update({ - 'SSL_SERVER_M_VERSION': cert.get_version(), - 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), - # 'SSL_SERVER_V_START': - # Validity of server's certificate (start time), - # 'SSL_SERVER_V_END': - # Validity of server's certificate (end time), - }) - - for prefix, dn in [('I', cert.get_issuer()), - ('S', cert.get_subject())]: - # X509Name objects don't seem to have a way to get the - # complete DN string. Use str() and slice it instead, - # because str(dn) == "<X509Name object '/C=US/ST=...'>" - dnstr = str(dn)[18:-2] - - wsgikey = 'SSL_SERVER_%s_DN' % prefix - ssl_environ[wsgikey] = dnstr - - # The DN should be of the form: /k1=v1/k2=v2, but we must allow - # for any value to contain slashes itself (in a URL). - while dnstr: - pos = dnstr.rfind('=') - dnstr, value = dnstr[:pos], dnstr[pos + 1:] - pos = dnstr.rfind('/') - dnstr, key = dnstr[:pos], dnstr[pos + 1:] - if key and value: - wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) - ssl_environ[wsgikey] = value - - return ssl_environ - - def makefile(self, sock, mode='r', bufsize=-1): - """Return socket file object.""" - if SSL and isinstance(sock, SSL.ConnectionType): - timeout = sock.gettimeout() - f = SSL_fileobject(sock, mode, bufsize) - f.ssl_timeout = timeout - return f - else: - return cheroot_server.CP_fileobject(sock, mode, bufsize) diff --git a/libraries/cheroot/test/__init__.py b/libraries/cheroot/test/__init__.py deleted file mode 100644 index e2a7b348..00000000 --- a/libraries/cheroot/test/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Cheroot test suite.""" diff --git a/libraries/cheroot/test/conftest.py b/libraries/cheroot/test/conftest.py deleted file mode 100644 index 9f5f9284..00000000 --- a/libraries/cheroot/test/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Pytest configuration module. - -Contains fixtures, which are tightly bound to the Cheroot framework -itself, useless for end-users' app testing. -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import pytest - -from ..testing import ( # noqa: F401 - native_server, wsgi_server, -) -from ..testing import get_server_client - - -@pytest.fixture # noqa: F811 -def wsgi_server_client(wsgi_server): - """Create a test client out of given WSGI server.""" - return get_server_client(wsgi_server) - - -@pytest.fixture # noqa: F811 -def native_server_client(native_server): - """Create a test client out of given HTTP server.""" - return get_server_client(native_server) diff --git a/libraries/cheroot/test/helper.py b/libraries/cheroot/test/helper.py deleted file mode 100644 index 38f40b26..00000000 --- a/libraries/cheroot/test/helper.py +++ /dev/null @@ -1,169 +0,0 @@ -"""A library of helper functions for the Cheroot test suite.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import datetime -import logging -import os -import sys -import time -import threading -import types - -from six.moves import http_client - -import six - -import cheroot.server -import cheroot.wsgi - -from cheroot.test import webtest - -log = logging.getLogger(__name__) -thisdir = os.path.abspath(os.path.dirname(__file__)) -serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') - - -config = { - 'bind_addr': ('127.0.0.1', 54583), - 'server': 'wsgi', - 'wsgi_app': None, -} - - -class CherootWebCase(webtest.WebCase): - """Helper class for a web app test suite.""" - - script_name = '' - scheme = 'http' - - available_servers = { - 'wsgi': cheroot.wsgi.Server, - 'native': cheroot.server.HTTPServer, - } - - @classmethod - def setup_class(cls): - """Create and run one HTTP server per class.""" - conf = config.copy() - conf.update(getattr(cls, 'config', {})) - - s_class = conf.pop('server', 'wsgi') - server_factory = cls.available_servers.get(s_class) - if server_factory is None: - raise RuntimeError('Unknown server in config: %s' % conf['server']) - cls.httpserver = server_factory(**conf) - - cls.HOST, cls.PORT = cls.httpserver.bind_addr - if cls.httpserver.ssl_adapter is None: - ssl = '' - cls.scheme = 'http' - else: - ssl = ' (ssl)' - cls.HTTP_CONN = http_client.HTTPSConnection - cls.scheme = 'https' - - v = sys.version.split()[0] - log.info('Python version used to run this test script: %s' % v) - log.info('Cheroot version: %s' % cheroot.__version__) - log.info('HTTP server version: %s%s' % (cls.httpserver.protocol, ssl)) - log.info('PID: %s' % os.getpid()) - - if hasattr(cls, 'setup_server'): - # Clear the wsgi server so that - # it can be updated with the new root - cls.setup_server() - cls.start() - - @classmethod - def teardown_class(cls): - """Cleanup HTTP server.""" - if hasattr(cls, 'setup_server'): - cls.stop() - - @classmethod - def start(cls): - """Load and start the HTTP server.""" - threading.Thread(target=cls.httpserver.safe_start).start() - while not cls.httpserver.ready: - time.sleep(0.1) - - @classmethod - def stop(cls): - """Terminate HTTP server.""" - cls.httpserver.stop() - td = getattr(cls, 'teardown', None) - if td: - td() - - date_tolerance = 2 - - def assertEqualDates(self, dt1, dt2, seconds=None): - """Assert abs(dt1 - dt2) is within Y seconds.""" - if seconds is None: - seconds = self.date_tolerance - - if dt1 > dt2: - diff = dt1 - dt2 - else: - diff = dt2 - dt1 - if not diff < datetime.timedelta(seconds=seconds): - raise AssertionError('%r and %r are not within %r seconds.' % - (dt1, dt2, seconds)) - - -class Request: - """HTTP request container.""" - - def __init__(self, environ): - """Initialize HTTP request.""" - self.environ = environ - - -class Response: - """HTTP response container.""" - - def __init__(self): - """Initialize HTTP response.""" - self.status = '200 OK' - self.headers = {'Content-Type': 'text/html'} - self.body = None - - def output(self): - """Generate iterable response body object.""" - if self.body is None: - return [] - elif isinstance(self.body, six.text_type): - return [self.body.encode('iso-8859-1')] - elif isinstance(self.body, six.binary_type): - return [self.body] - else: - return [x.encode('iso-8859-1') for x in self.body] - - -class Controller: - """WSGI app for tests.""" - - def __call__(self, environ, start_response): - """WSGI request handler.""" - req, resp = Request(environ), Response() - try: - # Python 3 supports unicode attribute names - # Python 2 encodes them - handler = self.handlers[environ['PATH_INFO']] - except KeyError: - resp.status = '404 Not Found' - else: - output = handler(req, resp) - if (output is not None and - not any(resp.status.startswith(status_code) - for status_code in ('204', '304'))): - resp.body = output - try: - resp.headers.setdefault('Content-Length', str(len(output))) - except TypeError: - if not isinstance(output, types.GeneratorType): - raise - start_response(resp.status, resp.headers.items()) - return resp.output() diff --git a/libraries/cheroot/test/test.pem b/libraries/cheroot/test/test.pem deleted file mode 100644 index 47a47042..00000000 --- a/libraries/cheroot/test/test.pem +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ -R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn -da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB -AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj -9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT -enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 -8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 -tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i -0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR -MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB -yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb -8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 -yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD -VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv -MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW -MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy -cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG -A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn -bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx -FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl -cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A -ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M -C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg -KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ -2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ -/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p -YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 -MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G -CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S -8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 -D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T -NluCaWQys3MS ------END CERTIFICATE----- diff --git a/libraries/cheroot/test/test__compat.py b/libraries/cheroot/test/test__compat.py deleted file mode 100644 index d34e5eb8..00000000 --- a/libraries/cheroot/test/test__compat.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -"""Test suite for cross-python compatibility helpers.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import pytest -import six - -from cheroot._compat import ntob, ntou, bton - - -@pytest.mark.parametrize( - 'func,inp,out', - [ - (ntob, 'bar', b'bar'), - (ntou, 'bar', u'bar'), - (bton, b'bar', 'bar'), - ], -) -def test_compat_functions_positive(func, inp, out): - """Check that compat functions work with correct input.""" - assert func(inp, encoding='utf-8') == out - - -@pytest.mark.parametrize( - 'func', - [ - ntob, - ntou, - ], -) -def test_compat_functions_negative_nonnative(func): - """Check that compat functions fail loudly for incorrect input.""" - non_native_test_str = b'bar' if six.PY3 else u'bar' - with pytest.raises(TypeError): - func(non_native_test_str, encoding='utf-8') - - -@pytest.mark.skip(reason='This test does not work now') -@pytest.mark.skipif( - six.PY3, - reason='This code path only appears in Python 2 version.', -) -def test_ntou_escape(): - """Check that ntou supports escape-encoding under Python 2.""" - expected = u'' - actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape') - assert actual == expected diff --git a/libraries/cheroot/test/test_conn.py b/libraries/cheroot/test/test_conn.py deleted file mode 100644 index f543dd9b..00000000 --- a/libraries/cheroot/test/test_conn.py +++ /dev/null @@ -1,897 +0,0 @@ -"""Tests for TCP connection handling, including proper and timely close.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import socket -import time - -from six.moves import range, http_client, urllib - -import six -import pytest - -from cheroot.test import helper, webtest - - -timeout = 1 -pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' - - -class Controller(helper.Controller): - """Controller for serving WSGI apps.""" - - def hello(req, resp): - """Render Hello world.""" - return 'Hello, world!' - - def pov(req, resp): - """Render pov value.""" - return pov - - def stream(req, resp): - """Render streaming response.""" - if 'set_cl' in req.environ['QUERY_STRING']: - resp.headers['Content-Length'] = str(10) - - def content(): - for x in range(10): - yield str(x) - - return content() - - def upload(req, resp): - """Process file upload and render thank.""" - if not req.environ['REQUEST_METHOD'] == 'POST': - raise AssertionError("'POST' != request.method %r" % - req.environ['REQUEST_METHOD']) - return "thanks for '%s'" % req.environ['wsgi.input'].read() - - def custom_204(req, resp): - """Render response with status 204.""" - resp.status = '204' - return 'Code = 204' - - def custom_304(req, resp): - """Render response with status 304.""" - resp.status = '304' - return 'Code = 304' - - def err_before_read(req, resp): - """Render response with status 500.""" - resp.status = '500 Internal Server Error' - return 'ok' - - def one_megabyte_of_a(req, resp): - """Render 1MB response.""" - return ['a' * 1024] * 1024 - - def wrong_cl_buffered(req, resp): - """Render buffered response with invalid length value.""" - resp.headers['Content-Length'] = '5' - return 'I have too many bytes' - - def wrong_cl_unbuffered(req, resp): - """Render unbuffered response with invalid length value.""" - resp.headers['Content-Length'] = '5' - return ['I too', ' have too many bytes'] - - def _munge(string): - """Encode PATH_INFO correctly depending on Python version. - - WSGI 1.0 is a mess around unicode. Create endpoints - that match the PATH_INFO that it produces. - """ - if six.PY3: - return string.encode('utf-8').decode('latin-1') - return string - - handlers = { - '/hello': hello, - '/pov': pov, - '/page1': pov, - '/page2': pov, - '/page3': pov, - '/stream': stream, - '/upload': upload, - '/custom/204': custom_204, - '/custom/304': custom_304, - '/err_before_read': err_before_read, - '/one_megabyte_of_a': one_megabyte_of_a, - '/wrong_cl_buffered': wrong_cl_buffered, - '/wrong_cl_unbuffered': wrong_cl_unbuffered, - } - - -@pytest.fixture -def testing_server(wsgi_server_client): - """Attach a WSGI app to the given server and pre-configure it.""" - app = Controller() - - def _timeout(req, resp): - return str(wsgi_server.timeout) - app.handlers['/timeout'] = _timeout - wsgi_server = wsgi_server_client.server_instance - wsgi_server.wsgi_app = app - wsgi_server.max_request_body_size = 1001 - wsgi_server.timeout = timeout - wsgi_server.server_client = wsgi_server_client - return wsgi_server - - -@pytest.fixture -def test_client(testing_server): - """Get and return a test client out of the given server.""" - return testing_server.server_client - - -def header_exists(header_name, headers): - """Check that a header is present.""" - return header_name.lower() in (k.lower() for (k, _) in headers) - - -def header_has_value(header_name, header_value, headers): - """Check that a header with a given value is present.""" - return header_name.lower() in (k.lower() for (k, v) in headers - if v == header_value) - - -def test_HTTP11_persistent_connections(test_client): - """Test persistent HTTP/1.1 connections.""" - # Initialize a persistent HTTP connection - http_connection = test_client.get_connection() - http_connection.auto_open = False - http_connection.connect() - - # Make the first request and assert there's no "Connection: close". - status_line, actual_headers, actual_resp_body = test_client.get( - '/pov', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - # Make another request on the same connection. - status_line, actual_headers, actual_resp_body = test_client.get( - '/page1', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - # Test client-side close. - status_line, actual_headers, actual_resp_body = test_client.get( - '/page2', http_conn=http_connection, - headers=[('Connection', 'close')] - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert header_has_value('Connection', 'close', actual_headers) - - # Make another request on the same connection, which should error. - with pytest.raises(http_client.NotConnected): - test_client.get('/pov', http_conn=http_connection) - - -@pytest.mark.parametrize( - 'set_cl', - ( - False, # Without Content-Length - True, # With Content-Length - ) -) -def test_streaming_11(test_client, set_cl): - """Test serving of streaming responses with HTTP/1.1 protocol.""" - # Initialize a persistent HTTP connection - http_connection = test_client.get_connection() - http_connection.auto_open = False - http_connection.connect() - - # Make the first request and assert there's no "Connection: close". - status_line, actual_headers, actual_resp_body = test_client.get( - '/pov', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - # 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. - status_line, actual_headers, actual_resp_body = test_client.get( - '/stream?set_cl=Yes', http_conn=http_connection - ) - assert header_exists('Content-Length', actual_headers) - assert not header_has_value('Connection', 'close', actual_headers) - assert not header_exists('Transfer-Encoding', actual_headers) - - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == b'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. - status_line, actual_headers, actual_resp_body = test_client.get( - '/stream', http_conn=http_connection - ) - assert not header_exists('Content-Length', actual_headers) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == b'0123456789' - - chunked_response = False - for k, v in actual_headers: - if k.lower() == 'transfer-encoding': - if str(v) == 'chunked': - chunked_response = True - - if chunked_response: - assert not header_has_value('Connection', 'close', actual_headers) - else: - assert header_has_value('Connection', 'close', actual_headers) - - # Make another request on the same connection, which should - # error. - with pytest.raises(http_client.NotConnected): - test_client.get('/pov', http_conn=http_connection) - - # Try HEAD. - # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864. - # TODO: figure out how can this be possible on an closed connection - # (chunked_response case) - status_line, actual_headers, actual_resp_body = test_client.head( - '/stream', http_conn=http_connection - ) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == b'' - assert not header_exists('Transfer-Encoding', actual_headers) - - -@pytest.mark.parametrize( - 'set_cl', - ( - False, # Without Content-Length - True, # With Content-Length - ) -) -def test_streaming_10(test_client, set_cl): - """Test serving of streaming responses with HTTP/1.0 protocol.""" - original_server_protocol = test_client.server_instance.protocol - test_client.server_instance.protocol = 'HTTP/1.0' - - # Initialize a persistent HTTP connection - http_connection = test_client.get_connection() - http_connection.auto_open = False - http_connection.connect() - - # Make the first request and assert Keep-Alive. - status_line, actual_headers, actual_resp_body = test_client.get( - '/pov', http_conn=http_connection, - headers=[('Connection', 'Keep-Alive')], - protocol='HTTP/1.0', - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert header_has_value('Connection', 'Keep-Alive', actual_headers) - - # 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. - status_line, actual_headers, actual_resp_body = test_client.get( - '/stream?set_cl=Yes', http_conn=http_connection, - headers=[('Connection', 'Keep-Alive')], - protocol='HTTP/1.0', - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == b'0123456789' - - assert header_exists('Content-Length', actual_headers) - assert header_has_value('Connection', 'Keep-Alive', actual_headers) - assert not header_exists('Transfer-Encoding', actual_headers) - else: - # When a Content-Length is not provided, - # the server should close the connection. - status_line, actual_headers, actual_resp_body = test_client.get( - '/stream', http_conn=http_connection, - headers=[('Connection', 'Keep-Alive')], - protocol='HTTP/1.0', - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == b'0123456789' - - assert not header_exists('Content-Length', actual_headers) - assert not header_has_value('Connection', 'Keep-Alive', actual_headers) - assert not header_exists('Transfer-Encoding', actual_headers) - - # Make another request on the same connection, which should error. - with pytest.raises(http_client.NotConnected): - test_client.get( - '/pov', http_conn=http_connection, - protocol='HTTP/1.0', - ) - - test_client.server_instance.protocol = original_server_protocol - - -@pytest.mark.parametrize( - 'http_server_protocol', - ( - 'HTTP/1.0', - 'HTTP/1.1', - ) -) -def test_keepalive(test_client, http_server_protocol): - """Test Keep-Alive enabled connections.""" - original_server_protocol = test_client.server_instance.protocol - test_client.server_instance.protocol = http_server_protocol - - http_client_protocol = 'HTTP/1.0' - - # Initialize a persistent HTTP connection - http_connection = test_client.get_connection() - http_connection.auto_open = False - http_connection.connect() - - # Test a normal HTTP/1.0 request. - status_line, actual_headers, actual_resp_body = test_client.get( - '/page2', - protocol=http_client_protocol, - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - # Test a keep-alive HTTP/1.0 request. - - status_line, actual_headers, actual_resp_body = test_client.get( - '/page3', headers=[('Connection', 'Keep-Alive')], - http_conn=http_connection, protocol=http_client_protocol, - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert header_has_value('Connection', 'Keep-Alive', actual_headers) - - # Remove the keep-alive header again. - status_line, actual_headers, actual_resp_body = test_client.get( - '/page3', http_conn=http_connection, - protocol=http_client_protocol, - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - test_client.server_instance.protocol = original_server_protocol - - -@pytest.mark.parametrize( - 'timeout_before_headers', - ( - True, - False, - ) -) -def test_HTTP11_Timeout(test_client, timeout_before_headers): - """Check timeout without sending any data. - - The server will close the conn with a 408. - """ - conn = test_client.get_connection() - conn.auto_open = False - conn.connect() - - if not timeout_before_headers: - # Connect but send half the headers only. - conn.send(b'GET /hello HTTP/1.1') - conn.send(('Host: %s' % conn.host).encode('ascii')) - # else: Connect but send nothing. - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The request should have returned 408 already. - response = conn.response_class(conn.sock, method='GET') - response.begin() - assert response.status == 408 - conn.close() - - -def test_HTTP11_Timeout_after_request(test_client): - """Check timeout after at least one request has succeeded. - - The server should close the connection without 408. - """ - fail_msg = "Writing to timed out socket didn't fail as it should have: %s" - - # Make an initial request - conn = test_client.get_connection() - conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) - conn.putheader('Host', conn.host) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - assert response.status == 200 - actual_body = response.read() - expected_body = str(timeout).encode() - assert actual_body == expected_body - - # Make a second request on the same socket - conn._output(b'GET /hello HTTP/1.1') - conn._output(('Host: %s' % conn.host).encode('ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method='GET') - response.begin() - assert response.status == 200 - actual_body = response.read() - expected_body = b'Hello, world!' - assert actual_body == expected_body - - # 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(('Host: %s' % conn.host).encode('ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method='GET') - try: - response.begin() - except (socket.error, http_client.BadStatusLine): - pass - except Exception as ex: - pytest.fail(fail_msg % ex) - else: - if response.status != 408: - pytest.fail(fail_msg % response.read()) - - conn.close() - - # Make another request on a new socket, which should work - conn = test_client.get_connection() - conn.putrequest('GET', '/pov', skip_host=True) - conn.putheader('Host', conn.host) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - assert response.status == 200 - actual_body = response.read() - expected_body = pov.encode() - assert actual_body == expected_body - - # Make another request on the same socket, - # but timeout on the headers - 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') - try: - response.begin() - except (socket.error, http_client.BadStatusLine): - pass - except Exception as ex: - pytest.fail(fail_msg % ex) - else: - if response.status != 408: - pytest.fail(fail_msg % response.read()) - - conn.close() - - # Retry the request on a new connection, which should work - conn = test_client.get_connection() - conn.putrequest('GET', '/pov', skip_host=True) - conn.putheader('Host', conn.host) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - assert response.status == 200 - actual_body = response.read() - expected_body = pov.encode() - assert actual_body == expected_body - conn.close() - - -def test_HTTP11_pipelining(test_client): - """Test HTTP/1.1 pipelining. - - httplib doesn't support this directly. - """ - conn = test_client.get_connection() - - # Put request 1 - conn.putrequest('GET', '/hello', skip_host=True) - conn.putheader('Host', conn.host) - conn.endheaders() - - for trial in range(5): - # Put next request - conn._output( - ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1') - ) - conn._output(('Host: %s' % conn.host).encode('ascii')) - conn._send_output() - - # Retrieve previous response - response = conn.response_class(conn.sock, method='GET') - # there is a bug in python3 regarding the buffering of - # ``conn.sock``. Until that bug get's fixed we will - # monkey patch the ``reponse`` instance. - # https://bugs.python.org/issue23377 - if six.PY3: - response.fp = conn.sock.makefile('rb', 0) - response.begin() - body = response.read(13) - assert response.status == 200 - assert body == b'Hello, world!' - - # Retrieve final response - response = conn.response_class(conn.sock, method='GET') - response.begin() - body = response.read() - assert response.status == 200 - assert body == b'Hello, world!' - - conn.close() - - -def test_100_Continue(test_client): - """Test 100-continue header processing.""" - conn = test_client.get_connection() - - # Try a page without an Expect request header first. - # Note that httplib's response.begin automatically ignores - # 100 Continue responses, so we must manually check for it. - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', conn.host) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '4') - conn.endheaders() - conn.send(b"d'oh") - response = conn.response_class(conn.sock, method='POST') - version, status, reason = response._read_status() - assert status != 100 - conn.close() - - # Now try a page with an Expect header... - conn.connect() - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', conn.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') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - assert status == 100 - while True: - line = response.fp.readline().strip() - if line: - pytest.fail( - '100 Continue should not output any headers. Got %r' % - line) - else: - break - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 200 - expected_resp_body = ("thanks for '%s'" % body).encode() - assert actual_resp_body == expected_resp_body - conn.close() - - -@pytest.mark.parametrize( - 'max_request_body_size', - ( - 0, - 1001, - ) -) -def test_readall_or_close(test_client, max_request_body_size): - """Test a max_request_body_size of 0 (the default) and 1001.""" - old_max = test_client.server_instance.max_request_body_size - - test_client.server_instance.max_request_body_size = max_request_body_size - - conn = test_client.get_connection() - - # Get a POST page with an error - conn.putrequest('POST', '/err_before_read', skip_host=True) - conn.putheader('Host', conn.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') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - assert status == 100 - skip = True - while skip: - skip = response.fp.readline().strip() - - # ...send the body - conn.send(b'x' * 1000) - - # ...get the final response - response.begin() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 500 - - # Now try a working page with an Expect header... - conn._output(b'POST /upload HTTP/1.1') - conn._output(('Host: %s' % conn.host).encode('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') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - assert status == 100 - skip = True - while skip: - skip = response.fp.readline().strip() - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 200 - expected_resp_body = ("thanks for '%s'" % body).encode() - assert actual_resp_body == expected_resp_body - conn.close() - - test_client.server_instance.max_request_body_size = old_max - - -def test_No_Message_Body(test_client): - """Test HTTP queries with an empty response body.""" - # Initialize a persistent HTTP connection - http_connection = test_client.get_connection() - http_connection.auto_open = False - http_connection.connect() - - # Make the first request and assert there's no "Connection: close". - status_line, actual_headers, actual_resp_body = test_client.get( - '/pov', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - assert actual_resp_body == pov.encode() - assert not header_exists('Connection', actual_headers) - - # Make a 204 request on the same connection. - status_line, actual_headers, actual_resp_body = test_client.get( - '/custom/204', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 204 - assert not header_exists('Content-Length', actual_headers) - assert actual_resp_body == b'' - assert not header_exists('Connection', actual_headers) - - # Make a 304 request on the same connection. - status_line, actual_headers, actual_resp_body = test_client.get( - '/custom/304', http_conn=http_connection - ) - actual_status = int(status_line[:3]) - assert actual_status == 304 - assert not header_exists('Content-Length', actual_headers) - assert actual_resp_body == b'' - assert not header_exists('Connection', actual_headers) - - -@pytest.mark.xfail( - reason='Server does not correctly read trailers/ending of the previous ' - 'HTTP request, thus the second request fails as the server tries ' - r"to parse b'Content-Type: application/json\r\n' as a " - 'Request-Line. This results in HTTP status code 400, instead of 413' - 'Ref: https://github.com/cherrypy/cheroot/issues/69' -) -def test_Chunked_Encoding(test_client): - """Test HTTP uploads with chunked transfer-encoding.""" - # Initialize a persistent HTTP connection - conn = test_client.get_connection() - - # Try a normal chunked request (with extensions) - body = ( - b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' - b'Content-Type: application/json\r\n' - b'\r\n' - ) - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', conn.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.endheaders() - conn.send(body) - response = conn.getresponse() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 200 - assert status_line[4:] == 'OK' - expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode() - assert actual_resp_body == expected_resp_body - - # Try a chunked request that exceeds server.max_request_body_size. - # Note that the delimiters and trailer are included. - body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n' - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', conn.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() - conn.send(body) - response = conn.getresponse() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 413 - conn.close() - - -def test_Content_Length_in(test_client): - """Try a non-chunked request where Content-Length exceeds limit. - - (server.max_request_body_size). - Assert error before body send. - """ - # Initialize a persistent HTTP connection - conn = test_client.get_connection() - - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', conn.host) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '9999') - conn.endheaders() - response = conn.getresponse() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - assert actual_status == 413 - expected_resp_body = ( - b'The entity sent with the request exceeds ' - b'the maximum allowed bytes.' - ) - assert actual_resp_body == expected_resp_body - conn.close() - - -def test_Content_Length_not_int(test_client): - """Test that malicious Content-Length header returns 400.""" - status_line, actual_headers, actual_resp_body = test_client.post( - '/upload', - headers=[ - ('Content-Type', 'text/plain'), - ('Content-Length', 'not-an-integer'), - ], - ) - actual_status = int(status_line[:3]) - - assert actual_status == 400 - assert actual_resp_body == b'Malformed Content-Length Header.' - - -@pytest.mark.parametrize( - 'uri,expected_resp_status,expected_resp_body', - ( - ('/wrong_cl_buffered', 500, - (b'The requested resource returned more bytes than the ' - b'declared Content-Length.')), - ('/wrong_cl_unbuffered', 200, b'I too'), - ) -) -def test_Content_Length_out( - test_client, - uri, expected_resp_status, expected_resp_body -): - """Test response with Content-Length less than the response body. - - (non-chunked response) - """ - conn = test_client.get_connection() - conn.putrequest('GET', uri, skip_host=True) - conn.putheader('Host', conn.host) - conn.endheaders() - - response = conn.getresponse() - status_line, actual_headers, actual_resp_body = webtest.shb(response) - actual_status = int(status_line[:3]) - - assert actual_status == expected_resp_status - assert actual_resp_body == expected_resp_body - - conn.close() - - -@pytest.mark.xfail( - reason='Sometimes this test fails due to low timeout. ' - 'Ref: https://github.com/cherrypy/cherrypy/issues/598' -) -def test_598(test_client): - """Test serving large file with a read timeout in place.""" - # Initialize a persistent HTTP connection - conn = test_client.get_connection() - remote_data_conn = urllib.request.urlopen( - '%s://%s:%s/one_megabyte_of_a' - % ('http', conn.host, conn.port) - ) - buf = remote_data_conn.read(512) - time.sleep(timeout * 0.6) - remaining = (1024 * 1024) - 512 - while remaining: - data = remote_data_conn.read(remaining) - if not data: - break - buf += data - remaining -= len(data) - - assert len(buf) == 1024 * 1024 - assert buf == b'a' * 1024 * 1024 - assert remaining == 0 - remote_data_conn.close() - - -@pytest.mark.parametrize( - 'invalid_terminator', - ( - b'\n\n', - b'\r\n\n', - ) -) -def test_No_CRLF(test_client, invalid_terminator): - """Test HTTP queries with no valid CRLF terminators.""" - # Initialize a persistent HTTP connection - conn = test_client.get_connection() - - # (b'%s' % b'') is not supported in Python 3.4, so just use + - conn.send(b'GET /hello HTTP/1.1' + invalid_terminator) - response = conn.response_class(conn.sock, method='GET') - response.begin() - actual_resp_body = response.read() - expected_resp_body = b'HTTP requires CRLF terminators' - assert actual_resp_body == expected_resp_body - conn.close() diff --git a/libraries/cheroot/test/test_core.py b/libraries/cheroot/test/test_core.py deleted file mode 100644 index 7c91b13e..00000000 --- a/libraries/cheroot/test/test_core.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Tests for managing HTTP issues (malformed requests, etc).""" -# -*- coding: utf-8 -*- -# vim: set fileencoding=utf-8 : - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import errno -import socket - -import pytest -import six -from six.moves import urllib - -from cheroot.test import helper - - -HTTP_BAD_REQUEST = 400 -HTTP_LENGTH_REQUIRED = 411 -HTTP_NOT_FOUND = 404 -HTTP_OK = 200 -HTTP_VERSION_NOT_SUPPORTED = 505 - - -class HelloController(helper.Controller): - """Controller for serving WSGI apps.""" - - def hello(req, resp): - """Render Hello world.""" - return 'Hello world!' - - def body_required(req, resp): - """Render Hello world or set 411.""" - if req.environ.get('Content-Length', None) is None: - resp.status = '411 Length Required' - return - return 'Hello world!' - - def query_string(req, resp): - """Render QUERY_STRING value.""" - return req.environ.get('QUERY_STRING', '') - - def asterisk(req, resp): - """Render request method value.""" - method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND') - tmpl = 'Got asterisk URI path with {method} method' - return tmpl.format(**locals()) - - def _munge(string): - """Encode PATH_INFO correctly depending on Python version. - - WSGI 1.0 is a mess around unicode. Create endpoints - that match the PATH_INFO that it produces. - """ - if six.PY3: - return string.encode('utf-8').decode('latin-1') - return string - - handlers = { - '/hello': hello, - '/no_body': hello, - '/body_required': body_required, - '/query_string': query_string, - _munge('/привіт'): hello, - _munge('/Юххууу'): hello, - '/\xa0Ðblah key 0 900 4 data': hello, - '/*': asterisk, - } - - -def _get_http_response(connection, method='GET'): - c = connection - kwargs = {'strict': c.strict} if hasattr(c, 'strict') else {} - # Python 3.2 removed the 'strict' feature, saying: - # "http.client now always assumes HTTP/1.x compliant servers." - return c.response_class(c.sock, method=method, **kwargs) - - -@pytest.fixture -def testing_server(wsgi_server_client): - """Attach a WSGI app to the given server and pre-configure it.""" - wsgi_server = wsgi_server_client.server_instance - wsgi_server.wsgi_app = HelloController() - wsgi_server.max_request_body_size = 30000000 - wsgi_server.server_client = wsgi_server_client - return wsgi_server - - -@pytest.fixture -def test_client(testing_server): - """Get and return a test client out of the given server.""" - return testing_server.server_client - - -def test_http_connect_request(test_client): - """Check that CONNECT query results in Method Not Allowed status.""" - status_line = test_client.connect('/anything')[0] - actual_status = int(status_line[:3]) - assert actual_status == 405 - - -def test_normal_request(test_client): - """Check that normal GET query succeeds.""" - status_line, _, actual_resp_body = test_client.get('/hello') - actual_status = int(status_line[:3]) - assert actual_status == HTTP_OK - assert actual_resp_body == b'Hello world!' - - -def test_query_string_request(test_client): - """Check that GET param is parsed well.""" - status_line, _, actual_resp_body = test_client.get( - '/query_string?test=True' - ) - actual_status = int(status_line[:3]) - assert actual_status == HTTP_OK - assert actual_resp_body == b'test=True' - - -@pytest.mark.parametrize( - 'uri', - ( - '/hello', # plain - '/query_string?test=True', # query - '/{0}?{1}={2}'.format( # quoted unicode - *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо')) - ), - ) -) -def test_parse_acceptable_uri(test_client, uri): - """Check that server responds with OK to valid GET queries.""" - status_line = test_client.get(uri)[0] - actual_status = int(status_line[:3]) - assert actual_status == HTTP_OK - - -@pytest.mark.xfail(six.PY2, reason='Fails on Python 2') -def test_parse_uri_unsafe_uri(test_client): - """Test that malicious URI does not allow HTTP injection. - - This effectively checks that sending GET request with URL - - /%A0%D0blah%20key%200%20900%204%20data - - is not converted into - - GET / - blah key 0 900 4 data - HTTP/1.1 - - which would be a security issue otherwise. - """ - c = test_client.get_connection() - resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1') - quoted = urllib.parse.quote(resource) - assert quoted == '/%A0%D0blah%20key%200%20900%204%20data' - request = 'GET {quoted} HTTP/1.1'.format(**locals()) - c._output(request.encode('utf-8')) - c._send_output() - response = _get_http_response(c, method='GET') - response.begin() - assert response.status == HTTP_OK - assert response.fp.read(12) == b'Hello world!' - c.close() - - -def test_parse_uri_invalid_uri(test_client): - """Check that server responds with Bad Request to invalid GET queries. - - Invalid request line test case: it should only contain US-ASCII. - """ - c = test_client.get_connection() - c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8')) - c._send_output() - response = _get_http_response(c, method='GET') - response.begin() - assert response.status == HTTP_BAD_REQUEST - assert response.fp.read(21) == b'Malformed Request-URI' - c.close() - - -@pytest.mark.parametrize( - 'uri', - ( - 'hello', # ascii - 'привіт', # non-ascii - ) -) -def test_parse_no_leading_slash_invalid(test_client, uri): - """Check that server responds with Bad Request to invalid GET queries. - - Invalid request line test case: it should have leading slash (be absolute). - """ - status_line, _, actual_resp_body = test_client.get( - urllib.parse.quote(uri) - ) - actual_status = int(status_line[:3]) - assert actual_status == HTTP_BAD_REQUEST - assert b'starting with a slash' in actual_resp_body - - -def test_parse_uri_absolute_uri(test_client): - """Check that server responds with Bad Request to Absolute URI. - - Only proxy servers should allow this. - """ - status_line, _, actual_resp_body = test_client.get('http://google.com/') - actual_status = int(status_line[:3]) - assert actual_status == HTTP_BAD_REQUEST - expected_body = b'Absolute URI not allowed if server is not a proxy.' - assert actual_resp_body == expected_body - - -def test_parse_uri_asterisk_uri(test_client): - """Check that server responds with OK to OPTIONS with "*" Absolute URI.""" - status_line, _, actual_resp_body = test_client.options('*') - actual_status = int(status_line[:3]) - assert actual_status == HTTP_OK - expected_body = b'Got asterisk URI path with OPTIONS method' - assert actual_resp_body == expected_body - - -def test_parse_uri_fragment_uri(test_client): - """Check that server responds with Bad Request to URI with fragment.""" - status_line, _, actual_resp_body = test_client.get( - '/hello?test=something#fake', - ) - actual_status = int(status_line[:3]) - assert actual_status == HTTP_BAD_REQUEST - expected_body = b'Illegal #fragment in Request-URI.' - assert actual_resp_body == expected_body - - -def test_no_content_length(test_client): - """Test POST query with an empty body being successful.""" - # "The presence of a message-body in a request is signaled by the - # inclusion of a Content-Length or Transfer-Encoding header field in - # the request's message-headers." - # - # Send a message with neither header and no body. - c = test_client.get_connection() - c.request('POST', '/no_body') - response = c.getresponse() - actual_resp_body = response.fp.read() - actual_status = response.status - assert actual_status == HTTP_OK - assert actual_resp_body == b'Hello world!' - - -def test_content_length_required(test_client): - """Test POST query with body failing because of missing Content-Length.""" - # Now send a message that has no Content-Length, but does send a body. - # Verify that CP times out the socket and responds - # with 411 Length Required. - - c = test_client.get_connection() - c.request('POST', '/body_required') - response = c.getresponse() - response.fp.read() - - actual_status = response.status - assert actual_status == HTTP_LENGTH_REQUIRED - - -@pytest.mark.parametrize( - 'request_line,status_code,expected_body', - ( - (b'GET /', # missing proto - HTTP_BAD_REQUEST, b'Malformed Request-Line'), - (b'GET / HTTPS/1.1', # invalid proto - HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol'), - (b'GET / HTTP/2.15', # invalid ver - HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request'), - ) -) -def test_malformed_request_line( - test_client, request_line, - status_code, expected_body -): - """Test missing or invalid HTTP version in Request-Line.""" - c = test_client.get_connection() - c._output(request_line) - c._send_output() - response = _get_http_response(c, method='GET') - response.begin() - assert response.status == status_code - assert response.fp.read(len(expected_body)) == expected_body - c.close() - - -def test_malformed_http_method(test_client): - """Test non-uppercase HTTP method.""" - c = test_client.get_connection() - c.putrequest('GeT', '/malformed_method_case') - c.putheader('Content-Type', 'text/plain') - c.endheaders() - - response = c.getresponse() - actual_status = response.status - assert actual_status == HTTP_BAD_REQUEST - actual_resp_body = response.fp.read(21) - assert actual_resp_body == b'Malformed method name' - - -def test_malformed_header(test_client): - """Check that broken HTTP header results in Bad Request.""" - c = test_client.get_connection() - c.putrequest('GET', '/') - c.putheader('Content-Type', 'text/plain') - # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941 - c._output(b'Re, 1.2.3.4#015#012') - c.endheaders() - - response = c.getresponse() - actual_status = response.status - assert actual_status == HTTP_BAD_REQUEST - actual_resp_body = response.fp.read(20) - assert actual_resp_body == b'Illegal header line.' - - -def test_request_line_split_issue_1220(test_client): - """Check that HTTP request line of exactly 256 chars length is OK.""" - Request_URI = ( - '/hello?' - 'intervenant-entreprise-evenement_classaction=' - 'evenement-mailremerciements' - '&_path=intervenant-entreprise-evenement' - '&intervenant-entreprise-evenement_action-id=19404' - '&intervenant-entreprise-evenement_id=19404' - '&intervenant-entreprise_id=28092' - ) - assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256 - - actual_resp_body = test_client.get(Request_URI)[2] - assert actual_resp_body == b'Hello world!' - - -def test_garbage_in(test_client): - """Test that server sends an error for garbage received over TCP.""" - # Connect without SSL regardless of server.scheme - - c = test_client.get_connection() - c._output(b'gjkgjklsgjklsgjkljklsg') - c._send_output() - response = c.response_class(c.sock, method='GET') - try: - response.begin() - actual_status = response.status - assert actual_status == HTTP_BAD_REQUEST - actual_resp_body = response.fp.read(22) - assert actual_resp_body == b'Malformed Request-Line' - c.close() - except socket.error as ex: - # "Connection reset by peer" is also acceptable. - if ex.errno != errno.ECONNRESET: - raise - - -class CloseController: - """Controller for testing the close callback.""" - - def __call__(self, environ, start_response): - """Get the req to know header sent status.""" - self.req = start_response.__self__.req - resp = CloseResponse(self.close) - start_response(resp.status, resp.headers.items()) - return resp - - def close(self): - """Close, writing hello.""" - self.req.write(b'hello') - - -class CloseResponse: - """Dummy empty response to trigger the no body status.""" - - def __init__(self, close): - """Use some defaults to ensure we have a header.""" - self.status = '200 OK' - self.headers = {'Content-Type': 'text/html'} - self.close = close - - def __getitem__(self, index): - """Ensure we don't have a body.""" - raise IndexError() - - def output(self): - """Return self to hook the close method.""" - return self - - -@pytest.fixture -def testing_server_close(wsgi_server_client): - """Attach a WSGI app to the given server and pre-configure it.""" - wsgi_server = wsgi_server_client.server_instance - wsgi_server.wsgi_app = CloseController() - wsgi_server.max_request_body_size = 30000000 - wsgi_server.server_client = wsgi_server_client - return wsgi_server - - -def test_send_header_before_closing(testing_server_close): - """Test we are actually sending the headers before calling 'close'.""" - _, _, resp_body = testing_server_close.server_client.get('/') - assert resp_body == b'hello' diff --git a/libraries/cheroot/test/test_server.py b/libraries/cheroot/test/test_server.py deleted file mode 100644 index c53f7a81..00000000 --- a/libraries/cheroot/test/test_server.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Tests for the HTTP server.""" -# -*- coding: utf-8 -*- -# vim: set fileencoding=utf-8 : - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import os -import socket -import tempfile -import threading -import time - -import pytest - -from .._compat import bton -from ..server import Gateway, HTTPServer -from ..testing import ( - ANY_INTERFACE_IPV4, - ANY_INTERFACE_IPV6, - EPHEMERAL_PORT, - get_server_client, -) - - -def make_http_server(bind_addr): - """Create and start an HTTP server bound to bind_addr.""" - httpserver = HTTPServer( - bind_addr=bind_addr, - gateway=Gateway, - ) - - threading.Thread(target=httpserver.safe_start).start() - - while not httpserver.ready: - time.sleep(0.1) - - return httpserver - - -non_windows_sock_test = pytest.mark.skipif( - not hasattr(socket, 'AF_UNIX'), - reason='UNIX domain sockets are only available under UNIX-based OS', -) - - -@pytest.fixture -def http_server(): - """Provision a server creator as a fixture.""" - def start_srv(): - bind_addr = yield - httpserver = make_http_server(bind_addr) - yield httpserver - yield httpserver - - srv_creator = iter(start_srv()) - next(srv_creator) - yield srv_creator - try: - while True: - httpserver = next(srv_creator) - if httpserver is not None: - httpserver.stop() - except StopIteration: - pass - - -@pytest.fixture -def unix_sock_file(): - """Check that bound UNIX socket address is stored in server.""" - tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp() - - yield tmp_sock_fname - - os.close(tmp_sock_fh) - os.unlink(tmp_sock_fname) - - -def test_prepare_makes_server_ready(): - """Check that prepare() makes the server ready, and stop() clears it.""" - httpserver = HTTPServer( - bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), - gateway=Gateway, - ) - - assert not httpserver.ready - assert not httpserver.requests._threads - - httpserver.prepare() - - assert httpserver.ready - assert httpserver.requests._threads - for thr in httpserver.requests._threads: - assert thr.ready - - httpserver.stop() - - assert not httpserver.requests._threads - assert not httpserver.ready - - -def test_stop_interrupts_serve(): - """Check that stop() interrupts running of serve().""" - httpserver = HTTPServer( - bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), - gateway=Gateway, - ) - - httpserver.prepare() - serve_thread = threading.Thread(target=httpserver.serve) - serve_thread.start() - - serve_thread.join(0.5) - assert serve_thread.is_alive() - - httpserver.stop() - - serve_thread.join(0.5) - assert not serve_thread.is_alive() - - -@pytest.mark.parametrize( - 'ip_addr', - ( - ANY_INTERFACE_IPV4, - ANY_INTERFACE_IPV6, - ) -) -def test_bind_addr_inet(http_server, ip_addr): - """Check that bound IP address is stored in server.""" - httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) - - assert httpserver.bind_addr[0] == ip_addr - assert httpserver.bind_addr[1] != EPHEMERAL_PORT - - -@non_windows_sock_test -def test_bind_addr_unix(http_server, unix_sock_file): - """Check that bound UNIX socket address is stored in server.""" - httpserver = http_server.send(unix_sock_file) - - assert httpserver.bind_addr == unix_sock_file - - -@pytest.mark.skip(reason="Abstract sockets don't work currently") -@non_windows_sock_test -def test_bind_addr_unix_abstract(http_server): - """Check that bound UNIX socket address is stored in server.""" - unix_abstract_sock = b'\x00cheroot/test/socket/here.sock' - httpserver = http_server.send(unix_abstract_sock) - - assert httpserver.bind_addr == unix_abstract_sock - - -PEERCRED_IDS_URI = '/peer_creds/ids' -PEERCRED_TEXTS_URI = '/peer_creds/texts' - - -class _TestGateway(Gateway): - def respond(self): - req = self.req - conn = req.conn - req_uri = bton(req.uri) - if req_uri == PEERCRED_IDS_URI: - peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid - return ['|'.join(map(str, peer_creds))] - elif req_uri == PEERCRED_TEXTS_URI: - return ['!'.join((conn.peer_user, conn.peer_group))] - return super(_TestGateway, self).respond() - - -@pytest.mark.skip( - reason='Test HTTP client is not able to work through UNIX socket currently' -) -@non_windows_sock_test -def test_peercreds_unix_sock(http_server, unix_sock_file): - """Check that peercred lookup and resolution work when enabled.""" - httpserver = http_server.send(unix_sock_file) - httpserver.gateway = _TestGateway - httpserver.peercreds_enabled = True - - testclient = get_server_client(httpserver) - - expected_peercreds = os.getpid(), os.getuid(), os.getgid() - expected_peercreds = '|'.join(map(str, expected_peercreds)) - assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds - assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI) - - httpserver.peercreds_resolve_enabled = True - import grp - expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name - expected_textcreds = '!'.join(map(str, expected_textcreds)) - assert testclient.get(PEERCRED_TEXTS_URI) == expected_textcreds diff --git a/libraries/cheroot/test/webtest.py b/libraries/cheroot/test/webtest.py deleted file mode 100644 index 43448f5b..00000000 --- a/libraries/cheroot/test/webtest.py +++ /dev/null @@ -1,581 +0,0 @@ -"""Extensions to unittest for web frameworks. - -Use the WebCase.getPage method to request a page from your HTTP server. -Framework Integration -===================== -If you have control over your server process, you can handle errors -in the server-side of the HTTP conversation a bit better. You must run -both the client (your WebCase tests) and the server in the same process -(but in separate threads, obviously). -When an error occurs in the framework, call server_error. It will print -the traceback to stdout, and keep any assertions you have from running -(the assumption is that, if the server errors, the page output will not -be of further significance to your tests). -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import pprint -import re -import socket -import sys -import time -import traceback -import os -import json -import unittest -import warnings - -from six.moves import range, http_client, map, urllib_parse -import six - -from more_itertools.more import always_iterable - - -def interface(host): - """Return an IP address for a client connection given the server host. - - If the server is listening on '0.0.0.0' (INADDR_ANY) - or '::' (IN6ADDR_ANY), this will return the proper localhost. - """ - if host == '0.0.0.0': - # INADDR_ANY, which should respond on localhost. - return '127.0.0.1' - if host == '::': - # IN6ADDR_ANY, which should respond on localhost. - return '::1' - return host - - -try: - # Jython support - if sys.platform[:4] == 'java': - def getchar(): - """Get a key press.""" - # Hopefully this is enough - return sys.stdin.read(1) - else: - # On Windows, msvcrt.getch reads a single char without output. - import msvcrt - - def getchar(): - """Get a key press.""" - return msvcrt.getch() -except ImportError: - # Unix getchr - import tty - import termios - - def getchar(): - """Get a key press.""" - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - - -# from jaraco.properties -class NonDataProperty: - """Non-data property decorator.""" - - def __init__(self, fget): - """Initialize a non-data property.""" - assert fget is not None, 'fget cannot be none' - assert callable(fget), 'fget must be callable' - self.fget = fget - - def __get__(self, obj, objtype=None): - """Return a class property.""" - if obj is None: - return self - return self.fget(obj) - - -class WebCase(unittest.TestCase): - """Helper web test suite base.""" - - HOST = '127.0.0.1' - PORT = 8000 - HTTP_CONN = http_client.HTTPConnection - PROTOCOL = 'HTTP/1.1' - - scheme = 'http' - url = None - - status = None - headers = None - body = None - - encoding = 'utf-8' - - time = None - - @property - def _Conn(self): - """Return HTTPConnection or HTTPSConnection based on self.scheme. - - * from http.client. - """ - cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper()) - return getattr(http_client, cls_name) - - def get_conn(self, auto_open=False): - """Return a connection to our HTTP server.""" - conn = self._Conn(self.interface(), self.PORT) - # Automatically re-connect? - conn.auto_open = auto_open - conn.connect() - return conn - - def set_persistent(self, on=True, auto_open=False): - """Make our HTTP_CONN persistent (or not). - - If the 'on' argument is True (the default), then self.HTTP_CONN - will be set to an instance of HTTP(S)?Connection - to persist across requests. - As this class only allows for a single open connection, if - self already has an open connection, it will be closed. - """ - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - self.HTTP_CONN = ( - self.get_conn(auto_open=auto_open) - if on - else self._Conn - ) - - @property - def persistent(self): # noqa: D401; irrelevant for properties - """Presense of the persistent HTTP connection.""" - return hasattr(self.HTTP_CONN, '__class__') - - @persistent.setter - def persistent(self, on): - self.set_persistent(on) - - def interface(self): - """Return an IP address for a client connection. - - If the server is listening on '0.0.0.0' (INADDR_ANY) - or '::' (IN6ADDR_ANY), this will return the proper localhost. - """ - return interface(self.HOST) - - def getPage(self, url, headers=None, method='GET', body=None, - protocol=None, raise_subcls=None): - """Open the url with debugging support. Return status, headers, body. - - url should be the identifier passed to the server, typically a - server-absolute path and query string (sent between method and - protocol), and should only be an absolute URI if proxy support is - enabled in the server. - - If the application under test generates absolute URIs, be sure - to wrap them first with strip_netloc:: - - class MyAppWebCase(WebCase): - def getPage(url, *args, **kwargs): - super(MyAppWebCase, self).getPage( - cheroot.test.webtest.strip_netloc(url), - *args, **kwargs - ) - - `raise_subcls` must be a tuple with the exceptions classes - or a single exception class that are not going to be considered - a socket.error regardless that they were are subclass of a - socket.error and therefore not considered for a connection retry. - """ - ServerError.on = False - - if isinstance(url, six.text_type): - url = url.encode('utf-8') - if isinstance(body, six.text_type): - body = body.encode('utf-8') - - self.url = url - self.time = None - start = time.time() - result = openURL(url, headers, method, body, self.HOST, self.PORT, - self.HTTP_CONN, protocol or self.PROTOCOL, - raise_subcls) - self.time = time.time() - start - self.status, self.headers, self.body = result - - # Build a list of request cookies from the previous response cookies. - self.cookies = [('Cookie', v) for k, v in self.headers - if k.lower() == 'set-cookie'] - - if ServerError.on: - raise ServerError() - return result - - @NonDataProperty - def interactive(self): - """Determine whether tests are run in interactive mode. - - Load interactivity setting from environment, where - the value can be numeric or a string like true or - False or 1 or 0. - """ - env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True') - is_interactive = bool(json.loads(env_str.lower())) - if is_interactive: - warnings.warn( - 'Interactive test failure interceptor support via ' - 'WEBTEST_INTERACTIVE environment variable is deprecated.', - DeprecationWarning - ) - return is_interactive - - console_height = 30 - - def _handlewebError(self, msg): - print('') - print(' ERROR: %s' % msg) - - if not self.interactive: - raise self.failureException(msg) - - p = (' Show: ' - '[B]ody [H]eaders [S]tatus [U]RL; ' - '[I]gnore, [R]aise, or sys.e[X]it >> ') - sys.stdout.write(p) - sys.stdout.flush() - while True: - i = getchar().upper() - if not isinstance(i, type('')): - i = i.decode('ascii') - if i not in 'BHSUIRX': - continue - print(i.upper()) # Also prints new line - if i == 'B': - for x, line in enumerate(self.body.splitlines()): - if (x + 1) % self.console_height == 0: - # The \r and comma should make the next line overwrite - sys.stdout.write('<-- More -->\r') - m = getchar().lower() - # Erase our "More" prompt - sys.stdout.write(' \r') - if m == 'q': - break - print(line) - elif i == 'H': - pprint.pprint(self.headers) - elif i == 'S': - print(self.status) - elif i == 'U': - print(self.url) - elif i == 'I': - # return without raising the normal exception - return - elif i == 'R': - raise self.failureException(msg) - elif i == 'X': - sys.exit() - sys.stdout.write(p) - sys.stdout.flush() - - @property - def status_code(self): # noqa: D401; irrelevant for properties - """Integer HTTP status code.""" - return int(self.status[:3]) - - def status_matches(self, expected): - """Check whether actual status matches expected.""" - actual = ( - self.status_code - if isinstance(expected, int) else - self.status - ) - return expected == actual - - def assertStatus(self, status, msg=None): - """Fail if self.status != status. - - status may be integer code, exact string status, or - iterable of allowed possibilities. - """ - if any(map(self.status_matches, always_iterable(status))): - return - - tmpl = 'Status {self.status} does not match {status}' - msg = msg or tmpl.format(**locals()) - self._handlewebError(msg) - - def assertHeader(self, key, value=None, msg=None): - """Fail if (key, [value]) not in self.headers.""" - lowkey = key.lower() - for k, v in self.headers: - if k.lower() == lowkey: - if value is None or str(value) == v: - return v - - if msg is None: - if value is None: - msg = '%r not in headers' % key - else: - msg = '%r:%r not in headers' % (key, value) - self._handlewebError(msg) - - def assertHeaderIn(self, key, values, msg=None): - """Fail if header indicated by key doesn't have one of the values.""" - lowkey = key.lower() - for k, v in self.headers: - if k.lower() == lowkey: - matches = [value for value in values if str(value) == v] - if matches: - return matches - - if msg is None: - msg = '%(key)r not in %(values)r' % vars() - self._handlewebError(msg) - - def assertHeaderItemValue(self, key, value, msg=None): - """Fail if the header does not contain the specified value.""" - actual_value = self.assertHeader(key, msg=msg) - header_values = map(str.strip, actual_value.split(',')) - if value in header_values: - return value - - if msg is None: - msg = '%r not in %r' % (value, header_values) - self._handlewebError(msg) - - def assertNoHeader(self, key, msg=None): - """Fail if key in self.headers.""" - lowkey = key.lower() - matches = [k for k, v in self.headers if k.lower() == lowkey] - if matches: - if msg is None: - msg = '%r in headers' % key - self._handlewebError(msg) - - def assertNoHeaderItemValue(self, key, value, msg=None): - """Fail if the header contains the specified value.""" - lowkey = key.lower() - hdrs = self.headers - matches = [k for k, v in hdrs if k.lower() == lowkey and v == value] - if matches: - if msg is None: - msg = '%r:%r in %r' % (key, value, hdrs) - self._handlewebError(msg) - - def assertBody(self, value, msg=None): - """Fail if value != self.body.""" - if isinstance(value, six.text_type): - value = value.encode(self.encoding) - if value != self.body: - if msg is None: - msg = 'expected body:\n%r\n\nactual body:\n%r' % ( - value, self.body) - self._handlewebError(msg) - - def assertInBody(self, value, msg=None): - """Fail if value not in self.body.""" - if isinstance(value, six.text_type): - value = value.encode(self.encoding) - if value not in self.body: - if msg is None: - msg = '%r not in body: %s' % (value, self.body) - self._handlewebError(msg) - - def assertNotInBody(self, value, msg=None): - """Fail if value in self.body.""" - if isinstance(value, six.text_type): - value = value.encode(self.encoding) - if value in self.body: - if msg is None: - msg = '%r found in body' % value - self._handlewebError(msg) - - def assertMatchesBody(self, pattern, msg=None, flags=0): - """Fail if value (a regex pattern) is not in self.body.""" - if isinstance(pattern, six.text_type): - pattern = pattern.encode(self.encoding) - if re.search(pattern, self.body, flags) is None: - if msg is None: - msg = 'No match for %r in body' % pattern - self._handlewebError(msg) - - -methods_with_bodies = ('POST', 'PUT', 'PATCH') - - -def cleanHeaders(headers, method, body, host, port): - """Return request headers, with required headers added (if missing).""" - if headers is None: - headers = [] - - # Add the required Host request header if not present. - # [This specifies the host:port of the server, not the client.] - found = False - for k, v in headers: - if k.lower() == 'host': - found = True - break - if not found: - if port == 80: - headers.append(('Host', host)) - else: - headers.append(('Host', '%s:%s' % (host, port))) - - if method in methods_with_bodies: - # Stick in default type and length headers if not present - found = False - for k, v in headers: - if k.lower() == 'content-type': - found = True - break - if not found: - headers.append( - ('Content-Type', 'application/x-www-form-urlencoded')) - headers.append(('Content-Length', str(len(body or '')))) - - return headers - - -def shb(response): - """Return status, headers, body the way we like from a response.""" - if six.PY3: - h = response.getheaders() - else: - h = [] - key, value = None, None - for line in response.msg.headers: - if line: - if line[0] in ' \t': - value += line.strip() - else: - if key and value: - h.append((key, value)) - key, value = line.split(':', 1) - key = key.strip() - value = value.strip() - if key and value: - h.append((key, value)) - - return '%s %s' % (response.status, response.reason), h, response.read() - - -def openURL(url, headers=None, method='GET', body=None, - host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection, - protocol='HTTP/1.1', raise_subcls=None): - """ - Open the given HTTP resource and return status, headers, and body. - - `raise_subcls` must be a tuple with the exceptions classes - or a single exception class that are not going to be considered - a socket.error regardless that they were are subclass of a - socket.error and therefore not considered for a connection retry. - """ - headers = cleanHeaders(headers, method, body, host, port) - - # Trying 10 times is simply in case of socket errors. - # Normal case--it should run once. - for trial in range(10): - try: - # Allow http_conn to be a class or an instance - if hasattr(http_conn, 'host'): - conn = http_conn - else: - conn = http_conn(interface(host), port) - - conn._http_vsn_str = protocol - conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()])) - - if six.PY3 and isinstance(url, bytes): - url = url.decode() - conn.putrequest(method.upper(), url, skip_host=True, - skip_accept_encoding=True) - - for key, value in headers: - conn.putheader(key, value.encode('Latin-1')) - conn.endheaders() - - if body is not None: - conn.send(body) - - # Handle response - response = conn.getresponse() - - s, h, b = shb(response) - - if not hasattr(http_conn, 'host'): - # We made our own conn instance. Close it. - conn.close() - - return s, h, b - except socket.error as e: - if raise_subcls is not None and isinstance(e, raise_subcls): - raise - else: - time.sleep(0.5) - if trial == 9: - raise - - -def strip_netloc(url): - """Return absolute-URI path from URL. - - Strip the scheme and host from the URL, returning the - server-absolute portion. - - Useful for wrapping an absolute-URI for which only the - path is expected (such as in calls to getPage). - - >>> strip_netloc('https://google.com/foo/bar?bing#baz') - '/foo/bar?bing' - - >>> strip_netloc('//google.com/foo/bar?bing#baz') - '/foo/bar?bing' - - >>> strip_netloc('/foo/bar?bing#baz') - '/foo/bar?bing' - """ - parsed = urllib_parse.urlparse(url) - scheme, netloc, path, params, query, fragment = parsed - stripped = '', '', path, params, query, '' - return urllib_parse.urlunparse(stripped) - - -# Add any exceptions which your web framework handles -# normally (that you don't want server_error to trap). -ignored_exceptions = [] - -# You'll want set this to True when you can't guarantee -# that each response will immediately follow each request; -# for example, when handling requests via multiple threads. -ignore_all = False - - -class ServerError(Exception): - """Exception for signalling server error.""" - - on = False - - -def server_error(exc=None): - """Server debug hook. - - Return True if exception handled, False if ignored. - You probably want to wrap this, so you can still handle an error using - your framework when it's ignored. - """ - if exc is None: - exc = sys.exc_info() - - if ignore_all or exc[0] in ignored_exceptions: - return False - else: - ServerError.on = True - print('') - print(''.join(traceback.format_exception(*exc))) - return True diff --git a/libraries/cheroot/testing.py b/libraries/cheroot/testing.py deleted file mode 100644 index f01d0aa1..00000000 --- a/libraries/cheroot/testing.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Pytest fixtures and other helpers for doing testing by end-users.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -from contextlib import closing -import errno -import socket -import threading -import time - -import pytest -from six.moves import http_client - -import cheroot.server -from cheroot.test import webtest -import cheroot.wsgi - -EPHEMERAL_PORT = 0 -NO_INTERFACE = None # Using this or '' will cause an exception -ANY_INTERFACE_IPV4 = '0.0.0.0' -ANY_INTERFACE_IPV6 = '::' - -config = { - cheroot.wsgi.Server: { - 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), - 'wsgi_app': None, - }, - cheroot.server.HTTPServer: { - 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), - 'gateway': cheroot.server.Gateway, - }, -} - - -def cheroot_server(server_factory): - """Set up and tear down a Cheroot server instance.""" - conf = config[server_factory].copy() - bind_port = conf.pop('bind_addr')[-1] - - for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4: - try: - actual_bind_addr = (interface, bind_port) - httpserver = server_factory( # create it - bind_addr=actual_bind_addr, - **conf - ) - except OSError: - pass - else: - break - - threading.Thread(target=httpserver.safe_start).start() # spawn it - while not httpserver.ready: # wait until fully initialized and bound - time.sleep(0.1) - - yield httpserver - - httpserver.stop() # destroy it - - -@pytest.fixture(scope='module') -def wsgi_server(): - """Set up and tear down a Cheroot WSGI server instance.""" - for srv in cheroot_server(cheroot.wsgi.Server): - yield srv - - -@pytest.fixture(scope='module') -def native_server(): - """Set up and tear down a Cheroot HTTP server instance.""" - for srv in cheroot_server(cheroot.server.HTTPServer): - yield srv - - -class _TestClient: - def __init__(self, server): - self._interface, self._host, self._port = _get_conn_data(server) - self._http_connection = self.get_connection() - self.server_instance = server - - def get_connection(self): - name = '{interface}:{port}'.format( - interface=self._interface, - port=self._port, - ) - return http_client.HTTPConnection(name) - - def request( - self, uri, method='GET', headers=None, http_conn=None, - protocol='HTTP/1.1', - ): - return webtest.openURL( - uri, method=method, - headers=headers, - host=self._host, port=self._port, - http_conn=http_conn or self._http_connection, - protocol=protocol, - ) - - def __getattr__(self, attr_name): - def _wrapper(uri, **kwargs): - http_method = attr_name.upper() - return self.request(uri, method=http_method, **kwargs) - - return _wrapper - - -def _probe_ipv6_sock(interface): - # Alternate way is to check IPs on interfaces using glibc, like: - # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py - try: - with closing(socket.socket(family=socket.AF_INET6)) as sock: - sock.bind((interface, 0)) - except (OSError, socket.error) as sock_err: - # In Python 3 socket.error is an alias for OSError - # In Python 2 socket.error is a subclass of IOError - if sock_err.errno != errno.EADDRNOTAVAIL: - raise - else: - return True - - return False - - -def _get_conn_data(server): - if isinstance(server.bind_addr, tuple): - host, port = server.bind_addr - else: - host, port = server.bind_addr, 0 - - interface = webtest.interface(host) - - if ':' in interface and not _probe_ipv6_sock(interface): - interface = '127.0.0.1' - if ':' in host: - host = interface - - return interface, host, port - - -def get_server_client(server): - """Create and return a test client for the given server.""" - return _TestClient(server) diff --git a/libraries/cheroot/workers/__init__.py b/libraries/cheroot/workers/__init__.py deleted file mode 100644 index 098b8f25..00000000 --- a/libraries/cheroot/workers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""HTTP workers pool.""" diff --git a/libraries/cheroot/workers/threadpool.py b/libraries/cheroot/workers/threadpool.py deleted file mode 100644 index ff8fbcee..00000000 --- a/libraries/cheroot/workers/threadpool.py +++ /dev/null @@ -1,271 +0,0 @@ -"""A thread-based worker pool.""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -import threading -import time -import socket - -from six.moves import queue - - -__all__ = ('WorkerThread', 'ThreadPool') - - -class TrueyZero: - """Object which equals and does math like the integer 0 but evals True.""" - - def __add__(self, other): - return other - - def __radd__(self, other): - return other - - -trueyzero = TrueyZero() - -_SHUTDOWNREQUEST = None - - -class WorkerThread(threading.Thread): - """Thread which continuously polls a Queue for Connection objects. - - Due to the timing issues of polling a Queue, a WorkerThread does not - check its own 'ready' flag after it has started. To stop the thread, - it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue - (one for each running WorkerThread). - """ - - conn = None - """The current connection pulled off the Queue, or None.""" - - server = None - """The HTTP Server which spawned this thread, and which owns the - Queue and is placing active connections into it.""" - - ready = False - """A simple flag for the calling server to know when this thread - has begun polling the Queue.""" - - def __init__(self, server): - """Initialize WorkerThread instance. - - Args: - server (cheroot.server.HTTPServer): web server object - receiving this request - """ - self.ready = False - self.server = server - - self.requests_seen = 0 - self.bytes_read = 0 - self.bytes_written = 0 - self.start_time = None - self.work_time = 0 - self.stats = { - 'Requests': lambda s: self.requests_seen + ( - (self.start_time is None) and - trueyzero or - self.conn.requests_seen - ), - 'Bytes Read': lambda s: self.bytes_read + ( - (self.start_time is None) and - trueyzero or - self.conn.rfile.bytes_read - ), - 'Bytes Written': lambda s: self.bytes_written + ( - (self.start_time is None) and - trueyzero or - self.conn.wfile.bytes_written - ), - 'Work Time': lambda s: self.work_time + ( - (self.start_time is None) and - trueyzero or - time.time() - self.start_time - ), - 'Read Throughput': lambda s: s['Bytes Read'](s) / ( - s['Work Time'](s) or 1e-6), - 'Write Throughput': lambda s: s['Bytes Written'](s) / ( - s['Work Time'](s) or 1e-6), - } - threading.Thread.__init__(self) - - def run(self): - """Process incoming HTTP connections. - - Retrieves incoming connections from thread pool. - """ - self.server.stats['Worker Threads'][self.getName()] = self.stats - try: - self.ready = True - while True: - conn = self.server.requests.get() - if conn is _SHUTDOWNREQUEST: - return - - self.conn = conn - if self.server.stats['Enabled']: - self.start_time = time.time() - try: - conn.communicate() - finally: - conn.close() - if self.server.stats['Enabled']: - self.requests_seen += self.conn.requests_seen - self.bytes_read += self.conn.rfile.bytes_read - self.bytes_written += self.conn.wfile.bytes_written - self.work_time += time.time() - self.start_time - self.start_time = None - self.conn = None - except (KeyboardInterrupt, SystemExit) as ex: - self.server.interrupt = ex - - -class ThreadPool: - """A Request Queue for an HTTPServer which pools threads. - - ThreadPool objects must provide min, get(), put(obj), start() - and stop(timeout) attributes. - """ - - def __init__( - self, server, min=10, max=-1, accepted_queue_size=-1, - accepted_queue_timeout=10): - """Initialize HTTP requests queue instance. - - Args: - server (cheroot.server.HTTPServer): web server object - receiving this request - min (int): minimum number of worker threads - max (int): maximum number of worker threads - accepted_queue_size (int): maximum number of active - requests in queue - accepted_queue_timeout (int): timeout for putting request - into queue - """ - self.server = server - self.min = min - self.max = max - self._threads = [] - self._queue = queue.Queue(maxsize=accepted_queue_size) - self._queue_put_timeout = accepted_queue_timeout - self.get = self._queue.get - - def start(self): - """Start the pool of threads.""" - for i in range(self.min): - self._threads.append(WorkerThread(self.server)) - for worker in self._threads: - worker.setName('CP Server ' + worker.getName()) - worker.start() - for worker in self._threads: - while not worker.ready: - time.sleep(.1) - - @property - def idle(self): # noqa: D401; irrelevant for properties - """Number of worker threads which are idle. Read-only.""" - return len([t for t in self._threads if t.conn is None]) - - def put(self, obj): - """Put request into queue. - - Args: - obj (cheroot.server.HTTPConnection): HTTP connection - waiting to be processed - """ - self._queue.put(obj, block=True, timeout=self._queue_put_timeout) - if obj is _SHUTDOWNREQUEST: - return - - def grow(self, amount): - """Spawn new worker threads (not above self.max).""" - if self.max > 0: - budget = max(self.max - len(self._threads), 0) - else: - # self.max <= 0 indicates no maximum - budget = float('inf') - - n_new = min(amount, budget) - - workers = [self._spawn_worker() for i in range(n_new)] - while not all(worker.ready for worker in workers): - time.sleep(.1) - self._threads.extend(workers) - - def _spawn_worker(self): - worker = WorkerThread(self.server) - worker.setName('CP Server ' + worker.getName()) - worker.start() - return worker - - def shrink(self, amount): - """Kill off worker threads (not below self.min).""" - # Grow/shrink the pool if necessary. - # Remove any dead threads from our list - for t in self._threads: - if not t.isAlive(): - self._threads.remove(t) - amount -= 1 - - # calculate the number of threads above the minimum - n_extra = max(len(self._threads) - self.min, 0) - - # don't remove more than amount - n_to_remove = min(amount, n_extra) - - # put shutdown requests on the queue equal to the number of threads - # to remove. As each request is processed by a worker, that worker - # will terminate and be culled from the list. - for n in range(n_to_remove): - self._queue.put(_SHUTDOWNREQUEST) - - def stop(self, timeout=5): - """Terminate all worker threads. - - Args: - timeout (int): time to wait for threads to stop gracefully - """ - # Must shut down threads here so the code that calls - # this method can know when all threads are stopped. - for worker in self._threads: - self._queue.put(_SHUTDOWNREQUEST) - - # Don't join currentThread (when stop is called inside a request). - current = threading.currentThread() - if timeout is not None and timeout >= 0: - endtime = time.time() + timeout - while self._threads: - worker = self._threads.pop() - if worker is not current and worker.isAlive(): - try: - if timeout is None or timeout < 0: - worker.join() - else: - remaining_time = endtime - time.time() - if remaining_time > 0: - worker.join(remaining_time) - if worker.isAlive(): - # We exhausted the timeout. - # Forcibly shut down the socket. - c = worker.conn - if c and not c.rfile.closed: - try: - c.socket.shutdown(socket.SHUT_RD) - except TypeError: - # pyOpenSSL sockets don't take an arg - c.socket.shutdown() - worker.join() - except (AssertionError, - # Ignore repeated Ctrl-C. - # See - # https://github.com/cherrypy/cherrypy/issues/691. - KeyboardInterrupt): - pass - - @property - def qsize(self): - """Return the queue size.""" - return self._queue.qsize() diff --git a/libraries/cheroot/wsgi.py b/libraries/cheroot/wsgi.py deleted file mode 100644 index a04c9438..00000000 --- a/libraries/cheroot/wsgi.py +++ /dev/null @@ -1,423 +0,0 @@ -"""This class holds Cheroot WSGI server implementation. - -Simplest example on how to use this server:: - - from cheroot import wsgi - - def my_crazy_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type','text/plain')] - start_response(status, response_headers) - return [b'Hello world!'] - - addr = '0.0.0.0', 8070 - server = wsgi.Server(addr, my_crazy_app) - server.start() - -The Cheroot WSGI server can serve as many WSGI applications -as you want in one instance by using a PathInfoDispatcher:: - - path_map = { - '/': my_crazy_app, - '/blog': my_blog_app, - } - d = wsgi.PathInfoDispatcher(path_map) - server = wsgi.Server(addr, d) -""" - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import sys - -import six -from six.moves import filter - -from . import server -from .workers import threadpool -from ._compat import ntob, bton - - -class Server(server.HTTPServer): - """A subclass of HTTPServer which calls a WSGI application.""" - - wsgi_version = (1, 0) - """The version of WSGI to produce.""" - - def __init__( - self, bind_addr, wsgi_app, numthreads=10, server_name=None, - max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5, - accepted_queue_size=-1, accepted_queue_timeout=10, - peercreds_enabled=False, peercreds_resolve_enabled=False, - ): - """Initialize WSGI Server instance. - - Args: - bind_addr (tuple): network interface to listen to - wsgi_app (callable): WSGI application callable - numthreads (int): number of threads for WSGI thread pool - server_name (str): web server name to be advertised via - Server HTTP header - max (int): maximum number of worker threads - request_queue_size (int): the 'backlog' arg to - socket.listen(); max queued connections - timeout (int): the timeout in seconds for accepted connections - shutdown_timeout (int): the total time, in seconds, to - wait for worker threads to cleanly exit - accepted_queue_size (int): maximum number of active - requests in queue - accepted_queue_timeout (int): timeout for putting request - into queue - """ - super(Server, self).__init__( - bind_addr, - gateway=wsgi_gateways[self.wsgi_version], - server_name=server_name, - peercreds_enabled=peercreds_enabled, - peercreds_resolve_enabled=peercreds_resolve_enabled, - ) - self.wsgi_app = wsgi_app - self.request_queue_size = request_queue_size - self.timeout = timeout - self.shutdown_timeout = shutdown_timeout - self.requests = threadpool.ThreadPool( - self, min=numthreads or 1, max=max, - accepted_queue_size=accepted_queue_size, - accepted_queue_timeout=accepted_queue_timeout) - - @property - def numthreads(self): - """Set minimum number of threads.""" - return self.requests.min - - @numthreads.setter - def numthreads(self, value): - self.requests.min = value - - -class Gateway(server.Gateway): - """A base class to interface HTTPServer with WSGI.""" - - def __init__(self, req): - """Initialize WSGI Gateway instance with request. - - Args: - req (HTTPRequest): current HTTP request - """ - super(Gateway, self).__init__(req) - self.started_response = False - self.env = self.get_environ() - self.remaining_bytes_out = None - - @classmethod - def gateway_map(cls): - """Create a mapping of gateways and their versions. - - Returns: - dict[tuple[int,int],class]: map of gateway version and - corresponding class - - """ - return dict( - (gw.version, gw) - for gw in cls.__subclasses__() - ) - - def get_environ(self): - """Return a new environ dict targeting the given wsgi.version.""" - raise NotImplementedError - - def respond(self): - """Process the current request. - - From :pep:`333`: - - The start_response callable must not actually transmit - the response headers. Instead, it must store them for the - server or gateway to transmit only after the first - iteration of the application return value that yields - a NON-EMPTY string, or upon the application's first - invocation of the write() callable. - """ - response = self.req.server.wsgi_app(self.env, self.start_response) - try: - for chunk in filter(None, response): - if not isinstance(chunk, six.binary_type): - raise ValueError('WSGI Applications must yield bytes') - self.write(chunk) - finally: - # Send headers if not already sent - self.req.ensure_headers_sent() - if hasattr(response, 'close'): - response.close() - - def start_response(self, status, headers, exc_info=None): - """WSGI callable to begin the HTTP response.""" - # "The application may call start_response more than once, - # if and only if the exc_info argument is provided." - if self.started_response and not exc_info: - raise AssertionError('WSGI start_response called a second ' - 'time with no exc_info.') - self.started_response = True - - # "if exc_info is provided, and the HTTP headers have already been - # sent, start_response must raise an error, and should raise the - # exc_info tuple." - if self.req.sent_headers: - try: - six.reraise(*exc_info) - finally: - exc_info = None - - self.req.status = self._encode_status(status) - - for k, v in headers: - if not isinstance(k, str): - raise TypeError( - 'WSGI response header key %r is not of type str.' % k) - if not isinstance(v, str): - raise TypeError( - 'WSGI response header value %r is not of type str.' % v) - if k.lower() == 'content-length': - self.remaining_bytes_out = int(v) - out_header = ntob(k), ntob(v) - self.req.outheaders.append(out_header) - - return self.write - - @staticmethod - def _encode_status(status): - """Cast status to bytes representation of current Python version. - - According to :pep:`3333`, when using Python 3, the response status - and headers must be bytes masquerading as unicode; that is, they - must be of type "str" but are restricted to code points in the - "latin-1" set. - """ - if six.PY2: - return status - if not isinstance(status, str): - raise TypeError('WSGI response status is not of type str.') - return status.encode('ISO-8859-1') - - def write(self, chunk): - """WSGI callable to write unbuffered data to the client. - - This method is also used internally by start_response (to write - data from the iterable returned by the WSGI application). - """ - if not self.started_response: - raise AssertionError('WSGI write called before start_response.') - - chunklen = len(chunk) - rbo = self.remaining_bytes_out - if rbo is not None and chunklen > rbo: - if not self.req.sent_headers: - # Whew. We can send a 500 to the client. - self.req.simple_response( - '500 Internal Server Error', - 'The requested resource returned more bytes than the ' - 'declared Content-Length.') - else: - # Dang. We have probably already sent data. Truncate the chunk - # to fit (so the client doesn't hang) and raise an error later. - chunk = chunk[:rbo] - - self.req.ensure_headers_sent() - - self.req.write(chunk) - - if rbo is not None: - rbo -= chunklen - if rbo < 0: - raise ValueError( - 'Response body exceeds the declared Content-Length.') - - -class Gateway_10(Gateway): - """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" - - version = 1, 0 - - def get_environ(self): - """Return a new environ dict targeting the given wsgi.version.""" - req = self.req - req_conn = req.conn - env = { - # set a non-standard environ entry so the WSGI app can know what - # the *real* server protocol is (and what features to support). - # See http://www.faqs.org/rfcs/rfc2145.html. - 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, - 'PATH_INFO': bton(req.path), - 'QUERY_STRING': bton(req.qs), - 'REMOTE_ADDR': req_conn.remote_addr or '', - 'REMOTE_PORT': str(req_conn.remote_port or ''), - 'REQUEST_METHOD': bton(req.method), - 'REQUEST_URI': bton(req.uri), - 'SCRIPT_NAME': '', - 'SERVER_NAME': req.server.server_name, - # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. - 'SERVER_PROTOCOL': bton(req.request_protocol), - 'SERVER_SOFTWARE': req.server.software, - 'wsgi.errors': sys.stderr, - 'wsgi.input': req.rfile, - 'wsgi.input_terminated': bool(req.chunked_read), - 'wsgi.multiprocess': False, - 'wsgi.multithread': True, - 'wsgi.run_once': False, - 'wsgi.url_scheme': bton(req.scheme), - 'wsgi.version': self.version, - } - - if isinstance(req.server.bind_addr, six.string_types): - # AF_UNIX. This isn't really allowed by WSGI, which doesn't - # address unix domain sockets. But it's better than nothing. - env['SERVER_PORT'] = '' - try: - env['X_REMOTE_PID'] = str(req_conn.peer_pid) - env['X_REMOTE_UID'] = str(req_conn.peer_uid) - env['X_REMOTE_GID'] = str(req_conn.peer_gid) - - env['X_REMOTE_USER'] = str(req_conn.peer_user) - env['X_REMOTE_GROUP'] = str(req_conn.peer_group) - - env['REMOTE_USER'] = env['X_REMOTE_USER'] - except RuntimeError: - """Unable to retrieve peer creds data. - - Unsupported by current kernel or socket error happened, or - unsupported socket type, or disabled. - """ - else: - env['SERVER_PORT'] = str(req.server.bind_addr[1]) - - # Request headers - env.update( - ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v)) - for k, v in req.inheaders.items() - ) - - # CONTENT_TYPE/CONTENT_LENGTH - ct = env.pop('HTTP_CONTENT_TYPE', None) - if ct is not None: - env['CONTENT_TYPE'] = ct - cl = env.pop('HTTP_CONTENT_LENGTH', None) - if cl is not None: - env['CONTENT_LENGTH'] = cl - - if req.conn.ssl_env: - env.update(req.conn.ssl_env) - - return env - - -class Gateway_u0(Gateway_10): - """A Gateway class to interface HTTPServer with WSGI u.0. - - WSGI u.0 is an experimental protocol, which uses unicode for keys - and values in both Python 2 and Python 3. - """ - - version = 'u', 0 - - def get_environ(self): - """Return a new environ dict targeting the given wsgi.version.""" - req = self.req - env_10 = super(Gateway_u0, self).get_environ() - env = dict(map(self._decode_key, env_10.items())) - - # Request-URI - enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8')) - try: - env['PATH_INFO'] = req.path.decode(enc) - env['QUERY_STRING'] = req.qs.decode(enc) - except UnicodeDecodeError: - # Fall back to latin 1 so apps can transcode if needed. - env['wsgi.url_encoding'] = 'ISO-8859-1' - env['PATH_INFO'] = env_10['PATH_INFO'] - env['QUERY_STRING'] = env_10['QUERY_STRING'] - - env.update(map(self._decode_value, env.items())) - - return env - - @staticmethod - def _decode_key(item): - k, v = item - if six.PY2: - k = k.decode('ISO-8859-1') - return k, v - - @staticmethod - def _decode_value(item): - k, v = item - skip_keys = 'REQUEST_URI', 'wsgi.input' - if six.PY3 or not isinstance(v, bytes) or k in skip_keys: - return k, v - return k, v.decode('ISO-8859-1') - - -wsgi_gateways = Gateway.gateway_map() - - -class PathInfoDispatcher: - """A WSGI dispatcher for dispatch based on the PATH_INFO.""" - - def __init__(self, apps): - """Initialize path info WSGI app dispatcher. - - Args: - apps (dict[str,object]|list[tuple[str,object]]): URI prefix - and WSGI app pairs - """ - try: - apps = list(apps.items()) - except AttributeError: - pass - - # Sort the apps by len(path), descending - def by_path_len(app): - return len(app[0]) - apps.sort(key=by_path_len, reverse=True) - - # The path_prefix strings must start, but not end, with a slash. - # Use "" instead of "/". - self.apps = [(p.rstrip('/'), a) for p, a in apps] - - def __call__(self, environ, start_response): - """Process incoming WSGI request. - - Ref: :pep:`3333` - - Args: - environ (Mapping): a dict containing WSGI environment variables - start_response (callable): function, which sets response - status and headers - - Returns: - list[bytes]: iterable containing bytes to be returned in - HTTP response body - - """ - path = environ['PATH_INFO'] or '/' - for p, app in self.apps: - # The apps list should be sorted by length, descending. - if path.startswith(p + '/') or path == p: - environ = environ.copy() - environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p - environ['PATH_INFO'] = path[len(p):] - return app(environ, start_response) - - start_response('404 Not Found', [('Content-Type', 'text/plain'), - ('Content-Length', '0')]) - return [''] - - -# compatibility aliases -globals().update( - WSGIServer=Server, - WSGIGateway=Gateway, - WSGIGateway_u0=Gateway_u0, - WSGIGateway_10=Gateway_10, - WSGIPathInfoDispatcher=PathInfoDispatcher, -) diff --git a/libraries/cherrypy/__init__.py b/libraries/cherrypy/__init__.py deleted file mode 100644 index c5925980..00000000 --- a/libraries/cherrypy/__init__.py +++ /dev/null @@ -1,362 +0,0 @@ -"""CherryPy is a pythonic, object-oriented HTTP framework. - -CherryPy consists of not one, but four separate API layers. - -The APPLICATION LAYER is the simplest. CherryPy applications are written as -a tree of classes and methods, where each branch in the tree corresponds to -a branch in the URL path. Each method is a 'page handler', which receives -GET and POST params as keyword arguments, and returns or yields the (HTML) -body of the response. The special method name 'index' is used for paths -that end in a slash, and the special method name 'default' is used to -handle multiple paths via a single handler. This layer also includes: - - * the 'exposed' attribute (and cherrypy.expose) - * cherrypy.quickstart() - * _cp_config attributes - * cherrypy.tools (including cherrypy.session) - * cherrypy.url() - -The ENVIRONMENT LAYER is used by developers at all levels. It provides -information about the current request and response, plus the application -and server environment, via a (default) set of top-level objects: - - * cherrypy.request - * cherrypy.response - * cherrypy.engine - * cherrypy.server - * cherrypy.tree - * cherrypy.config - * cherrypy.thread_data - * cherrypy.log - * cherrypy.HTTPError, NotFound, and HTTPRedirect - * cherrypy.lib - -The EXTENSION LAYER allows advanced users to construct and share their own -plugins. It consists of: - - * Hook API - * Tool API - * Toolbox API - * Dispatch API - * Config Namespace API - -Finally, there is the CORE LAYER, which uses the core API's to construct -the default components which are available at higher layers. You can think -of the default components as the 'reference implementation' for CherryPy. -Megaframeworks (and advanced users) may replace the default components -with customized or extended components. The core API's are: - - * Application API - * Engine API - * Request API - * Server API - * WSGI API - -These API's are described in the `CherryPy specification -<https://github.com/cherrypy/cherrypy/wiki/CherryPySpec>`_. -""" - -from threading import local as _local - -from ._cperror import ( - HTTPError, HTTPRedirect, InternalRedirect, - NotFound, CherryPyException, -) - -from . import _cpdispatch as dispatch - -from ._cptools import default_toolbox as tools, Tool -from ._helper import expose, popargs, url - -from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig - -import cherrypy.lib.httputil as _httputil - -from ._cptree import Application -from . import _cpwsgi as wsgi - -from . import process -try: - from .process import win32 - engine = win32.Win32Bus() - engine.console_control_handler = win32.ConsoleCtrlHandler(engine) - del win32 -except ImportError: - engine = process.bus - -from . import _cpchecker - -__all__ = ( - 'HTTPError', 'HTTPRedirect', 'InternalRedirect', - 'NotFound', 'CherryPyException', - 'dispatch', 'tools', 'Tool', 'Application', - 'wsgi', 'process', 'tree', 'engine', - 'quickstart', 'serving', 'request', 'response', 'thread_data', - 'log', 'expose', 'popargs', 'url', 'config', -) - - -__import__('cherrypy._cptools') -__import__('cherrypy._cprequest') - - -tree = _cptree.Tree() - - -__version__ = '17.4.0' - - -engine.listeners['before_request'] = set() -engine.listeners['after_request'] = set() - - -engine.autoreload = process.plugins.Autoreloader(engine) -engine.autoreload.subscribe() - -engine.thread_manager = process.plugins.ThreadManager(engine) -engine.thread_manager.subscribe() - -engine.signal_handler = process.plugins.SignalHandler(engine) - - -class _HandleSignalsPlugin(object): - """Handle signals from other processes. - - Based on the configured platform handlers above. - """ - - def __init__(self, bus): - self.bus = bus - - def subscribe(self): - """Add the handlers based on the platform.""" - if hasattr(self.bus, 'signal_handler'): - self.bus.signal_handler.subscribe() - if hasattr(self.bus, 'console_control_handler'): - self.bus.console_control_handler.subscribe() - - -engine.signals = _HandleSignalsPlugin(engine) - - -server = _cpserver.Server() -server.subscribe() - - -def quickstart(root=None, script_name='', config=None): - """Mount the given root, start the builtin server (and engine), then block. - - root: an instance of a "controller class" (a collection of page handler - methods) which represents the root of the application. - script_name: a string containing the "mount point" of the application. - This should start with a slash, and be the path portion of the URL - at which to mount the given root. For example, if root.index() will - handle requests to "http://www.example.com:8080/dept/app1/", then - the script_name argument would be "/dept/app1". - - It MUST NOT end in a slash. If the script_name refers to the root - of the URI, it MUST be an empty string (not "/"). - config: a file or dict containing application config. If this contains - a [global] section, those entries will be used in the global - (site-wide) config. - """ - if config: - _global_conf_alias.update(config) - - tree.mount(root, script_name, config) - - engine.signals.subscribe() - engine.start() - engine.block() - - -class _Serving(_local): - """An interface for registering request and response objects. - - Rather than have a separate "thread local" object for the request and - the response, this class works as a single threadlocal container for - both objects (and any others which developers wish to define). In this - way, we can easily dump those objects when we stop/start a new HTTP - conversation, yet still refer to them as module-level globals in a - thread-safe way. - """ - - request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), - _httputil.Host('127.0.0.1', 1111)) - """ - The request object for the current thread. In the main thread, - and any threads which are not receiving HTTP requests, this is None.""" - - response = _cprequest.Response() - """ - The response object for the current thread. In the main thread, - and any threads which are not receiving HTTP requests, this is None.""" - - def load(self, request, response): - self.request = request - self.response = response - - def clear(self): - """Remove all attributes of self.""" - self.__dict__.clear() - - -serving = _Serving() - - -class _ThreadLocalProxy(object): - - __slots__ = ['__attrname__', '__dict__'] - - def __init__(self, attrname): - self.__attrname__ = attrname - - def __getattr__(self, name): - child = getattr(serving, self.__attrname__) - return getattr(child, name) - - def __setattr__(self, name, value): - if name in ('__attrname__', ): - object.__setattr__(self, name, value) - else: - child = getattr(serving, self.__attrname__) - setattr(child, name, value) - - def __delattr__(self, name): - child = getattr(serving, self.__attrname__) - delattr(child, name) - - @property - def __dict__(self): - child = getattr(serving, self.__attrname__) - d = child.__class__.__dict__.copy() - d.update(child.__dict__) - return d - - def __getitem__(self, key): - child = getattr(serving, self.__attrname__) - return child[key] - - def __setitem__(self, key, value): - child = getattr(serving, self.__attrname__) - child[key] = value - - def __delitem__(self, key): - child = getattr(serving, self.__attrname__) - del child[key] - - def __contains__(self, key): - child = getattr(serving, self.__attrname__) - return key in child - - def __len__(self): - child = getattr(serving, self.__attrname__) - return len(child) - - def __nonzero__(self): - child = getattr(serving, self.__attrname__) - return bool(child) - # Python 3 - __bool__ = __nonzero__ - - -# Create request and response object (the same objects will be used -# throughout the entire life of the webserver, but will redirect -# to the "serving" object) -request = _ThreadLocalProxy('request') -response = _ThreadLocalProxy('response') - -# Create thread_data object as a thread-specific all-purpose storage - - -class _ThreadData(_local): - """A container for thread-specific data.""" - - -thread_data = _ThreadData() - - -# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. -# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. -# The only other way would be to change what is returned from type(request) -# and that's not possible in pure Python (you'd have to fake ob_type). -def _cherrypy_pydoc_resolve(thing, forceload=0): - """Given an object or a path to an object, get the object and its name.""" - if isinstance(thing, _ThreadLocalProxy): - thing = getattr(serving, thing.__attrname__) - return _pydoc._builtin_resolve(thing, forceload) - - -try: - import pydoc as _pydoc - _pydoc._builtin_resolve = _pydoc.resolve - _pydoc.resolve = _cherrypy_pydoc_resolve -except ImportError: - pass - - -class _GlobalLogManager(_cplogging.LogManager): - """A site-wide LogManager; routes to app.log or global log as appropriate. - - This :class:`LogManager<cherrypy._cplogging.LogManager>` implements - cherrypy.log() and cherrypy.log.access(). If either - function is called during a request, the message will be sent to the - logger for the current Application. If they are called outside of a - request, the message will be sent to the site-wide logger. - """ - - def __call__(self, *args, **kwargs): - """Log the given message to the app.log or global log. - - Log the given message to the app.log or global - log as appropriate. - """ - # Do NOT use try/except here. See - # https://github.com/cherrypy/cherrypy/issues/945 - if hasattr(request, 'app') and hasattr(request.app, 'log'): - log = request.app.log - else: - log = self - return log.error(*args, **kwargs) - - def access(self): - """Log an access message to the app.log or global log. - - Log the given message to the app.log or global - log as appropriate. - """ - try: - return request.app.log.access() - except AttributeError: - return _cplogging.LogManager.access(self) - - -log = _GlobalLogManager() -# Set a default screen handler on the global log. -log.screen = True -log.error_file = '' -# Using an access file makes CP about 10% slower. Leave off by default. -log.access_file = '' - - -@engine.subscribe('log') -def _buslog(msg, level): - log.error(msg, 'ENGINE', severity=level) - - -# Use _global_conf_alias so quickstart can use 'config' as an arg -# without shadowing cherrypy.config. -config = _global_conf_alias = _cpconfig.Config() -config.defaults = { - 'tools.log_tracebacks.on': True, - 'tools.log_headers.on': True, - 'tools.trailing_slash.on': True, - 'tools.encode.on': True -} -config.namespaces['log'] = lambda k, v: setattr(log, k, v) -config.namespaces['checker'] = lambda k, v: setattr(checker, k, v) -# Must reset to get our defaults applied. -config.reset() - -checker = _cpchecker.Checker() -engine.subscribe('start', checker) diff --git a/libraries/cherrypy/__main__.py b/libraries/cherrypy/__main__.py deleted file mode 100644 index 6674f7cb..00000000 --- a/libraries/cherrypy/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""CherryPy'd cherryd daemon runner.""" -from cherrypy.daemon import run - - -__name__ == '__main__' and run() diff --git a/libraries/cherrypy/_cpchecker.py b/libraries/cherrypy/_cpchecker.py deleted file mode 100644 index 39b7c972..00000000 --- a/libraries/cherrypy/_cpchecker.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Checker for CherryPy sites and mounted apps.""" -import os -import warnings - -import six -from six.moves import builtins - -import cherrypy - - -class Checker(object): - """A checker for CherryPy sites and their mounted applications. - - When this object is called at engine startup, it executes each - of its own methods whose names start with ``check_``. If you wish - to disable selected checks, simply add a line in your global - config which sets the appropriate method to False:: - - [global] - checker.check_skipped_app_config = False - - You may also dynamically add or replace ``check_*`` methods in this way. - """ - - on = True - """If True (the default), run all checks; if False, turn off all checks.""" - - def __init__(self): - """Initialize Checker instance.""" - self._populate_known_types() - - def __call__(self): - """Run all check_* methods.""" - if self.on: - oldformatwarning = warnings.formatwarning - warnings.formatwarning = self.formatwarning - try: - for name in dir(self): - if name.startswith('check_'): - method = getattr(self, name) - if method and hasattr(method, '__call__'): - method() - finally: - warnings.formatwarning = oldformatwarning - - def formatwarning(self, message, category, filename, lineno, line=None): - """Format a warning.""" - return 'CherryPy Checker:\n%s\n\n' % message - - # This value should be set inside _cpconfig. - global_config_contained_paths = False - - def check_app_config_entries_dont_start_with_script_name(self): - """Check for App config with sections that repeat script_name.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - if sn == '': - continue - sn_atoms = sn.strip('/').split('/') - for key in app.config.keys(): - key_atoms = key.strip('/').split('/') - if key_atoms[:len(sn_atoms)] == sn_atoms: - warnings.warn( - 'The application mounted at %r has config ' - 'entries that start with its script name: %r' % (sn, - key)) - - def check_site_config_entries_in_app_config(self): - """Check for mounted Applications that have site-scoped config.""" - for sn, app in six.iteritems(cherrypy.tree.apps): - if not isinstance(app, cherrypy.Application): - continue - - msg = [] - for section, entries in six.iteritems(app.config): - if section.startswith('/'): - for key, value in six.iteritems(entries): - for n in ('engine.', 'server.', 'tree.', 'checker.'): - if key.startswith(n): - msg.append('[%s] %s = %s' % - (section, key, value)) - if msg: - msg.insert(0, - 'The application mounted at %r contains the ' - 'following config entries, which are only allowed ' - 'in site-wide config. Move them to a [global] ' - 'section and pass them to cherrypy.config.update() ' - 'instead of tree.mount().' % sn) - warnings.warn(os.linesep.join(msg)) - - def check_skipped_app_config(self): - """Check for mounted Applications that have no config.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - msg = 'The Application mounted at %r has an empty config.' % sn - if self.global_config_contained_paths: - msg += (' It looks like the config you passed to ' - 'cherrypy.config.update() contains application-' - 'specific sections. You must explicitly pass ' - 'application config via ' - 'cherrypy.tree.mount(..., config=app_config)') - warnings.warn(msg) - return - - def check_app_config_brackets(self): - """Check for App config with extraneous brackets in section names.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - for key in app.config.keys(): - if key.startswith('[') or key.endswith(']'): - warnings.warn( - 'The application mounted at %r has config ' - 'section names with extraneous brackets: %r. ' - 'Config *files* need brackets; config *dicts* ' - '(e.g. passed to tree.mount) do not.' % (sn, key)) - - def check_static_paths(self): - """Check Application config for incorrect static paths.""" - # Use the dummy Request object in the main thread. - request = cherrypy.request - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - request.app = app - for section in app.config: - # get_resource will populate request.config - request.get_resource(section + '/dummy.html') - conf = request.config.get - - if conf('tools.staticdir.on', False): - msg = '' - root = conf('tools.staticdir.root') - dir = conf('tools.staticdir.dir') - if dir is None: - msg = 'tools.staticdir.dir is not set.' - else: - fulldir = '' - if os.path.isabs(dir): - fulldir = dir - if root: - msg = ('dir is an absolute path, even ' - 'though a root is provided.') - testdir = os.path.join(root, dir[1:]) - if os.path.exists(testdir): - msg += ( - '\nIf you meant to serve the ' - 'filesystem folder at %r, remove the ' - 'leading slash from dir.' % (testdir,)) - else: - if not root: - msg = ( - 'dir is a relative path and ' - 'no root provided.') - else: - fulldir = os.path.join(root, dir) - if not os.path.isabs(fulldir): - msg = ('%r is not an absolute path.' % ( - fulldir,)) - - if fulldir and not os.path.exists(fulldir): - if msg: - msg += '\n' - msg += ('%r (root + dir) is not an existing ' - 'filesystem path.' % fulldir) - - if msg: - warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r' - % (msg, section, root, dir)) - - # -------------------------- Compatibility -------------------------- # - obsolete = { - 'server.default_content_type': 'tools.response_headers.headers', - 'log_access_file': 'log.access_file', - 'log_config_options': None, - 'log_file': 'log.error_file', - 'log_file_not_found': None, - 'log_request_headers': 'tools.log_headers.on', - 'log_to_screen': 'log.screen', - 'show_tracebacks': 'request.show_tracebacks', - 'throw_errors': 'request.throw_errors', - 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' - 'cherrypy.Application(Root())))'), - } - - deprecated = {} - - def _compat(self, config): - """Process config and warn on each obsolete or deprecated entry.""" - for section, conf in config.items(): - if isinstance(conf, dict): - for k in conf: - if k in self.obsolete: - warnings.warn('%r is obsolete. Use %r instead.\n' - 'section: [%s]' % - (k, self.obsolete[k], section)) - elif k in self.deprecated: - warnings.warn('%r is deprecated. Use %r instead.\n' - 'section: [%s]' % - (k, self.deprecated[k], section)) - else: - if section in self.obsolete: - warnings.warn('%r is obsolete. Use %r instead.' - % (section, self.obsolete[section])) - elif section in self.deprecated: - warnings.warn('%r is deprecated. Use %r instead.' - % (section, self.deprecated[section])) - - def check_compatibility(self): - """Process config and warn on each obsolete or deprecated entry.""" - self._compat(cherrypy.config) - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - self._compat(app.config) - - # ------------------------ Known Namespaces ------------------------ # - extra_config_namespaces = [] - - def _known_ns(self, app): - ns = ['wsgi'] - ns.extend(app.toolboxes) - ns.extend(app.namespaces) - ns.extend(app.request_class.namespaces) - ns.extend(cherrypy.config.namespaces) - ns += self.extra_config_namespaces - - for section, conf in app.config.items(): - is_path_section = section.startswith('/') - if is_path_section and isinstance(conf, dict): - for k in conf: - atoms = k.split('.') - if len(atoms) > 1: - if atoms[0] not in ns: - # Spit out a special warning if a known - # namespace is preceded by "cherrypy." - if atoms[0] == 'cherrypy' and atoms[1] in ns: - msg = ( - 'The config entry %r is invalid; ' - 'try %r instead.\nsection: [%s]' - % (k, '.'.join(atoms[1:]), section)) - else: - msg = ( - 'The config entry %r is invalid, ' - 'because the %r config namespace ' - 'is unknown.\n' - 'section: [%s]' % (k, atoms[0], section)) - warnings.warn(msg) - elif atoms[0] == 'tools': - if atoms[1] not in dir(cherrypy.tools): - msg = ( - 'The config entry %r may be invalid, ' - 'because the %r tool was not found.\n' - 'section: [%s]' % (k, atoms[1], section)) - warnings.warn(msg) - - def check_config_namespaces(self): - """Process config and warn on each unknown config namespace.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - self._known_ns(app) - - # -------------------------- Config Types -------------------------- # - known_config_types = {} - - def _populate_known_types(self): - b = [x for x in vars(builtins).values() - if type(x) is type(str)] - - def traverse(obj, namespace): - for name in dir(obj): - # Hack for 3.2's warning about body_params - if name == 'body_params': - continue - vtype = type(getattr(obj, name, None)) - if vtype in b: - self.known_config_types[namespace + '.' + name] = vtype - - traverse(cherrypy.request, 'request') - traverse(cherrypy.response, 'response') - traverse(cherrypy.server, 'server') - traverse(cherrypy.engine, 'engine') - traverse(cherrypy.log, 'log') - - def _known_types(self, config): - msg = ('The config entry %r in section %r is of type %r, ' - 'which does not match the expected type %r.') - - for section, conf in config.items(): - if not isinstance(conf, dict): - conf = {section: conf} - for k, v in conf.items(): - if v is not None: - expected_type = self.known_config_types.get(k, None) - vtype = type(v) - if expected_type and vtype != expected_type: - warnings.warn(msg % (k, section, vtype.__name__, - expected_type.__name__)) - - def check_config_types(self): - """Assert that config values are of the same type as default values.""" - self._known_types(cherrypy.config) - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - self._known_types(app.config) - - # -------------------- Specific config warnings -------------------- # - def check_localhost(self): - """Warn if any socket_host is 'localhost'. See #711.""" - for k, v in cherrypy.config.items(): - if k == 'server.socket_host' and v == 'localhost': - warnings.warn("The use of 'localhost' as a socket host can " - 'cause problems on newer systems, since ' - "'localhost' can map to either an IPv4 or an " - "IPv6 address. You should use '127.0.0.1' " - "or '[::1]' instead.") diff --git a/libraries/cherrypy/_cpcompat.py b/libraries/cherrypy/_cpcompat.py deleted file mode 100644 index f454505c..00000000 --- a/libraries/cherrypy/_cpcompat.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Compatibility code for using CherryPy with various versions of Python. - -To retain compatibility with older Python versions, this module provides a -useful abstraction over the differences between Python versions, sometimes by -preferring a newer idiom, sometimes an older one, and sometimes a custom one. - -In particular, Python 2 uses str and '' for byte strings, while Python 3 -uses str and '' for unicode strings. We will call each of these the 'native -string' type for each version. Because of this major difference, this module -provides -two functions: 'ntob', which translates native strings (of type 'str') into -byte strings regardless of Python version, and 'ntou', which translates native -strings to unicode strings. - -Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'. -They were created with Python 2.3-2.5 compatibility in mind. -Instead, use unicode literals (from __future__) and bytes literals -and their .encode/.decode methods as needed. -""" - -import re -import sys -import threading - -import six -from six.moves import urllib - - -if six.PY3: - def ntob(n, encoding='ISO-8859-1'): - """Return the given native string as a byte string in the given - encoding. - """ - assert_native(n) - # In Python 3, the native string type is unicode - return n.encode(encoding) - - def ntou(n, encoding='ISO-8859-1'): - """Return the given native string as a unicode string with the given - encoding. - """ - assert_native(n) - # In Python 3, the native string type is unicode - return n - - def tonative(n, encoding='ISO-8859-1'): - """Return the given string as a native string in the given encoding.""" - # In Python 3, the native string type is unicode - if isinstance(n, bytes): - return n.decode(encoding) - return n -else: - # Python 2 - def ntob(n, encoding='ISO-8859-1'): - """Return the given native string as a byte string in the given - encoding. - """ - assert_native(n) - # In Python 2, the native string type is bytes. Assume it's already - # in the given encoding, which for ISO-8859-1 is almost always what - # was intended. - return n - - def ntou(n, encoding='ISO-8859-1'): - """Return the given native string as a unicode string with the given - encoding. - """ - assert_native(n) - # In Python 2, the native string type is bytes. - # First, check for the special encoding 'escape'. The test suite uses - # this to signal that it wants to pass a string with embedded \uXXXX - # escapes, but without having to prefix it with u'' for Python 2, - # but no prefix for Python 3. - if encoding == 'escape': - return six.text_type( # unicode for Python 2 - re.sub(r'\\u([0-9a-zA-Z]{4})', - lambda m: six.unichr(int(m.group(1), 16)), - n.decode('ISO-8859-1'))) - # Assume it's already in the given encoding, which for ISO-8859-1 - # is almost always what was intended. - return n.decode(encoding) - - def tonative(n, encoding='ISO-8859-1'): - """Return the given string as a native string in the given encoding.""" - # In Python 2, the native string type is bytes. - if isinstance(n, six.text_type): # unicode for Python 2 - return n.encode(encoding) - return n - - -def assert_native(n): - if not isinstance(n, str): - raise TypeError('n must be a native str (got %s)' % type(n).__name__) - - -# Some platforms don't expose HTTPSConnection, so handle it separately -HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None) - - -def _unquote_plus_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.unquote_plus(string).decode(encoding, errors) - - -def _unquote_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.unquote(string).decode(encoding, errors) - - -def _quote_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.quote(string.encode(encoding, errors)) - - -unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat -unquote = urllib.parse.unquote if six.PY3 else _unquote_compat -quote = urllib.parse.quote if six.PY3 else _quote_compat - -try: - # Prefer simplejson - import simplejson as json -except ImportError: - import json - - -json_decode = json.JSONDecoder().decode -_json_encode = json.JSONEncoder().iterencode - - -if six.PY3: - # Encode to bytes on Python 3 - def json_encode(value): - for chunk in _json_encode(value): - yield chunk.encode('utf-8') -else: - json_encode = _json_encode - - -text_or_bytes = six.text_type, bytes - - -if sys.version_info >= (3, 3): - Timer = threading.Timer - Event = threading.Event -else: - # Python 3.2 and earlier - Timer = threading._Timer - Event = threading._Event - -# html module come in 3.2 version -try: - from html import escape -except ImportError: - from cgi import escape - - -# html module needed the argument quote=False because in cgi the default -# is False. With quote=True the results differ. - -def escape_html(s, escape_quote=False): - """Replace special characters "&", "<" and ">" to HTML-safe sequences. - - When escape_quote=True, escape (') and (") chars. - """ - return escape(s, quote=escape_quote) diff --git a/libraries/cherrypy/_cpconfig.py b/libraries/cherrypy/_cpconfig.py deleted file mode 100644 index 79d9d911..00000000 --- a/libraries/cherrypy/_cpconfig.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -Configuration system for CherryPy. - -Configuration in CherryPy is implemented via dictionaries. Keys are strings -which name the mapped value, which may be of any type. - - -Architecture ------------- - -CherryPy Requests are part of an Application, which runs in a global context, -and configuration data may apply to any of those three scopes: - -Global - Configuration entries which apply everywhere are stored in - cherrypy.config. - -Application - Entries which apply to each mounted application are stored - on the Application object itself, as 'app.config'. This is a two-level - dict where each key is a path, or "relative URL" (for example, "/" or - "/path/to/my/page"), and each value is a config dict. Usually, this - data is provided in the call to tree.mount(root(), config=conf), - although you may also use app.merge(conf). - -Request - Each Request object possesses a single 'Request.config' dict. - Early in the request process, this dict is populated by merging global - config entries, Application entries (whose path equals or is a parent - of Request.path_info), and any config acquired while looking up the - page handler (see next). - - -Declaration ------------ - -Configuration data may be supplied as a Python dictionary, as a filename, -or as an open file object. When you supply a filename or file, CherryPy -uses Python's builtin ConfigParser; you declare Application config by -writing each path as a section header:: - - [/path/to/my/page] - request.stream = True - -To declare global configuration entries, place them in a [global] section. - -You may also declare config entries directly on the classes and methods -(page handlers) that make up your CherryPy application via the ``_cp_config`` -attribute, set with the ``cherrypy.config`` decorator. For example:: - - @cherrypy.config(**{'tools.gzip.on': True}) - class Demo: - - @cherrypy.expose - @cherrypy.config(**{'request.show_tracebacks': False}) - def index(self): - return "Hello world" - -.. note:: - - This behavior is only guaranteed for the default dispatcher. - Other dispatchers may have different restrictions on where - you can attach config attributes. - - -Namespaces ----------- - -Configuration keys are separated into namespaces by the first "." in the key. -Current namespaces: - -engine - Controls the 'application engine', including autoreload. - These can only be declared in the global config. - -tree - Grafts cherrypy.Application objects onto cherrypy.tree. - These can only be declared in the global config. - -hooks - Declares additional request-processing functions. - -log - Configures the logging for each application. - These can only be declared in the global or / config. - -request - Adds attributes to each Request. - -response - Adds attributes to each Response. - -server - Controls the default HTTP server via cherrypy.server. - These can only be declared in the global config. - -tools - Runs and configures additional request-processing packages. - -wsgi - Adds WSGI middleware to an Application's "pipeline". - These can only be declared in the app's root config ("/"). - -checker - Controls the 'checker', which looks for common errors in - app state (including config) when the engine starts. - Global config only. - -The only key that does not exist in a namespace is the "environment" entry. -This special entry 'imports' other config entries from a template stored in -cherrypy._cpconfig.environments[environment]. It only applies to the global -config, and only when you use cherrypy.config.update. - -You can define your own namespaces to be called at the Global, Application, -or Request level, by adding a named handler to cherrypy.config.namespaces, -app.namespaces, or app.request_class.namespaces. The name can -be any string, and the handler must be either a callable or a (Python 2.5 -style) context manager. -""" - -import cherrypy -from cherrypy._cpcompat import text_or_bytes -from cherrypy.lib import reprconf - - -def _if_filename_register_autoreload(ob): - """Register for autoreload if ob is a string (presumed filename).""" - is_filename = isinstance(ob, text_or_bytes) - is_filename and cherrypy.engine.autoreload.files.add(ob) - - -def merge(base, other): - """Merge one app config (from a dict, file, or filename) into another. - - If the given config is a filename, it will be appended to - the list of files to monitor for "autoreload" changes. - """ - _if_filename_register_autoreload(other) - - # Load other into base - for section, value_map in reprconf.Parser.load(other).items(): - if not isinstance(value_map, dict): - raise ValueError( - 'Application config must include section headers, but the ' - "config you tried to merge doesn't have any sections. " - 'Wrap your config in another dict with paths as section ' - "headers, for example: {'/': config}.") - base.setdefault(section, {}).update(value_map) - - -class Config(reprconf.Config): - """The 'global' configuration data for the entire CherryPy process.""" - - def update(self, config): - """Update self from a dict, file or filename.""" - _if_filename_register_autoreload(config) - super(Config, self).update(config) - - def _apply(self, config): - """Update self from a dict.""" - if isinstance(config.get('global'), dict): - if len(config) > 1: - cherrypy.checker.global_config_contained_paths = True - config = config['global'] - if 'tools.staticdir.dir' in config: - config['tools.staticdir.section'] = 'global' - super(Config, self)._apply(config) - - @staticmethod - def __call__(**kwargs): - """Decorate for page handlers to set _cp_config.""" - def tool_decorator(f): - _Vars(f).setdefault('_cp_config', {}).update(kwargs) - return f - return tool_decorator - - -class _Vars(object): - """Adapter allowing setting a default attribute on a function or class.""" - - def __init__(self, target): - self.target = target - - def setdefault(self, key, default): - if not hasattr(self.target, key): - setattr(self.target, key, default) - return getattr(self.target, key) - - -# Sphinx begin config.environments -Config.environments = environments = { - 'staging': { - 'engine.autoreload.on': False, - 'checker.on': False, - 'tools.log_headers.on': False, - 'request.show_tracebacks': False, - 'request.show_mismatched_params': False, - }, - 'production': { - 'engine.autoreload.on': False, - 'checker.on': False, - 'tools.log_headers.on': False, - 'request.show_tracebacks': False, - 'request.show_mismatched_params': False, - 'log.screen': False, - }, - 'embedded': { - # For use with CherryPy embedded in another deployment stack. - 'engine.autoreload.on': False, - 'checker.on': False, - 'tools.log_headers.on': False, - 'request.show_tracebacks': False, - 'request.show_mismatched_params': False, - 'log.screen': False, - 'engine.SIGHUP': None, - 'engine.SIGTERM': None, - }, - 'test_suite': { - 'engine.autoreload.on': False, - 'checker.on': False, - 'tools.log_headers.on': False, - 'request.show_tracebacks': True, - 'request.show_mismatched_params': True, - 'log.screen': False, - }, -} -# Sphinx end config.environments - - -def _server_namespace_handler(k, v): - """Config handler for the "server" namespace.""" - atoms = k.split('.', 1) - if len(atoms) > 1: - # Special-case config keys of the form 'server.servername.socket_port' - # to configure additional HTTP servers. - if not hasattr(cherrypy, 'servers'): - cherrypy.servers = {} - - servername, k = atoms - if servername not in cherrypy.servers: - from cherrypy import _cpserver - cherrypy.servers[servername] = _cpserver.Server() - # On by default, but 'on = False' can unsubscribe it (see below). - cherrypy.servers[servername].subscribe() - - if k == 'on': - if v: - cherrypy.servers[servername].subscribe() - else: - cherrypy.servers[servername].unsubscribe() - else: - setattr(cherrypy.servers[servername], k, v) - else: - setattr(cherrypy.server, k, v) - - -Config.namespaces['server'] = _server_namespace_handler - - -def _engine_namespace_handler(k, v): - """Config handler for the "engine" namespace.""" - engine = cherrypy.engine - - if k in {'SIGHUP', 'SIGTERM'}: - engine.subscribe(k, v) - return - - if '.' in k: - plugin, attrname = k.split('.', 1) - try: - plugin = getattr(engine, plugin) - except Exception as error: - setattr(engine, k, v) - else: - op = 'subscribe' if v else 'unsubscribe' - sub_unsub = getattr(plugin, op, None) - if attrname == 'on' and callable(sub_unsub): - sub_unsub() - return - setattr(plugin, attrname, v) - else: - setattr(engine, k, v) - - -Config.namespaces['engine'] = _engine_namespace_handler - - -def _tree_namespace_handler(k, v): - """Namespace handler for the 'tree' config namespace.""" - if isinstance(v, dict): - for script_name, app in v.items(): - cherrypy.tree.graft(app, script_name) - msg = 'Mounted: %s on %s' % (app, script_name or '/') - cherrypy.engine.log(msg) - else: - cherrypy.tree.graft(v, v.script_name) - cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/')) - - -Config.namespaces['tree'] = _tree_namespace_handler diff --git a/libraries/cherrypy/_cpdispatch.py b/libraries/cherrypy/_cpdispatch.py deleted file mode 100644 index 83eb79cb..00000000 --- a/libraries/cherrypy/_cpdispatch.py +++ /dev/null @@ -1,686 +0,0 @@ -"""CherryPy dispatchers. - -A 'dispatcher' is the object which looks up the 'page handler' callable -and collects config for the current request based on the path_info, other -request attributes, and the application architecture. The core calls the -dispatcher as early as possible, passing it a 'path_info' argument. - -The default dispatcher discovers the page handler by matching path_info -to a hierarchical arrangement of objects, starting at request.app.root. -""" - -import string -import sys -import types -try: - classtype = (type, types.ClassType) -except AttributeError: - classtype = type - -import cherrypy - - -class PageHandler(object): - - """Callable which sets response.body.""" - - def __init__(self, callable, *args, **kwargs): - self.callable = callable - self.args = args - self.kwargs = kwargs - - @property - def args(self): - """The ordered args should be accessible from post dispatch hooks.""" - return cherrypy.serving.request.args - - @args.setter - def args(self, args): - cherrypy.serving.request.args = args - return cherrypy.serving.request.args - - @property - def kwargs(self): - """The named kwargs should be accessible from post dispatch hooks.""" - return cherrypy.serving.request.kwargs - - @kwargs.setter - def kwargs(self, kwargs): - cherrypy.serving.request.kwargs = kwargs - return cherrypy.serving.request.kwargs - - def __call__(self): - try: - return self.callable(*self.args, **self.kwargs) - except TypeError: - x = sys.exc_info()[1] - try: - test_callable_spec(self.callable, self.args, self.kwargs) - except cherrypy.HTTPError: - raise sys.exc_info()[1] - except Exception: - raise x - raise - - -def test_callable_spec(callable, callable_args, callable_kwargs): - """ - Inspect callable and test to see if the given args are suitable for it. - - When an error occurs during the handler's invoking stage there are 2 - erroneous cases: - 1. Too many parameters passed to a function which doesn't define - one of *args or **kwargs. - 2. Too little parameters are passed to the function. - - There are 3 sources of parameters to a cherrypy handler. - 1. query string parameters are passed as keyword parameters to the - handler. - 2. body parameters are also passed as keyword parameters. - 3. when partial matching occurs, the final path atoms are passed as - positional args. - Both the query string and path atoms are part of the URI. If they are - incorrect, then a 404 Not Found should be raised. Conversely the body - parameters are part of the request; if they are invalid a 400 Bad Request. - """ - show_mismatched_params = getattr( - cherrypy.serving.request, 'show_mismatched_params', False) - try: - (args, varargs, varkw, defaults) = getargspec(callable) - except TypeError: - if isinstance(callable, object) and hasattr(callable, '__call__'): - (args, varargs, varkw, - defaults) = getargspec(callable.__call__) - else: - # If it wasn't one of our own types, re-raise - # the original error - raise - - if args and ( - # For callable objects, which have a __call__(self) method - hasattr(callable, '__call__') or - # For normal methods - inspect.ismethod(callable) - ): - # Strip 'self' - args = args[1:] - - arg_usage = dict([(arg, 0,) for arg in args]) - vararg_usage = 0 - varkw_usage = 0 - extra_kwargs = set() - - for i, value in enumerate(callable_args): - try: - arg_usage[args[i]] += 1 - except IndexError: - vararg_usage += 1 - - for key in callable_kwargs.keys(): - try: - arg_usage[key] += 1 - except KeyError: - varkw_usage += 1 - extra_kwargs.add(key) - - # figure out which args have defaults. - args_with_defaults = args[-len(defaults or []):] - for i, val in enumerate(defaults or []): - # Defaults take effect only when the arg hasn't been used yet. - if arg_usage[args_with_defaults[i]] == 0: - arg_usage[args_with_defaults[i]] += 1 - - missing_args = [] - multiple_args = [] - for key, usage in arg_usage.items(): - if usage == 0: - missing_args.append(key) - elif usage > 1: - multiple_args.append(key) - - if missing_args: - # In the case where the method allows body arguments - # there are 3 potential errors: - # 1. not enough query string parameters -> 404 - # 2. not enough body parameters -> 400 - # 3. not enough path parts (partial matches) -> 404 - # - # We can't actually tell which case it is, - # so I'm raising a 404 because that covers 2/3 of the - # possibilities - # - # In the case where the method does not allow body - # arguments it's definitely a 404. - message = None - if show_mismatched_params: - message = 'Missing parameters: %s' % ','.join(missing_args) - raise cherrypy.HTTPError(404, message=message) - - # the extra positional arguments come from the path - 404 Not Found - if not varargs and vararg_usage > 0: - raise cherrypy.HTTPError(404) - - body_params = cherrypy.serving.request.body.params or {} - body_params = set(body_params.keys()) - qs_params = set(callable_kwargs.keys()) - body_params - - if multiple_args: - if qs_params.intersection(set(multiple_args)): - # If any of the multiple parameters came from the query string then - # it's a 404 Not Found - error = 404 - else: - # Otherwise it's a 400 Bad Request - error = 400 - - message = None - if show_mismatched_params: - message = 'Multiple values for parameters: '\ - '%s' % ','.join(multiple_args) - raise cherrypy.HTTPError(error, message=message) - - if not varkw and varkw_usage > 0: - - # If there were extra query string parameters, it's a 404 Not Found - extra_qs_params = set(qs_params).intersection(extra_kwargs) - if extra_qs_params: - message = None - if show_mismatched_params: - message = 'Unexpected query string '\ - 'parameters: %s' % ', '.join(extra_qs_params) - raise cherrypy.HTTPError(404, message=message) - - # If there were any extra body parameters, it's a 400 Not Found - extra_body_params = set(body_params).intersection(extra_kwargs) - if extra_body_params: - message = None - if show_mismatched_params: - message = 'Unexpected body parameters: '\ - '%s' % ', '.join(extra_body_params) - raise cherrypy.HTTPError(400, message=message) - - -try: - import inspect -except ImportError: - 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] - - -class LateParamPageHandler(PageHandler): - - """When passing cherrypy.request.params to the page handler, we do not - want to capture that dict too early; we want to give tools like the - decoding tool a chance to modify the params dict in-between the lookup - of the handler and the actual calling of the handler. This subclass - takes that into account, and allows request.params to be 'bound late' - (it's more complicated than that, but that's the effect). - """ - - @property - def kwargs(self): - """Page handler kwargs (with cherrypy.request.params copied in).""" - kwargs = cherrypy.serving.request.params.copy() - if self._kwargs: - kwargs.update(self._kwargs) - return kwargs - - @kwargs.setter - def kwargs(self, kwargs): - cherrypy.serving.request.kwargs = kwargs - self._kwargs = kwargs - - -if sys.version_info < (3, 0): - punctuation_to_underscores = string.maketrans( - string.punctuation, '_' * len(string.punctuation)) - - def validate_translator(t): - if not isinstance(t, str) or len(t) != 256: - raise ValueError( - 'The translate argument must be a str of len 256.') -else: - punctuation_to_underscores = str.maketrans( - string.punctuation, '_' * len(string.punctuation)) - - def validate_translator(t): - if not isinstance(t, dict): - raise ValueError('The translate argument must be a dict.') - - -class Dispatcher(object): - - """CherryPy Dispatcher which walks a tree of objects to find a handler. - - The tree is rooted at cherrypy.request.app.root, and each hierarchical - component in the path_info argument is matched to a corresponding nested - attribute of the root object. Matching handlers must have an 'exposed' - attribute which evaluates to True. The special method name "index" - matches a URI which ends in a slash ("/"). The special method name - "default" may match a portion of the path_info (but only when no longer - substring of the path_info matches some other object). - - This is the default, built-in dispatcher for CherryPy. - """ - - dispatch_method_name = '_cp_dispatch' - """ - The name of the dispatch method that nodes may optionally implement - to provide their own dynamic dispatch algorithm. - """ - - def __init__(self, dispatch_method_name=None, - translate=punctuation_to_underscores): - validate_translator(translate) - self.translate = translate - if dispatch_method_name: - self.dispatch_method_name = dispatch_method_name - - def __call__(self, path_info): - """Set handler and config for the current request.""" - request = cherrypy.serving.request - func, vpath = self.find_handler(path_info) - - if func: - # Decode any leftover %2F in the virtual_path atoms. - vpath = [x.replace('%2F', '/') for x in vpath] - request.handler = LateParamPageHandler(func, *vpath) - else: - request.handler = cherrypy.NotFound() - - def find_handler(self, path): - """Return the appropriate page handler, plus any virtual path. - - This will return two objects. The first will be a callable, - which can be used to generate page output. Any parameters from - the query string or request body will be sent to that callable - as keyword arguments. - - The callable is found by traversing the application's tree, - starting from cherrypy.request.app.root, and matching path - components to successive objects in the tree. For example, the - URL "/path/to/handler" might return root.path.to.handler. - - The second object returned will be a list of names which are - 'virtual path' components: parts of the URL which are dynamic, - and were not used when looking up the handler. - These virtual path components are passed to the handler as - positional arguments. - """ - request = cherrypy.serving.request - app = request.app - root = app.root - dispatch_name = self.dispatch_method_name - - # Get config for the root object/path. - fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] - fullpath_len = len(fullpath) - segleft = fullpath_len - nodeconf = {} - if hasattr(root, '_cp_config'): - nodeconf.update(root._cp_config) - if '/' in app.config: - nodeconf.update(app.config['/']) - object_trail = [['root', root, nodeconf, segleft]] - - node = root - iternames = fullpath[:] - while iternames: - name = iternames[0] - # map to legal Python identifiers (e.g. replace '.' with '_') - objname = name.translate(self.translate) - - nodeconf = {} - subnode = getattr(node, objname, None) - pre_len = len(iternames) - if subnode is None: - dispatch = getattr(node, dispatch_name, None) - if dispatch and hasattr(dispatch, '__call__') and not \ - getattr(dispatch, 'exposed', False) and \ - pre_len > 1: - # Don't expose the hidden 'index' token to _cp_dispatch - # We skip this if pre_len == 1 since it makes no sense - # to call a dispatcher when we have no tokens left. - index_name = iternames.pop() - subnode = dispatch(vpath=iternames) - iternames.append(index_name) - else: - # We didn't find a path, but keep processing in case there - # is a default() handler. - iternames.pop(0) - else: - # We found the path, remove the vpath entry - iternames.pop(0) - segleft = len(iternames) - if segleft > pre_len: - # No path segment was removed. Raise an error. - raise cherrypy.CherryPyException( - 'A vpath segment was added. Custom dispatchers may only ' - 'remove elements. While trying to process ' - '{0} in {1}'.format(name, fullpath) - ) - elif segleft == pre_len: - # Assume that the handler used the current path segment, but - # did not pop it. This allows things like - # return getattr(self, vpath[0], None) - iternames.pop(0) - segleft -= 1 - node = subnode - - if node is not None: - # Get _cp_config attached to this node. - if hasattr(node, '_cp_config'): - nodeconf.update(node._cp_config) - - # Mix in values from app.config for this path. - existing_len = fullpath_len - pre_len - if existing_len != 0: - curpath = '/' + '/'.join(fullpath[0:existing_len]) - else: - curpath = '' - new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] - for seg in new_segs: - curpath += '/' + seg - if curpath in app.config: - nodeconf.update(app.config[curpath]) - - object_trail.append([name, node, nodeconf, segleft]) - - def set_conf(): - """Collapse all object_trail config into cherrypy.request.config. - """ - base = cherrypy.config.copy() - # Note that we merge the config from each node - # even if that node was None. - for name, obj, conf, segleft in object_trail: - base.update(conf) - if 'tools.staticdir.dir' in conf: - base['tools.staticdir.section'] = '/' + \ - '/'.join(fullpath[0:fullpath_len - segleft]) - return base - - # Try successive objects (reverse order) - num_candidates = len(object_trail) - 1 - for i in range(num_candidates, -1, -1): - - name, candidate, nodeconf, segleft = object_trail[i] - if candidate is None: - continue - - # Try a "default" method on the current leaf. - if hasattr(candidate, 'default'): - defhandler = candidate.default - if getattr(defhandler, 'exposed', False): - # Insert any extra _cp_config from the default handler. - conf = getattr(defhandler, '_cp_config', {}) - object_trail.insert( - i + 1, ['default', defhandler, conf, segleft]) - request.config = set_conf() - # See https://github.com/cherrypy/cherrypy/issues/613 - request.is_index = path.endswith('/') - return defhandler, fullpath[fullpath_len - segleft:-1] - - # Uncomment the next line to restrict positional params to - # "default". - # if i < num_candidates - 2: continue - - # Try the current leaf. - if getattr(candidate, 'exposed', False): - request.config = set_conf() - if i == num_candidates: - # We found the extra ".index". Mark request so tools - # can redirect if path_info has no trailing slash. - request.is_index = True - else: - # We're not at an 'index' handler. Mark request so tools - # can redirect if path_info has NO trailing slash. - # Note that this also includes handlers which take - # positional parameters (virtual paths). - request.is_index = False - return candidate, fullpath[fullpath_len - segleft:-1] - - # We didn't find anything - request.config = set_conf() - return None, [] - - -class MethodDispatcher(Dispatcher): - - """Additional dispatch based on cherrypy.request.method.upper(). - - Methods named GET, POST, etc will be called on an exposed class. - The method names must be all caps; the appropriate Allow header - will be output showing all capitalized method names as allowable - HTTP verbs. - - Note that the containing class must be exposed, not the methods. - """ - - def __call__(self, path_info): - """Set handler and config for the current request.""" - request = cherrypy.serving.request - resource, vpath = self.find_handler(path_info) - - if resource: - # Set Allow header - avail = [m for m in dir(resource) if m.isupper()] - if 'GET' in avail and 'HEAD' not in avail: - avail.append('HEAD') - avail.sort() - cherrypy.serving.response.headers['Allow'] = ', '.join(avail) - - # Find the subhandler - meth = request.method.upper() - func = getattr(resource, meth, None) - if func is None and meth == 'HEAD': - func = getattr(resource, 'GET', None) - if func: - # Grab any _cp_config on the subhandler. - if hasattr(func, '_cp_config'): - request.config.update(func._cp_config) - - # Decode any leftover %2F in the virtual_path atoms. - vpath = [x.replace('%2F', '/') for x in vpath] - request.handler = LateParamPageHandler(func, *vpath) - else: - request.handler = cherrypy.HTTPError(405) - else: - request.handler = cherrypy.NotFound() - - -class RoutesDispatcher(object): - - """A Routes based dispatcher for CherryPy.""" - - def __init__(self, full_result=False, **mapper_options): - """ - Routes dispatcher - - Set full_result to True if you wish the controller - and the action to be passed on to the page handler - parameters. By default they won't be. - """ - import routes - self.full_result = full_result - self.controllers = {} - self.mapper = routes.Mapper(**mapper_options) - self.mapper.controller_scan = self.controllers.keys - - def connect(self, name, route, controller, **kwargs): - self.controllers[name] = controller - self.mapper.connect(name, route, controller=name, **kwargs) - - def redirect(self, url): - raise cherrypy.HTTPRedirect(url) - - def __call__(self, path_info): - """Set handler and config for the current request.""" - func = self.find_handler(path_info) - if func: - cherrypy.serving.request.handler = LateParamPageHandler(func) - else: - cherrypy.serving.request.handler = cherrypy.NotFound() - - def find_handler(self, path_info): - """Find the right page handler, and set request.config.""" - import routes - - request = cherrypy.serving.request - - config = routes.request_config() - config.mapper = self.mapper - if hasattr(request, 'wsgi_environ'): - config.environ = request.wsgi_environ - config.host = request.headers.get('Host', None) - config.protocol = request.scheme - config.redirect = self.redirect - - result = self.mapper.match(path_info) - - config.mapper_dict = result - params = {} - if result: - params = result.copy() - if not self.full_result: - params.pop('controller', None) - params.pop('action', None) - request.params.update(params) - - # Get config for the root object/path. - request.config = base = cherrypy.config.copy() - curpath = '' - - def merge(nodeconf): - if 'tools.staticdir.dir' in nodeconf: - nodeconf['tools.staticdir.section'] = curpath or '/' - base.update(nodeconf) - - app = request.app - root = app.root - if hasattr(root, '_cp_config'): - merge(root._cp_config) - if '/' in app.config: - merge(app.config['/']) - - # Mix in values from app.config. - atoms = [x for x in path_info.split('/') if x] - if atoms: - last = atoms.pop() - else: - last = None - for atom in atoms: - curpath = '/'.join((curpath, atom)) - if curpath in app.config: - merge(app.config[curpath]) - - handler = None - if result: - controller = result.get('controller') - controller = self.controllers.get(controller, controller) - if controller: - if isinstance(controller, classtype): - controller = controller() - # Get config from the controller. - if hasattr(controller, '_cp_config'): - merge(controller._cp_config) - - action = result.get('action') - if action is not None: - handler = getattr(controller, action, None) - # Get config from the handler - if hasattr(handler, '_cp_config'): - merge(handler._cp_config) - else: - handler = controller - - # Do the last path atom here so it can - # override the controller's _cp_config. - if last: - curpath = '/'.join((curpath, last)) - if curpath in app.config: - merge(app.config[curpath]) - - return handler - - -def XMLRPCDispatcher(next_dispatcher=Dispatcher()): - from cherrypy.lib import xmlrpcutil - - def xmlrpc_dispatch(path_info): - path_info = xmlrpcutil.patched_path(path_info) - return next_dispatcher(path_info) - return xmlrpc_dispatch - - -def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, - **domains): - """ - Select a different handler based on the Host header. - - This can be useful when running multiple sites within one CP server. - It allows several domains to point to different parts of a single - website structure. For example:: - - http://www.domain.example -> root - http://www.domain2.example -> root/domain2/ - http://www.domain2.example:443 -> root/secure - - can be accomplished via the following config:: - - [/] - request.dispatch = cherrypy.dispatch.VirtualHost( - **{'www.domain2.example': '/domain2', - 'www.domain2.example:443': '/secure', - }) - - next_dispatcher - The next dispatcher object in the dispatch chain. - The VirtualHost dispatcher adds a prefix to the URL and calls - another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). - - use_x_forwarded_host - If True (the default), any "X-Forwarded-Host" - request header will be used instead of the "Host" header. This - is commonly added by HTTP servers (such as Apache) when proxying. - - ``**domains`` - A dict of {host header value: virtual prefix} pairs. - The incoming "Host" request header is looked up in this dict, - and, if a match is found, the corresponding "virtual prefix" - value will be prepended to the URL path before calling the - next dispatcher. Note that you often need separate entries - for "example.com" and "www.example.com". In addition, "Host" - headers may contain the port number. - """ - from cherrypy.lib import httputil - - def vhost_dispatch(path_info): - request = cherrypy.serving.request - header = request.headers.get - - domain = header('Host', '') - if use_x_forwarded_host: - domain = header('X-Forwarded-Host', domain) - - prefix = domains.get(domain, '') - if prefix: - path_info = httputil.urljoin(prefix, path_info) - - result = next_dispatcher(path_info) - - # Touch up staticdir config. See - # https://github.com/cherrypy/cherrypy/issues/614. - section = request.config.get('tools.staticdir.section') - if section: - section = section[len(prefix):] - request.config['tools.staticdir.section'] = section - - return result - return vhost_dispatch diff --git a/libraries/cherrypy/_cperror.py b/libraries/cherrypy/_cperror.py deleted file mode 100644 index e2a8fad8..00000000 --- a/libraries/cherrypy/_cperror.py +++ /dev/null @@ -1,619 +0,0 @@ -"""Exception classes for CherryPy. - -CherryPy provides (and uses) exceptions for declaring that the HTTP response -should be a status other than the default "200 OK". You can ``raise`` them like -normal Python exceptions. You can also call them and they will raise -themselves; this means you can set an -:class:`HTTPError<cherrypy._cperror.HTTPError>` -or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the -:attr:`request.handler<cherrypy._cprequest.Request.handler>`. - -.. _redirectingpost: - -Redirecting POST -================ - -When you GET a resource and are redirected by the server to another Location, -there's generally no problem since GET is both a "safe method" (there should -be no side-effects) and an "idempotent method" (multiple calls are no different -than a single call). - -POST, however, is neither safe nor idempotent--if you -charge a credit card, you don't want to be charged twice by a redirect! - -For this reason, *none* of the 3xx responses permit a user-agent (browser) to -resubmit a POST on redirection without first confirming the action with the -user: - -===== ================================= =========== -300 Multiple Choices Confirm with the user -301 Moved Permanently Confirm with the user -302 Found (Object moved temporarily) Confirm with the user -303 See Other GET the new URI; no confirmation -304 Not modified for conditional GET only; - POST should not raise this error -305 Use Proxy Confirm with the user -307 Temporary Redirect Confirm with the user -===== ================================= =========== - -However, browsers have historically implemented these restrictions poorly; -in particular, many browsers do not force the user to confirm 301, 302 -or 307 when redirecting POST. For this reason, CherryPy defaults to 303, -which most user-agents appear to have implemented correctly. Therefore, if -you raise HTTPRedirect for a POST request, the user-agent will most likely -attempt to GET the new URI (without asking for confirmation from the user). -We realize this is confusing for developers, but it's the safest thing we -could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` -or any other 3xx status if you know what you're doing, but given the -environment, we couldn't let any of those be the default. - -Custom Error Handling -===================== - -.. image:: /refman/cperrors.gif - -Anticipated HTTP responses --------------------------- - -The 'error_page' config namespace can be used to provide custom HTML output for -expected responses (like 404 Not Found). Supply a filename from which the -output will be read. The contents will be interpolated with the values -%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python -`string formatting -<http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_. - -:: - - _cp_config = { - 'error_page.404': os.path.join(localDir, "static/index.html") - } - - -Beginning in version 3.1, you may also provide a function or other callable as -an error_page entry. It will be passed the same status, message, traceback and -version arguments that are interpolated into templates:: - - def error_page_402(status, message, traceback, version): - return "Error %s - Well, I'm very sorry but you haven't paid!" % status - cherrypy.config.update({'error_page.402': error_page_402}) - -Also in 3.1, in addition to the numbered error codes, you may also supply -"error_page.default" to handle all codes which do not have their own error_page -entry. - - - -Unanticipated errors --------------------- - -CherryPy also has a generic error handling mechanism: whenever an unanticipated -error occurs in your code, it will call -:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to -set the response status, headers, and body. By default, this is the same -output as -:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide -some other behavior, you generally replace "request.error_response". - -Here is some sample code that shows how to display a custom error message and -send an e-mail containing the error:: - - from cherrypy import _cperror - - def handle_error(): - cherrypy.response.status = 500 - cherrypy.response.body = [ - "<html><body>Sorry, an error occurred</body></html>" - ] - sendMail('error@domain.com', - 'Error in your web app', - _cperror.format_exc()) - - @cherrypy.config(**{'request.error_response': handle_error}) - class Root: - pass - -Note that you have to explicitly set -:attr:`response.body <cherrypy._cprequest.Response.body>` -and not simply return an error message as a result. -""" - -import io -import contextlib -from sys import exc_info as _exc_info -from traceback import format_exception as _format_exception -from xml.sax import saxutils - -import six -from six.moves import urllib - -from more_itertools import always_iterable - -import cherrypy -from cherrypy._cpcompat import escape_html -from cherrypy._cpcompat import ntob -from cherrypy._cpcompat import tonative -from cherrypy._helper import classproperty -from cherrypy.lib import httputil as _httputil - - -class CherryPyException(Exception): - - """A base class for CherryPy exceptions.""" - pass - - -class InternalRedirect(CherryPyException): - - """Exception raised to switch to the handler for a different URL. - - This exception will redirect processing to another path within the site - (without informing the client). Provide the new path as an argument when - raising the exception. Provide any params in the querystring for the new - URL. - """ - - def __init__(self, path, query_string=''): - self.request = cherrypy.serving.request - - self.query_string = query_string - if '?' in path: - # Separate any params included in the path - path, self.query_string = path.split('?', 1) - - # Note that urljoin will "do the right thing" whether url is: - # 1. a URL relative to root (e.g. "/dummy") - # 2. a URL relative to the current path - # Note that any query string will be discarded. - path = urllib.parse.urljoin(self.request.path_info, path) - - # Set a 'path' member attribute so that code which traps this - # error can have access to it. - self.path = path - - CherryPyException.__init__(self, path, self.query_string) - - -class HTTPRedirect(CherryPyException): - - """Exception raised when the request should be redirected. - - This exception will force a HTTP redirect to the URL or URL's you give it. - The new URL must be passed as the first argument to the Exception, - e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. - If a URL is absolute, it will be used as-is. If it is relative, it is - assumed to be relative to the current cherrypy.request.path_info. - - If one of the provided URL is a unicode object, it will be encoded - using the default encoding or the one passed in parameter. - - There are multiple types of redirect, from which you can select via the - ``status`` argument. If you do not provide a ``status`` arg, it defaults to - 303 (or 302 if responding with HTTP/1.0). - - Examples:: - - raise cherrypy.HTTPRedirect("") - raise cherrypy.HTTPRedirect("/abs/path", 307) - raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) - - See :ref:`redirectingpost` for additional caveats. - """ - - urls = None - """The list of URL's to emit.""" - - encoding = 'utf-8' - """The encoding when passed urls are not native strings""" - - def __init__(self, urls, status=None, encoding=None): - self.urls = abs_urls = [ - # Note that urljoin will "do the right thing" whether url is: - # 1. a complete URL with host (e.g. "http://www.example.com/test") - # 2. a URL relative to root (e.g. "/dummy") - # 3. a URL relative to the current path - # Note that any query string in cherrypy.request is discarded. - urllib.parse.urljoin( - cherrypy.url(), - tonative(url, encoding or self.encoding), - ) - for url in always_iterable(urls) - ] - - status = ( - int(status) - if status is not None - else self.default_status - ) - if not 300 <= status <= 399: - raise ValueError('status must be between 300 and 399.') - - CherryPyException.__init__(self, abs_urls, status) - - @classproperty - def default_status(cls): - """ - The default redirect status for the request. - - RFC 2616 indicates a 301 response code fits our goal; however, - browser support for 301 is quite messy. Use 302/303 instead. See - http://www.alanflavell.org.uk/www/post-redirect.html - """ - return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302 - - @property - def status(self): - """The integer HTTP status code to emit.""" - _, status = self.args[:2] - return status - - def set_response(self): - """Modify cherrypy.response status, headers, and body to represent - self. - - CherryPy uses this internally, but you can also use it to create an - HTTPRedirect object and set its output without *raising* the exception. - """ - response = cherrypy.serving.response - response.status = status = self.status - - if status in (300, 301, 302, 303, 307): - response.headers['Content-Type'] = 'text/html;charset=utf-8' - # "The ... URI SHOULD be given by the Location field - # in the response." - response.headers['Location'] = self.urls[0] - - # "Unless the request method was HEAD, the entity of the response - # SHOULD contain a short hypertext note with a hyperlink to the - # new URI(s)." - msg = { - 300: 'This resource can be found at ', - 301: 'This resource has permanently moved to ', - 302: 'This resource resides temporarily at ', - 303: 'This resource can be found at ', - 307: 'This resource has moved temporarily to ', - }[status] - msg += '<a href=%s>%s</a>.' - msgs = [ - msg % (saxutils.quoteattr(u), escape_html(u)) - for u in self.urls - ] - response.body = ntob('<br />\n'.join(msgs), 'utf-8') - # Previous code may have set C-L, so we have to reset it - # (allow finalize to set it). - response.headers.pop('Content-Length', None) - elif status == 304: - # Not Modified. - # "The response MUST include the following header fields: - # Date, unless its omission is required by section 14.18.1" - # The "Date" header should have been set in Response.__init__ - - # "...the response SHOULD NOT include other entity-headers." - for key in ('Allow', 'Content-Encoding', 'Content-Language', - 'Content-Length', 'Content-Location', 'Content-MD5', - 'Content-Range', 'Content-Type', 'Expires', - 'Last-Modified'): - if key in response.headers: - del response.headers[key] - - # "The 304 response MUST NOT contain a message-body." - response.body = None - # Previous code may have set C-L, so we have to reset it. - response.headers.pop('Content-Length', None) - elif status == 305: - # Use Proxy. - # self.urls[0] should be the URI of the proxy. - response.headers['Location'] = ntob(self.urls[0], 'utf-8') - response.body = None - # Previous code may have set C-L, so we have to reset it. - response.headers.pop('Content-Length', None) - else: - raise ValueError('The %s status code is unknown.' % status) - - def __call__(self): - """Use this exception as a request.handler (raise self).""" - raise self - - -def clean_headers(status): - """Remove any headers which should not apply to an error response.""" - response = cherrypy.serving.response - - # Remove headers which applied to the original content, - # but do not apply to the error page. - respheaders = response.headers - for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After', - 'Vary', 'Content-Encoding', 'Content-Length', 'Expires', - 'Content-Location', 'Content-MD5', 'Last-Modified']: - if key in respheaders: - del respheaders[key] - - if status != 416: - # A server sending a response with status code 416 (Requested - # range not satisfiable) SHOULD include a Content-Range field - # with a byte-range-resp-spec of "*". The instance-length - # specifies the current length of the selected resource. - # A response with status code 206 (Partial Content) MUST NOT - # include a Content-Range field with a byte-range- resp-spec of "*". - if 'Content-Range' in respheaders: - del respheaders['Content-Range'] - - -class HTTPError(CherryPyException): - - """Exception used to return an HTTP error code (4xx-5xx) to the client. - - This exception can be used to automatically send a response using a - http status code, with an appropriate error page. It takes an optional - ``status`` argument (which must be between 400 and 599); it defaults to 500 - ("Internal Server Error"). It also takes an optional ``message`` argument, - which will be returned in the response body. See - `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_ - for a complete list of available error codes and when to use them. - - Examples:: - - raise cherrypy.HTTPError(403) - raise cherrypy.HTTPError( - "403 Forbidden", "You are not allowed to access this resource.") - """ - - status = None - """The HTTP status code. May be of type int or str (with a Reason-Phrase). - """ - - code = None - """The integer HTTP status code.""" - - reason = None - """The HTTP Reason-Phrase string.""" - - def __init__(self, status=500, message=None): - self.status = status - try: - self.code, self.reason, defaultmsg = _httputil.valid_status(status) - except ValueError: - raise self.__class__(500, _exc_info()[1].args[0]) - - if self.code < 400 or self.code > 599: - raise ValueError('status must be between 400 and 599.') - - # See http://www.python.org/dev/peps/pep-0352/ - # self.message = message - self._message = message or defaultmsg - CherryPyException.__init__(self, status, message) - - def set_response(self): - """Modify cherrypy.response status, headers, and body to represent - self. - - CherryPy uses this internally, but you can also use it to create an - HTTPError object and set its output without *raising* the exception. - """ - response = cherrypy.serving.response - - clean_headers(self.code) - - # In all cases, finalize will be called after this method, - # so don't bother cleaning up response values here. - response.status = self.status - tb = None - if cherrypy.serving.request.show_tracebacks: - tb = format_exc() - - response.headers.pop('Content-Length', None) - - content = self.get_error_page(self.status, traceback=tb, - message=self._message) - response.body = content - - _be_ie_unfriendly(self.code) - - def get_error_page(self, *args, **kwargs): - return get_error_page(*args, **kwargs) - - def __call__(self): - """Use this exception as a request.handler (raise self).""" - raise self - - @classmethod - @contextlib.contextmanager - def handle(cls, exception, status=500, message=''): - """Translate exception into an HTTPError.""" - try: - yield - except exception as exc: - raise cls(status, message or str(exc)) - - -class NotFound(HTTPError): - - """Exception raised when a URL could not be mapped to any handler (404). - - This is equivalent to raising - :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`. - """ - - def __init__(self, path=None): - if path is None: - request = cherrypy.serving.request - path = request.script_name + request.path_info - self.args = (path,) - HTTPError.__init__(self, 404, "The path '%s' was not found." % path) - - -_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC -"-//W3C//DTD XHTML 1.0 Transitional//EN" -"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html> -<head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta> - <title>%(status)s</title> - <style type="text/css"> - #powered_by { - margin-top: 20px; - border-top: 2px solid black; - font-style: italic; - } - - #traceback { - color: red; - } - </style> -</head> - <body> - <h2>%(status)s</h2> - <p>%(message)s</p> - <pre id="traceback">%(traceback)s</pre> - <div id="powered_by"> - <span> - Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a> - </span> - </div> - </body> -</html> -''' - - -def get_error_page(status, **kwargs): - """Return an HTML page, containing a pretty error response. - - status should be an int or a str. - kwargs will be interpolated into the page template. - """ - try: - code, reason, message = _httputil.valid_status(status) - except ValueError: - raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) - - # We can't use setdefault here, because some - # callers send None for kwarg values. - if kwargs.get('status') is None: - kwargs['status'] = '%s %s' % (code, reason) - if kwargs.get('message') is None: - kwargs['message'] = message - if kwargs.get('traceback') is None: - kwargs['traceback'] = '' - if kwargs.get('version') is None: - kwargs['version'] = cherrypy.__version__ - - for k, v in six.iteritems(kwargs): - if v is None: - kwargs[k] = '' - else: - kwargs[k] = escape_html(kwargs[k]) - - # Use a custom template or callable for the error page? - pages = cherrypy.serving.request.error_page - error_page = pages.get(code) or pages.get('default') - - # Default template, can be overridden below. - template = _HTTPErrorTemplate - if error_page: - try: - if hasattr(error_page, '__call__'): - # The caller function may be setting headers manually, - # so we delegate to it completely. We may be returning - # an iterator as well as a string here. - # - # We *must* make sure any content is not unicode. - result = error_page(**kwargs) - if cherrypy.lib.is_iterator(result): - from cherrypy.lib.encoding import UTF8StreamEncoder - return UTF8StreamEncoder(result) - elif isinstance(result, six.text_type): - return result.encode('utf-8') - else: - if not isinstance(result, bytes): - raise ValueError( - 'error page function did not ' - 'return a bytestring, six.text_type or an ' - 'iterator - returned object of type %s.' - % (type(result).__name__)) - return result - else: - # Load the template from this path. - template = io.open(error_page, newline='').read() - except Exception: - e = _format_exception(*_exc_info())[-1] - m = kwargs['message'] - if m: - m += '<br />' - m += 'In addition, the custom error page failed:\n<br />%s' % e - kwargs['message'] = m - - response = cherrypy.serving.response - response.headers['Content-Type'] = 'text/html;charset=utf-8' - result = template % kwargs - return result.encode('utf-8') - - -_ie_friendly_error_sizes = { - 400: 512, 403: 256, 404: 512, 405: 256, - 406: 512, 408: 512, 409: 512, 410: 256, - 500: 512, 501: 512, 505: 512, -} - - -def _be_ie_unfriendly(status): - response = cherrypy.serving.response - - # For some statuses, Internet Explorer 5+ shows "friendly error - # messages" instead of our response.body if the body is smaller - # than a given size. Fix this by returning a body over that size - # (by adding whitespace). - # See http://support.microsoft.com/kb/q218155/ - s = _ie_friendly_error_sizes.get(status, 0) - if s: - s += 1 - # Since we are issuing an HTTP error status, we assume that - # the entity is short, and we should just collapse it. - content = response.collapse_body() - content_length = len(content) - if content_length and content_length < s: - # IN ADDITION: the response must be written to IE - # in one chunk or it will still get replaced! Bah. - content = content + (b' ' * (s - content_length)) - response.body = content - response.headers['Content-Length'] = str(len(content)) - - -def format_exc(exc=None): - """Return exc (or sys.exc_info if None), formatted.""" - try: - if exc is None: - exc = _exc_info() - if exc == (None, None, None): - return '' - import traceback - return ''.join(traceback.format_exception(*exc)) - finally: - del exc - - -def bare_error(extrabody=None): - """Produce status, headers, body for a critical error. - - Returns a triple without calling any other questionable functions, - so it should be as error-free as possible. Call it from an HTTP server - if you get errors outside of the request. - - If extrabody is None, a friendly but rather unhelpful error message - is set in the body. If extrabody is a string, it will be appended - as-is to the body. - """ - - # The whole point of this function is to be a last line-of-defense - # in handling errors. That is, it must not raise any errors itself; - # it cannot be allowed to fail. Therefore, don't add to it! - # In particular, don't call any other CP functions. - - body = b'Unrecoverable error in the server.' - if extrabody is not None: - if not isinstance(extrabody, bytes): - extrabody = extrabody.encode('utf-8') - body += b'\n' + extrabody - - return (b'500 Internal Server Error', - [(b'Content-Type', b'text/plain'), - (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))], - [body]) diff --git a/libraries/cherrypy/_cplogging.py b/libraries/cherrypy/_cplogging.py deleted file mode 100644 index 53b9addb..00000000 --- a/libraries/cherrypy/_cplogging.py +++ /dev/null @@ -1,482 +0,0 @@ -""" -Simple config -============= - -Although CherryPy uses the :mod:`Python logging module <logging>`, it does so -behind the scenes so that simple logging is simple, but complicated logging -is still possible. "Simple" logging means that you can log to the screen -(i.e. console/stdout) or to a file, and that you can easily have separate -error and access log files. - -Here are the simplified logging settings. You use these by adding lines to -your config file or dict. You should set these at either the global level or -per application (see next), but generally not both. - - * ``log.screen``: Set this to True to have both "error" and "access" messages - printed to stdout. - * ``log.access_file``: Set this to an absolute filename where you want - "access" messages written. - * ``log.error_file``: Set this to an absolute filename where you want "error" - messages written. - -Many events are automatically logged; to log your own application events, call -:func:`cherrypy.log`. - -Architecture -============ - -Separate scopes ---------------- - -CherryPy provides log managers at both the global and application layers. -This means you can have one set of logging rules for your entire site, -and another set of rules specific to each application. The global log -manager is found at :func:`cherrypy.log`, and the log manager for each -application is found at :attr:`app.log<cherrypy._cptree.Application.log>`. -If you're inside a request, the latter is reachable from -``cherrypy.request.app.log``; if you're outside a request, you'll have to -obtain a reference to the ``app``: either the return value of -:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used -:func:`quickstart()<cherrypy.quickstart>` instead, via -``cherrypy.tree.apps['/']``. - -By default, the global logs are named "cherrypy.error" and "cherrypy.access", -and the application logs are named "cherrypy.error.2378745" and -"cherrypy.access.2378745" (the number is the id of the Application object). -This means that the application logs "bubble up" to the site logs, so if your -application has no log handlers, the site-level handlers will still log the -messages. - -Errors vs. Access ------------------ - -Each log manager handles both "access" messages (one per HTTP request) and -"error" messages (everything else). Note that the "error" log is not just for -errors! The format of access messages is highly formalized, but the error log -isn't--it receives messages from a variety of sources (including full error -tracebacks, if enabled). - -If you are logging the access log and error log to the same source, then there -is a possibility that a specially crafted error message may replicate an access -log message as described in CWE-117. In this case it is the application -developer's responsibility to manually escape data before -using CherryPy's log() -functionality, or they may create an application that is vulnerable to CWE-117. -This would be achieved by using a custom handler escape any special characters, -and attached as described below. - -Custom Handlers -=============== - -The simple settings above work by manipulating Python's standard :mod:`logging` -module. So when you need something more complex, the full power of the standard -module is yours to exploit. You can borrow or create custom handlers, formats, -filters, and much more. Here's an example that skips the standard FileHandler -and uses a RotatingFileHandler instead: - -:: - - #python - log = app.log - - # Remove the default FileHandlers if present. - log.error_file = "" - log.access_file = "" - - maxBytes = getattr(log, "rot_maxBytes", 10000000) - backupCount = getattr(log, "rot_backupCount", 1000) - - # Make a new RotatingFileHandler for the error log. - fname = getattr(log, "rot_error_file", "error.log") - h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) - h.setLevel(DEBUG) - h.setFormatter(_cplogging.logfmt) - log.error_log.addHandler(h) - - # Make a new RotatingFileHandler for the access log. - fname = getattr(log, "rot_access_file", "access.log") - h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) - h.setLevel(DEBUG) - h.setFormatter(_cplogging.logfmt) - log.access_log.addHandler(h) - - -The ``rot_*`` attributes are pulled straight from the application log object. -Since "log.*" config entries simply set attributes on the log object, you can -add custom attributes to your heart's content. Note that these handlers are -used ''instead'' of the default, simple handlers outlined above (so don't set -the "log.error_file" config entry, for example). -""" - -import datetime -import logging -import os -import sys - -import six - -import cherrypy -from cherrypy import _cperror - - -# Silence the no-handlers "warning" (stderr write!) in stdlib logging -logging.Logger.manager.emittedNoHandlerWarning = 1 -logfmt = logging.Formatter('%(message)s') - - -class NullHandler(logging.Handler): - - """A no-op logging handler to silence the logging.lastResort handler.""" - - def handle(self, record): - pass - - def emit(self, record): - pass - - def createLock(self): - self.lock = None - - -class LogManager(object): - - """An object to assist both simple and advanced logging. - - ``cherrypy.log`` is an instance of this class. - """ - - appid = None - """The id() of the Application object which owns this log manager. If this - is a global log manager, appid is None.""" - - error_log = None - """The actual :class:`logging.Logger` instance for error messages.""" - - access_log = None - """The actual :class:`logging.Logger` instance for access messages.""" - - access_log_format = ( - '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' - if six.PY3 else - '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' - ) - - logger_root = None - """The "top-level" logger name. - - This string will be used as the first segment in the Logger names. - The default is "cherrypy", for example, in which case the Logger names - will be of the form:: - - cherrypy.error.<appid> - cherrypy.access.<appid> - """ - - def __init__(self, appid=None, logger_root='cherrypy'): - self.logger_root = logger_root - self.appid = appid - if appid is None: - self.error_log = logging.getLogger('%s.error' % logger_root) - self.access_log = logging.getLogger('%s.access' % logger_root) - else: - self.error_log = logging.getLogger( - '%s.error.%s' % (logger_root, appid)) - self.access_log = logging.getLogger( - '%s.access.%s' % (logger_root, appid)) - self.error_log.setLevel(logging.INFO) - self.access_log.setLevel(logging.INFO) - - # Silence the no-handlers "warning" (stderr write!) in stdlib logging - self.error_log.addHandler(NullHandler()) - self.access_log.addHandler(NullHandler()) - - cherrypy.engine.subscribe('graceful', self.reopen_files) - - def reopen_files(self): - """Close and reopen all file handlers.""" - for log in (self.error_log, self.access_log): - for h in log.handlers: - if isinstance(h, logging.FileHandler): - h.acquire() - h.stream.close() - h.stream = open(h.baseFilename, h.mode) - h.release() - - def error(self, msg='', context='', severity=logging.INFO, - traceback=False): - """Write the given ``msg`` to the error log. - - This is not just for errors! Applications may call this at any time - to log application-specific information. - - If ``traceback`` is True, the traceback of the current exception - (if any) will be appended to ``msg``. - """ - exc_info = None - if traceback: - exc_info = _cperror._exc_info() - - self.error_log.log( - severity, - ' '.join((self.time(), context, msg)), - exc_info=exc_info, - ) - - def __call__(self, *args, **kwargs): - """An alias for ``error``.""" - return self.error(*args, **kwargs) - - def access(self): - """Write to the access log (in Apache/NCSA Combined Log format). - - See the - `apache documentation - <http://httpd.apache.org/docs/current/logs.html#combined>`_ - for format details. - - CherryPy calls this automatically for you. Note there are no arguments; - it collects the data itself from - :class:`cherrypy.request<cherrypy._cprequest.Request>`. - - Like Apache started doing in 2.0.46, non-printable and other special - characters in %r (and we expand that to all parts) are escaped using - \\xhh sequences, where hh stands for the hexadecimal representation - of the raw byte. Exceptions from this rule are " and \\, which are - escaped by prepending a backslash, and all whitespace characters, - which are written in their C-style notation (\\n, \\t, etc). - """ - request = cherrypy.serving.request - remote = request.remote - response = cherrypy.serving.response - outheaders = response.headers - inheaders = request.headers - if response.output_status is None: - status = '-' - else: - status = response.output_status.split(b' ', 1)[0] - if six.PY3: - status = status.decode('ISO-8859-1') - - atoms = {'h': remote.name or remote.ip, - 'l': '-', - 'u': getattr(request, 'login', None) or '-', - 't': self.time(), - 'r': request.request_line, - 's': status, - 'b': dict.get(outheaders, 'Content-Length', '') or '-', - 'f': dict.get(inheaders, 'Referer', ''), - 'a': dict.get(inheaders, 'User-Agent', ''), - 'o': dict.get(inheaders, 'Host', '-'), - 'i': request.unique_id, - 'z': LazyRfc3339UtcTime(), - } - if six.PY3: - for k, v in atoms.items(): - if not isinstance(v, str): - v = str(v) - v = v.replace('"', '\\"').encode('utf8') - # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc - # and backslash for us. All we have to do is strip the quotes. - v = repr(v)[2:-1] - - # in python 3.0 the repr of bytes (as returned by encode) - # uses double \'s. But then the logger escapes them yet, again - # resulting in quadruple slashes. Remove the extra one here. - v = v.replace('\\\\', '\\') - - # Escape double-quote. - atoms[k] = v - - try: - self.access_log.log( - logging.INFO, self.access_log_format.format(**atoms)) - except Exception: - self(traceback=True) - else: - for k, v in atoms.items(): - if isinstance(v, six.text_type): - v = v.encode('utf8') - elif not isinstance(v, str): - v = str(v) - # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc - # and backslash for us. All we have to do is strip the quotes. - v = repr(v)[1:-1] - # Escape double-quote. - atoms[k] = v.replace('"', '\\"') - - try: - self.access_log.log( - logging.INFO, self.access_log_format % atoms) - except Exception: - self(traceback=True) - - def time(self): - """Return now() in Apache Common Log Format (no timezone).""" - now = datetime.datetime.now() - monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', - 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] - month = monthnames[now.month - 1].capitalize() - return ('[%02d/%s/%04d:%02d:%02d:%02d]' % - (now.day, month, now.year, now.hour, now.minute, now.second)) - - def _get_builtin_handler(self, log, key): - for h in log.handlers: - if getattr(h, '_cpbuiltin', None) == key: - return h - - # ------------------------- Screen handlers ------------------------- # - def _set_screen_handler(self, log, enable, stream=None): - h = self._get_builtin_handler(log, 'screen') - if enable: - if not h: - if stream is None: - stream = sys.stderr - h = logging.StreamHandler(stream) - h.setFormatter(logfmt) - h._cpbuiltin = 'screen' - log.addHandler(h) - elif h: - log.handlers.remove(h) - - @property - def screen(self): - """Turn stderr/stdout logging on or off. - - If you set this to True, it'll add the appropriate StreamHandler for - you. If you set it to False, it will remove the handler. - """ - h = self._get_builtin_handler - has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') - return bool(has_h) - - @screen.setter - def screen(self, newvalue): - self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) - self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) - - # -------------------------- File handlers -------------------------- # - - def _add_builtin_file_handler(self, log, fname): - h = logging.FileHandler(fname) - h.setFormatter(logfmt) - h._cpbuiltin = 'file' - log.addHandler(h) - - def _set_file_handler(self, log, filename): - h = self._get_builtin_handler(log, 'file') - if filename: - if h: - if h.baseFilename != os.path.abspath(filename): - h.close() - log.handlers.remove(h) - self._add_builtin_file_handler(log, filename) - else: - self._add_builtin_file_handler(log, filename) - else: - if h: - h.close() - log.handlers.remove(h) - - @property - def error_file(self): - """The filename for self.error_log. - - If you set this to a string, it'll add the appropriate FileHandler for - you. If you set it to ``None`` or ``''``, it will remove the handler. - """ - h = self._get_builtin_handler(self.error_log, 'file') - if h: - return h.baseFilename - return '' - - @error_file.setter - def error_file(self, newvalue): - self._set_file_handler(self.error_log, newvalue) - - @property - def access_file(self): - """The filename for self.access_log. - - If you set this to a string, it'll add the appropriate FileHandler for - you. If you set it to ``None`` or ``''``, it will remove the handler. - """ - h = self._get_builtin_handler(self.access_log, 'file') - if h: - return h.baseFilename - return '' - - @access_file.setter - def access_file(self, newvalue): - self._set_file_handler(self.access_log, newvalue) - - # ------------------------- WSGI handlers ------------------------- # - - def _set_wsgi_handler(self, log, enable): - h = self._get_builtin_handler(log, 'wsgi') - if enable: - if not h: - h = WSGIErrorHandler() - h.setFormatter(logfmt) - h._cpbuiltin = 'wsgi' - log.addHandler(h) - elif h: - log.handlers.remove(h) - - @property - def wsgi(self): - """Write errors to wsgi.errors. - - If you set this to True, it'll add the appropriate - :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you - (which writes errors to ``wsgi.errors``). - If you set it to False, it will remove the handler. - """ - return bool(self._get_builtin_handler(self.error_log, 'wsgi')) - - @wsgi.setter - def wsgi(self, newvalue): - self._set_wsgi_handler(self.error_log, newvalue) - - -class WSGIErrorHandler(logging.Handler): - - "A handler class which writes logging records to environ['wsgi.errors']." - - def flush(self): - """Flushes the stream.""" - try: - stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') - except (AttributeError, KeyError): - pass - else: - stream.flush() - - def emit(self, record): - """Emit a record.""" - try: - stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') - except (AttributeError, KeyError): - pass - else: - try: - msg = self.format(record) - fs = '%s\n' - import types - # if no unicode support... - if not hasattr(types, 'UnicodeType'): - stream.write(fs % msg) - else: - try: - stream.write(fs % msg) - except UnicodeError: - stream.write(fs % msg.encode('UTF-8')) - self.flush() - except Exception: - self.handleError(record) - - -class LazyRfc3339UtcTime(object): - def __str__(self): - """Return now() in RFC3339 UTC Format.""" - now = datetime.datetime.now() - return now.isoformat('T') + 'Z' diff --git a/libraries/cherrypy/_cpmodpy.py b/libraries/cherrypy/_cpmodpy.py deleted file mode 100644 index ac91e625..00000000 --- a/libraries/cherrypy/_cpmodpy.py +++ /dev/null @@ -1,356 +0,0 @@ -"""Native adapter for serving CherryPy via mod_python - -Basic usage: - -########################################## -# Application in a module called myapp.py -########################################## - -import cherrypy - -class Root: - @cherrypy.expose - def index(self): - return 'Hi there, Ho there, Hey there' - - -# We will use this method from the mod_python configuration -# as the entry point to our application -def setup_server(): - cherrypy.tree.mount(Root()) - cherrypy.config.update({'environment': 'production', - 'log.screen': False, - 'show_tracebacks': False}) - -########################################## -# mod_python settings for apache2 -# This should reside in your httpd.conf -# or a file that will be loaded at -# apache startup -########################################## - -# Start -DocumentRoot "/" -Listen 8080 -LoadModule python_module /usr/lib/apache2/modules/mod_python.so - -<Location "/"> - PythonPath "sys.path+['/path/to/my/application']" - SetHandler python-program - PythonHandler cherrypy._cpmodpy::handler - PythonOption cherrypy.setup myapp::setup_server - PythonDebug On -</Location> -# End - -The actual path to your mod_python.so is dependent on your -environment. In this case we suppose a global mod_python -installation on a Linux distribution such as Ubuntu. - -We do set the PythonPath configuration setting so that -your application can be found by from the user running -the apache2 instance. Of course if your application -resides in the global site-package this won't be needed. - -Then restart apache2 and access http://127.0.0.1:8080 -""" - -import io -import logging -import os -import re -import sys - -import six - -from more_itertools import always_iterable - -import cherrypy -from cherrypy._cperror import format_exc, bare_error -from cherrypy.lib import httputil - - -# ------------------------------ Request-handling - - -def setup(req): - from mod_python import apache - - # Run any setup functions defined by a "PythonOption cherrypy.setup" - # directive. - options = req.get_options() - if 'cherrypy.setup' in options: - for function in options['cherrypy.setup'].split(): - atoms = function.split('::', 1) - if len(atoms) == 1: - mod = __import__(atoms[0], globals(), locals()) - else: - modname, fname = atoms - mod = __import__(modname, globals(), locals(), [fname]) - func = getattr(mod, fname) - func() - - cherrypy.config.update({'log.screen': False, - 'tools.ignore_headers.on': True, - 'tools.ignore_headers.headers': ['Range'], - }) - - engine = cherrypy.engine - if hasattr(engine, 'signal_handler'): - engine.signal_handler.unsubscribe() - if hasattr(engine, 'console_control_handler'): - engine.console_control_handler.unsubscribe() - engine.autoreload.unsubscribe() - cherrypy.server.unsubscribe() - - @engine.subscribe('log') - def _log(msg, level): - newlevel = apache.APLOG_ERR - if logging.DEBUG >= level: - newlevel = apache.APLOG_DEBUG - elif logging.INFO >= level: - newlevel = apache.APLOG_INFO - elif logging.WARNING >= level: - newlevel = apache.APLOG_WARNING - # On Windows, req.server is required or the msg will vanish. See - # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html - # Also, "When server is not specified...LogLevel does not apply..." - apache.log_error(msg, newlevel, req.server) - - engine.start() - - def cherrypy_cleanup(data): - engine.exit() - try: - # apache.register_cleanup wasn't available until 3.1.4. - apache.register_cleanup(cherrypy_cleanup) - except AttributeError: - req.server.register_cleanup(req, cherrypy_cleanup) - - -class _ReadOnlyRequest: - expose = ('read', 'readline', 'readlines') - - def __init__(self, req): - for method in self.expose: - self.__dict__[method] = getattr(req, method) - - -recursive = False - -_isSetUp = False - - -def handler(req): - from mod_python import apache - try: - global _isSetUp - if not _isSetUp: - setup(req) - _isSetUp = True - - # Obtain a Request object from CherryPy - local = req.connection.local_addr - local = httputil.Host( - local[0], local[1], req.connection.local_host or '') - remote = req.connection.remote_addr - remote = httputil.Host( - remote[0], remote[1], req.connection.remote_host or '') - - scheme = req.parsed_uri[0] or 'http' - req.get_basic_auth_pw() - - try: - # apache.mpm_query only became available in mod_python 3.1 - q = apache.mpm_query - threaded = q(apache.AP_MPMQ_IS_THREADED) - forked = q(apache.AP_MPMQ_IS_FORKED) - except AttributeError: - bad_value = ("You must provide a PythonOption '%s', " - "either 'on' or 'off', when running a version " - 'of mod_python < 3.1') - - options = req.get_options() - - threaded = options.get('multithread', '').lower() - if threaded == 'on': - threaded = True - elif threaded == 'off': - threaded = False - else: - raise ValueError(bad_value % 'multithread') - - forked = options.get('multiprocess', '').lower() - if forked == 'on': - forked = True - elif forked == 'off': - forked = False - else: - raise ValueError(bad_value % 'multiprocess') - - sn = cherrypy.tree.script_name(req.uri or '/') - if sn is None: - send_response(req, '404 Not Found', [], '') - else: - app = cherrypy.tree.apps[sn] - method = req.method - path = req.uri - qs = req.args or '' - reqproto = req.protocol - headers = list(six.iteritems(req.headers_in)) - rfile = _ReadOnlyRequest(req) - prev = None - - try: - redirections = [] - while True: - request, response = app.get_serving(local, remote, scheme, - 'HTTP/1.1') - request.login = req.user - request.multithread = bool(threaded) - request.multiprocess = bool(forked) - request.app = app - request.prev = prev - - # Run the CherryPy Request object and obtain the response - try: - request.run(method, path, qs, reqproto, headers, rfile) - break - except cherrypy.InternalRedirect: - ir = sys.exc_info()[1] - app.release_serving() - prev = request - - if not recursive: - if ir.path in redirections: - raise RuntimeError( - 'InternalRedirector visited the same URL ' - 'twice: %r' % ir.path) - else: - # Add the *previous* path_info + qs to - # redirections. - if qs: - qs = '?' + qs - redirections.append(sn + path + qs) - - # Munge environment and try again. - method = 'GET' - path = ir.path - qs = ir.query_string - rfile = io.BytesIO() - - send_response( - req, response.output_status, response.header_list, - response.body, response.stream) - finally: - app.release_serving() - except Exception: - tb = format_exc() - cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) - s, h, b = bare_error() - send_response(req, s, h, b) - return apache.OK - - -def send_response(req, status, headers, body, stream=False): - # Set response status - req.status = int(status[:3]) - - # Set response headers - req.content_type = 'text/plain' - for header, value in headers: - if header.lower() == 'content-type': - req.content_type = value - continue - req.headers_out.add(header, value) - - if stream: - # Flush now so the status and headers are sent immediately. - req.flush() - - # Set response body - for seg in always_iterable(body): - req.write(seg) - - -# --------------- Startup tools for CherryPy + mod_python --------------- # -try: - import subprocess - - def popen(fullcmd): - p = subprocess.Popen(fullcmd, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - close_fds=True) - return p.stdout -except ImportError: - def popen(fullcmd): - pipein, pipeout = os.popen4(fullcmd) - return pipeout - - -def read_process(cmd, args=''): - fullcmd = '%s %s' % (cmd, args) - pipeout = popen(fullcmd) - try: - firstline = pipeout.readline() - cmd_not_found = re.search( - b'(not recognized|No such file|not found)', - firstline, - re.IGNORECASE - ) - if cmd_not_found: - raise IOError('%s must be on your system path.' % cmd) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -class ModPythonServer(object): - - template = """ -# Apache2 server configuration file for running CherryPy with mod_python. - -DocumentRoot "/" -Listen %(port)s -LoadModule python_module modules/mod_python.so - -<Location %(loc)s> - SetHandler python-program - PythonHandler %(handler)s - PythonDebug On -%(opts)s -</Location> -""" - - def __init__(self, loc='/', port=80, opts=None, apache_path='apache', - handler='cherrypy._cpmodpy::handler'): - self.loc = loc - self.port = port - self.opts = opts - self.apache_path = apache_path - self.handler = handler - - def start(self): - opts = ''.join([' PythonOption %s %s\n' % (k, v) - for k, v in self.opts]) - conf_data = self.template % {'port': self.port, - 'loc': self.loc, - 'opts': opts, - 'handler': self.handler, - } - - mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') - f = open(mpconf, 'wb') - try: - f.write(conf_data) - finally: - f.close() - - response = read_process(self.apache_path, '-k start -f %s' % mpconf) - self.ready = True - return response - - def stop(self): - os.popen('apache -k stop') - self.ready = False diff --git a/libraries/cherrypy/_cpnative_server.py b/libraries/cherrypy/_cpnative_server.py deleted file mode 100644 index 55653c35..00000000 --- a/libraries/cherrypy/_cpnative_server.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Native adapter for serving CherryPy via its builtin server.""" - -import logging -import sys -import io - -import cheroot.server - -import cherrypy -from cherrypy._cperror import format_exc, bare_error -from cherrypy.lib import httputil - - -class NativeGateway(cheroot.server.Gateway): - """Native gateway implementation allowing to bypass WSGI.""" - - recursive = False - - def respond(self): - """Obtain response from CherryPy machinery and then send it.""" - req = self.req - try: - # Obtain a Request object from CherryPy - local = req.server.bind_addr - local = httputil.Host(local[0], local[1], '') - remote = req.conn.remote_addr, req.conn.remote_port - remote = httputil.Host(remote[0], remote[1], '') - - scheme = req.scheme - sn = cherrypy.tree.script_name(req.uri or '/') - if sn is None: - self.send_response('404 Not Found', [], ['']) - else: - app = cherrypy.tree.apps[sn] - method = req.method - path = req.path - qs = req.qs or '' - headers = req.inheaders.items() - rfile = req.rfile - prev = None - - try: - redirections = [] - while True: - request, response = app.get_serving( - local, remote, scheme, 'HTTP/1.1') - request.multithread = True - request.multiprocess = False - request.app = app - request.prev = prev - - # Run the CherryPy Request object and obtain the - # response - try: - request.run(method, path, qs, - req.request_protocol, headers, rfile) - break - except cherrypy.InternalRedirect: - ir = sys.exc_info()[1] - app.release_serving() - prev = request - - if not self.recursive: - if ir.path in redirections: - raise RuntimeError( - 'InternalRedirector visited the same ' - 'URL twice: %r' % ir.path) - else: - # Add the *previous* path_info + qs to - # redirections. - if qs: - qs = '?' + qs - redirections.append(sn + path + qs) - - # Munge environment and try again. - method = 'GET' - path = ir.path - qs = ir.query_string - rfile = io.BytesIO() - - self.send_response( - response.output_status, response.header_list, - response.body) - finally: - app.release_serving() - except Exception: - tb = format_exc() - # print tb - cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) - s, h, b = bare_error() - self.send_response(s, h, b) - - def send_response(self, status, headers, body): - """Send response to HTTP request.""" - req = self.req - - # Set response status - req.status = status or b'500 Server Error' - - # Set response headers - for header, value in headers: - req.outheaders.append((header, value)) - if (req.ready and not req.sent_headers): - req.sent_headers = True - req.send_headers() - - # Set response body - for seg in body: - req.write(seg) - - -class CPHTTPServer(cheroot.server.HTTPServer): - """Wrapper for cheroot.server.HTTPServer. - - cheroot has been designed to not reference CherryPy in any way, - so that it can be used in other frameworks and applications. - Therefore, we wrap it here, so we can apply some attributes - from config -> cherrypy.server -> HTTPServer. - """ - - def __init__(self, server_adapter=cherrypy.server): - """Initialize CPHTTPServer.""" - self.server_adapter = server_adapter - - server_name = (self.server_adapter.socket_host or - self.server_adapter.socket_file or - None) - - cheroot.server.HTTPServer.__init__( - self, server_adapter.bind_addr, NativeGateway, - minthreads=server_adapter.thread_pool, - maxthreads=server_adapter.thread_pool_max, - server_name=server_name) - - self.max_request_header_size = ( - self.server_adapter.max_request_header_size or 0) - self.max_request_body_size = ( - self.server_adapter.max_request_body_size or 0) - self.request_queue_size = self.server_adapter.socket_queue_size - self.timeout = self.server_adapter.socket_timeout - self.shutdown_timeout = self.server_adapter.shutdown_timeout - self.protocol = self.server_adapter.protocol_version - self.nodelay = self.server_adapter.nodelay - - ssl_module = self.server_adapter.ssl_module or 'pyopenssl' - if self.server_adapter.ssl_context: - adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) - self.ssl_adapter = adapter_class( - self.server_adapter.ssl_certificate, - self.server_adapter.ssl_private_key, - self.server_adapter.ssl_certificate_chain, - self.server_adapter.ssl_ciphers) - self.ssl_adapter.context = self.server_adapter.ssl_context - elif self.server_adapter.ssl_certificate: - adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) - self.ssl_adapter = adapter_class( - self.server_adapter.ssl_certificate, - self.server_adapter.ssl_private_key, - self.server_adapter.ssl_certificate_chain, - self.server_adapter.ssl_ciphers) diff --git a/libraries/cherrypy/_cpreqbody.py b/libraries/cherrypy/_cpreqbody.py deleted file mode 100644 index 893fe5f5..00000000 --- a/libraries/cherrypy/_cpreqbody.py +++ /dev/null @@ -1,1000 +0,0 @@ -"""Request body processing for CherryPy. - -.. versionadded:: 3.2 - -Application authors have complete control over the parsing of HTTP request -entities. In short, -:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>` -is now always set to an instance of -:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`, -and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`. - -When an HTTP request includes an entity body, it is often desirable to -provide that information to applications in a form other than the raw bytes. -Different content types demand different approaches. Examples: - - * For a GIF file, we want the raw bytes in a stream. - * An HTML form is better parsed into its component fields, and each text field - decoded from bytes to unicode. - * A JSON body should be deserialized into a Python dict or list. - -When the request contains a Content-Type header, the media type is used as a -key to look up a value in the -:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict. -If the full media -type is not found, then the major type is tried; for example, if no processor -is found for the 'image/jpeg' type, then we look for a processor for the -'image' types altogether. If neither the full type nor the major type has a -matching processor, then a default processor is used -(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most -types, this means no processing is done, and the body is left unread as a -raw byte stream. Processors are configurable in an 'on_start_resource' hook. - -Some processors, especially those for the 'text' types, attempt to decode bytes -to unicode. If the Content-Type request header includes a 'charset' parameter, -this is used to decode the entity. Otherwise, one or more default charsets may -be attempted, although this decision is up to each processor. If a processor -successfully decodes an Entity or Part, it should set the -:attr:`charset<cherrypy._cpreqbody.Entity.charset>` attribute -on the Entity or Part to the name of the successful charset, so that -applications can easily re-encode or transcode the value if they wish. - -If the Content-Type of the request entity is of major type 'multipart', then -the above parsing process, and possibly a decoding process, is performed for -each part. - -For both the full entity and multipart parts, a Content-Disposition header may -be used to fill :attr:`name<cherrypy._cpreqbody.Entity.name>` and -:attr:`filename<cherrypy._cpreqbody.Entity.filename>` attributes on the -request.body or the Part. - -.. _custombodyprocessors: - -Custom Processors -================= - -You can add your own processors for any specific or major MIME type. Simply add -it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a -hook/tool that runs at ``on_start_resource`` or ``before_request_body``. -Here's the built-in JSON tool for an example:: - - def json_in(force=True, debug=False): - request = cherrypy.serving.request - def json_processor(entity): - '''Read application/json data into request.json.''' - if not entity.headers.get("Content-Length", ""): - raise cherrypy.HTTPError(411) - - body = entity.fp.read() - try: - request.json = json_decode(body) - except ValueError: - raise cherrypy.HTTPError(400, 'Invalid JSON document') - if force: - request.body.processors.clear() - request.body.default_proc = cherrypy.HTTPError( - 415, 'Expected an application/json content type') - request.body.processors['application/json'] = json_processor - -We begin by defining a new ``json_processor`` function to stick in the -``processors`` dictionary. All processor functions take a single argument, -the ``Entity`` instance they are to process. It will be called whenever a -request is received (for those URI's where the tool is turned on) which -has a ``Content-Type`` of "application/json". - -First, it checks for a valid ``Content-Length`` (raising 411 if not valid), -then reads the remaining bytes on the socket. The ``fp`` object knows its -own length, so it won't hang waiting for data that never arrives. It will -return when all data has been read. Then, we decode those bytes using -Python's built-in ``json`` module, and stick the decoded result onto -``request.json`` . If it cannot be decoded, we raise 400. - -If the "force" argument is True (the default), the ``Tool`` clears the -``processors`` dict so that request entities of other ``Content-Types`` -aren't parsed at all. Since there's no entry for those invalid MIME -types, the ``default_proc`` method of ``cherrypy.request.body`` is -called. But this does nothing by default (usually to provide the page -handler an opportunity to handle it.) -But in our case, we want to raise 415, so we replace -``request.body.default_proc`` -with the error (``HTTPError`` instances, when called, raise themselves). - -If we were defining a custom processor, we can do so without making a ``Tool``. -Just add the config entry:: - - request.body.processors = {'application/json': json_processor} - -Note that you can only replace the ``processors`` dict wholesale this way, -not update the existing one. -""" - -try: - from io import DEFAULT_BUFFER_SIZE -except ImportError: - DEFAULT_BUFFER_SIZE = 8192 -import re -import sys -import tempfile -try: - from urllib import unquote_plus -except ImportError: - def unquote_plus(bs): - """Bytes version of urllib.parse.unquote_plus.""" - bs = bs.replace(b'+', b' ') - atoms = bs.split(b'%') - for i in range(1, len(atoms)): - item = atoms[i] - try: - pct = int(item[:2], 16) - atoms[i] = bytes([pct]) + item[2:] - except ValueError: - pass - return b''.join(atoms) - -import six -import cheroot.server - -import cherrypy -from cherrypy._cpcompat import ntou, unquote -from cherrypy.lib import httputil - - -# ------------------------------- Processors -------------------------------- # - -def process_urlencoded(entity): - """Read application/x-www-form-urlencoded data into entity.params.""" - qs = entity.fp.read() - for charset in entity.attempt_charsets: - try: - params = {} - for aparam in qs.split(b'&'): - for pair in aparam.split(b';'): - if not pair: - continue - - atoms = pair.split(b'=', 1) - if len(atoms) == 1: - atoms.append(b'') - - key = unquote_plus(atoms[0]).decode(charset) - value = unquote_plus(atoms[1]).decode(charset) - - if key in params: - if not isinstance(params[key], list): - params[key] = [params[key]] - params[key].append(value) - else: - params[key] = value - except UnicodeDecodeError: - pass - else: - entity.charset = charset - break - else: - raise cherrypy.HTTPError( - 400, 'The request entity could not be decoded. The following ' - 'charsets were attempted: %s' % repr(entity.attempt_charsets)) - - # Now that all values have been successfully parsed and decoded, - # apply them to the entity.params dict. - for key, value in params.items(): - if key in entity.params: - if not isinstance(entity.params[key], list): - entity.params[key] = [entity.params[key]] - entity.params[key].append(value) - else: - entity.params[key] = value - - -def process_multipart(entity): - """Read all multipart parts into entity.parts.""" - ib = '' - if 'boundary' in entity.content_type.params: - # http://tools.ietf.org/html/rfc2046#section-5.1.1 - # "The grammar for parameters on the Content-type field is such that it - # is often necessary to enclose the boundary parameter values in quotes - # on the Content-type line" - ib = entity.content_type.params['boundary'].strip('"') - - if not re.match('^[ -~]{0,200}[!-~]$', ib): - raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) - - ib = ('--' + ib).encode('ascii') - - # Find the first marker - while True: - b = entity.readline() - if not b: - return - - b = b.strip() - if b == ib: - break - - # Read all parts - while True: - part = entity.part_class.from_fp(entity.fp, ib) - entity.parts.append(part) - part.process() - if part.fp.done: - break - - -def process_multipart_form_data(entity): - """Read all multipart/form-data parts into entity.parts or entity.params. - """ - process_multipart(entity) - - kept_parts = [] - for part in entity.parts: - if part.name is None: - kept_parts.append(part) - else: - if part.filename is None: - # It's a regular field - value = part.fullvalue() - else: - # It's a file upload. Retain the whole part so consumer code - # has access to its .file and .filename attributes. - value = part - - if part.name in entity.params: - if not isinstance(entity.params[part.name], list): - entity.params[part.name] = [entity.params[part.name]] - entity.params[part.name].append(value) - else: - entity.params[part.name] = value - - entity.parts = kept_parts - - -def _old_process_multipart(entity): - """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" - process_multipart(entity) - - params = entity.params - - for part in entity.parts: - if part.name is None: - key = ntou('parts') - else: - key = part.name - - if part.filename is None: - # It's a regular field - value = part.fullvalue() - else: - # It's a file upload. Retain the whole part so consumer code - # has access to its .file and .filename attributes. - value = part - - if key in params: - if not isinstance(params[key], list): - params[key] = [params[key]] - params[key].append(value) - else: - params[key] = value - - -# -------------------------------- Entities --------------------------------- # -class Entity(object): - - """An HTTP request body, or MIME multipart body. - - This class collects information about the HTTP request entity. When a - given entity is of MIME type "multipart", each part is parsed into its own - Entity instance, and the set of parts stored in - :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`. - - Between the ``before_request_body`` and ``before_handler`` tools, CherryPy - tries to process the request body (if any) by calling - :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`. - This uses the ``content_type`` of the Entity to look up a suitable - processor in - :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, - a dict. - If a matching processor cannot be found for the complete Content-Type, - it tries again using the major type. For example, if a request with an - entity of type "image/jpeg" arrives, but no processor can be found for - that complete type, then one is sought for the major type "image". If a - processor is still not found, then the - :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method - of the Entity is called (which does nothing by default; you can - override this too). - - CherryPy includes processors for the "application/x-www-form-urlencoded" - type, the "multipart/form-data" type, and the "multipart" major type. - CherryPy 3.2 processes these types almost exactly as older versions. - Parts are passed as arguments to the page handler using their - ``Content-Disposition.name`` if given, otherwise in a generic "parts" - argument. Each such part is either a string, or the - :class:`Part<cherrypy._cpreqbody.Part>` itself if it's a file. (In this - case it will have ``file`` and ``filename`` attributes, or possibly a - ``value`` attribute). Each Part is itself a subclass of - Entity, and has its own ``process`` method and ``processors`` dict. - - There is a separate processor for the "multipart" major type which is more - flexible, and simply stores all multipart parts in - :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can - enable it with:: - - cherrypy.request.body.processors['multipart'] = \ - _cpreqbody.process_multipart - - in an ``on_start_resource`` tool. - """ - - # http://tools.ietf.org/html/rfc2046#section-4.1.2: - # "The default character set, which must be assumed in the - # absence of a charset parameter, is US-ASCII." - # However, many browsers send data in utf-8 with no charset. - attempt_charsets = ['utf-8'] - r"""A list of strings, each of which should be a known encoding. - - When the Content-Type of the request body warrants it, each of the given - encodings will be tried in order. The first one to successfully decode the - entity without raising an error is stored as - :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults - to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by - `HTTP/1.1 - <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), - but ``['us-ascii', 'utf-8']`` for multipart parts. - """ - - charset = None - """The successful decoding; see "attempt_charsets" above.""" - - content_type = None - """The value of the Content-Type request header. - - If the Entity is part of a multipart payload, this will be the Content-Type - given in the MIME headers for this part. - """ - - default_content_type = 'application/x-www-form-urlencoded' - """This defines a default ``Content-Type`` to use if no Content-Type header - is given. The empty string is used for RequestBody, which results in the - request body not being read or parsed at all. This is by design; a missing - ``Content-Type`` header in the HTTP request entity is an error at best, - and a security hole at worst. For multipart parts, however, the MIME spec - declares that a part with no Content-Type defaults to "text/plain" - (see :class:`Part<cherrypy._cpreqbody.Part>`). - """ - - filename = None - """The ``Content-Disposition.filename`` header, if available.""" - - fp = None - """The readable socket file object.""" - - headers = None - """A dict of request/multipart header names and values. - - This is a copy of the ``request.headers`` for the ``request.body``; - for multipart parts, it is the set of headers for that part. - """ - - length = None - """The value of the ``Content-Length`` header, if provided.""" - - name = None - """The "name" parameter of the ``Content-Disposition`` header, if any.""" - - params = None - """ - If the request Content-Type is 'application/x-www-form-urlencoded' or - multipart, this will be a dict of the params pulled from the entity - body; that is, it will be the portion of request.params that come - from the message body (sometimes called "POST params", although they - can be sent with various HTTP method verbs). This value is set between - the 'before_request_body' and 'before_handler' hooks (assuming that - process_request_body is True).""" - - processors = {'application/x-www-form-urlencoded': process_urlencoded, - 'multipart/form-data': process_multipart_form_data, - 'multipart': process_multipart, - } - """A dict of Content-Type names to processor methods.""" - - parts = None - """A list of Part instances if ``Content-Type`` is of major type - "multipart".""" - - part_class = None - """The class used for multipart parts. - - You can replace this with custom subclasses to alter the processing of - multipart parts. - """ - - def __init__(self, fp, headers, params=None, parts=None): - # Make an instance-specific copy of the class processors - # so Tools, etc. can replace them per-request. - self.processors = self.processors.copy() - - self.fp = fp - self.headers = headers - - if params is None: - params = {} - self.params = params - - if parts is None: - parts = [] - self.parts = parts - - # Content-Type - self.content_type = headers.elements('Content-Type') - if self.content_type: - self.content_type = self.content_type[0] - else: - self.content_type = httputil.HeaderElement.from_str( - self.default_content_type) - - # Copy the class 'attempt_charsets', prepending any Content-Type - # charset - dec = self.content_type.params.get('charset', None) - if dec: - self.attempt_charsets = [dec] + [c for c in self.attempt_charsets - if c != dec] - else: - self.attempt_charsets = self.attempt_charsets[:] - - # Length - self.length = None - clen = headers.get('Content-Length', None) - # If Transfer-Encoding is 'chunked', ignore any Content-Length. - if ( - clen is not None and - 'chunked' not in headers.get('Transfer-Encoding', '') - ): - try: - self.length = int(clen) - except ValueError: - pass - - # Content-Disposition - self.name = None - self.filename = None - disp = headers.elements('Content-Disposition') - if disp: - disp = disp[0] - if 'name' in disp.params: - self.name = disp.params['name'] - if self.name.startswith('"') and self.name.endswith('"'): - self.name = self.name[1:-1] - if 'filename' in disp.params: - self.filename = disp.params['filename'] - if ( - self.filename.startswith('"') and - self.filename.endswith('"') - ): - self.filename = self.filename[1:-1] - if 'filename*' in disp.params: - # @see https://tools.ietf.org/html/rfc5987 - encoding, lang, filename = disp.params['filename*'].split("'") - self.filename = unquote(str(filename), encoding) - - def read(self, size=None, fp_out=None): - return self.fp.read(size, fp_out) - - def readline(self, size=None): - return self.fp.readline(size) - - def readlines(self, sizehint=None): - return self.fp.readlines(sizehint) - - def __iter__(self): - return self - - def __next__(self): - line = self.readline() - if not line: - raise StopIteration - return line - - def next(self): - return self.__next__() - - def read_into_file(self, fp_out=None): - """Read the request body into fp_out (or make_file() if None). - - Return fp_out. - """ - if fp_out is None: - fp_out = self.make_file() - self.read(fp_out=fp_out) - return fp_out - - def make_file(self): - """Return a file-like object into which the request body will be read. - - By default, this will return a TemporaryFile. Override as needed. - See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" - return tempfile.TemporaryFile() - - def fullvalue(self): - """Return this entity as a string, whether stored in a file or not.""" - if self.file: - # It was stored in a tempfile. Read it. - self.file.seek(0) - value = self.file.read() - self.file.seek(0) - else: - value = self.value - value = self.decode_entity(value) - return value - - def decode_entity(self, value): - """Return a given byte encoded value as a string""" - for charset in self.attempt_charsets: - try: - value = value.decode(charset) - except UnicodeDecodeError: - pass - else: - self.charset = charset - return value - else: - raise cherrypy.HTTPError( - 400, - 'The request entity could not be decoded. The following ' - 'charsets were attempted: %s' % repr(self.attempt_charsets) - ) - - def process(self): - """Execute the best-match processor for the given media type.""" - proc = None - ct = self.content_type.value - try: - proc = self.processors[ct] - except KeyError: - toptype = ct.split('/', 1)[0] - try: - proc = self.processors[toptype] - except KeyError: - pass - if proc is None: - self.default_proc() - else: - proc(self) - - def default_proc(self): - """Called if a more-specific processor is not found for the - ``Content-Type``. - """ - # Leave the fp alone for someone else to read. This works fine - # for request.body, but the Part subclasses need to override this - # so they can move on to the next part. - pass - - -class Part(Entity): - - """A MIME part entity, part of a multipart entity.""" - - # "The default character set, which must be assumed in the absence of a - # charset parameter, is US-ASCII." - attempt_charsets = ['us-ascii', 'utf-8'] - r"""A list of strings, each of which should be a known encoding. - - When the Content-Type of the request body warrants it, each of the given - encodings will be tried in order. The first one to successfully decode the - entity without raising an error is stored as - :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults - to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by - `HTTP/1.1 - <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), - but ``['us-ascii', 'utf-8']`` for multipart parts. - """ - - boundary = None - """The MIME multipart boundary.""" - - default_content_type = 'text/plain' - """This defines a default ``Content-Type`` to use if no Content-Type header - is given. The empty string is used for RequestBody, which results in the - request body not being read or parsed at all. This is by design; a missing - ``Content-Type`` header in the HTTP request entity is an error at best, - and a security hole at worst. For multipart parts, however (this class), - the MIME spec declares that a part with no Content-Type defaults to - "text/plain". - """ - - # This is the default in stdlib cgi. We may want to increase it. - maxrambytes = 1000 - """The threshold of bytes after which point the ``Part`` will store - its data in a file (generated by - :func:`make_file<cherrypy._cprequest.Entity.make_file>`) - instead of a string. Defaults to 1000, just like the :mod:`cgi` - module in Python's standard library. - """ - - def __init__(self, fp, headers, boundary): - Entity.__init__(self, fp, headers) - self.boundary = boundary - self.file = None - self.value = None - - @classmethod - def from_fp(cls, fp, boundary): - headers = cls.read_headers(fp) - return cls(fp, headers, boundary) - - @classmethod - def read_headers(cls, fp): - headers = httputil.HeaderMap() - while True: - line = fp.readline() - if not line: - # No more data--illegal end of headers - raise EOFError('Illegal end of headers.') - - if line == b'\r\n': - # Normal end of headers - break - if not line.endswith(b'\r\n'): - raise ValueError('MIME requires CRLF terminators: %r' % line) - - if line[0] in b' \t': - # It's a continuation line. - v = line.strip().decode('ISO-8859-1') - else: - k, v = line.split(b':', 1) - k = k.strip().decode('ISO-8859-1') - v = v.strip().decode('ISO-8859-1') - - existing = headers.get(k) - if existing: - v = ', '.join((existing, v)) - headers[k] = v - - return headers - - def read_lines_to_boundary(self, fp_out=None): - """Read bytes from self.fp and return or write them to a file. - - If the 'fp_out' argument is None (the default), all bytes read are - returned in a single byte string. - - If the 'fp_out' argument is not None, it must be a file-like - object that supports the 'write' method; all bytes read will be - written to the fp, and that fp is returned. - """ - endmarker = self.boundary + b'--' - delim = b'' - prev_lf = True - lines = [] - seen = 0 - while True: - line = self.fp.readline(1 << 16) - if not line: - raise EOFError('Illegal end of multipart body.') - if line.startswith(b'--') and prev_lf: - strippedline = line.strip() - if strippedline == self.boundary: - break - if strippedline == endmarker: - self.fp.finish() - break - - line = delim + line - - if line.endswith(b'\r\n'): - delim = b'\r\n' - line = line[:-2] - prev_lf = True - elif line.endswith(b'\n'): - delim = b'\n' - line = line[:-1] - prev_lf = True - else: - delim = b'' - prev_lf = False - - if fp_out is None: - lines.append(line) - seen += len(line) - if seen > self.maxrambytes: - fp_out = self.make_file() - for line in lines: - fp_out.write(line) - else: - fp_out.write(line) - - if fp_out is None: - result = b''.join(lines) - return result - else: - fp_out.seek(0) - return fp_out - - def default_proc(self): - """Called if a more-specific processor is not found for the - ``Content-Type``. - """ - if self.filename: - # Always read into a file if a .filename was given. - self.file = self.read_into_file() - else: - result = self.read_lines_to_boundary() - if isinstance(result, bytes): - self.value = result - else: - self.file = result - - def read_into_file(self, fp_out=None): - """Read the request body into fp_out (or make_file() if None). - - Return fp_out. - """ - if fp_out is None: - fp_out = self.make_file() - self.read_lines_to_boundary(fp_out=fp_out) - return fp_out - - -Entity.part_class = Part - -inf = float('inf') - - -class SizedReader: - - def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, - has_trailers=False): - # Wrap our fp in a buffer so peek() works - self.fp = fp - self.length = length - self.maxbytes = maxbytes - self.buffer = b'' - self.bufsize = bufsize - self.bytes_read = 0 - self.done = False - self.has_trailers = has_trailers - - def read(self, size=None, fp_out=None): - """Read bytes from the request body and return or write them to a file. - - A number of bytes less than or equal to the 'size' argument are read - off the socket. The actual number of bytes read are tracked in - self.bytes_read. The number may be smaller than 'size' when 1) the - client sends fewer bytes, 2) the 'Content-Length' request header - specifies fewer bytes than requested, or 3) the number of bytes read - exceeds self.maxbytes (in which case, 413 is raised). - - If the 'fp_out' argument is None (the default), all bytes read are - returned in a single byte string. - - If the 'fp_out' argument is not None, it must be a file-like - object that supports the 'write' method; all bytes read will be - written to the fp, and None is returned. - """ - - if self.length is None: - if size is None: - remaining = inf - else: - remaining = size - else: - remaining = self.length - self.bytes_read - if size and size < remaining: - remaining = size - if remaining == 0: - self.finish() - if fp_out is None: - return b'' - else: - return None - - chunks = [] - - # Read bytes from the buffer. - if self.buffer: - if remaining is inf: - data = self.buffer - self.buffer = b'' - else: - data = self.buffer[:remaining] - self.buffer = self.buffer[remaining:] - datalen = len(data) - remaining -= datalen - - # Check lengths. - self.bytes_read += datalen - if self.maxbytes and self.bytes_read > self.maxbytes: - raise cherrypy.HTTPError(413) - - # Store the data. - if fp_out is None: - chunks.append(data) - else: - fp_out.write(data) - - # Read bytes from the socket. - while remaining > 0: - chunksize = min(remaining, self.bufsize) - try: - data = self.fp.read(chunksize) - except Exception: - e = sys.exc_info()[1] - if e.__class__.__name__ == 'MaxSizeExceeded': - # Post data is too big - raise cherrypy.HTTPError( - 413, 'Maximum request length: %r' % e.args[1]) - else: - raise - if not data: - self.finish() - break - datalen = len(data) - remaining -= datalen - - # Check lengths. - self.bytes_read += datalen - if self.maxbytes and self.bytes_read > self.maxbytes: - raise cherrypy.HTTPError(413) - - # Store the data. - if fp_out is None: - chunks.append(data) - else: - fp_out.write(data) - - if fp_out is None: - return b''.join(chunks) - - def readline(self, size=None): - """Read a line from the request body and return it.""" - chunks = [] - while size is None or size > 0: - chunksize = self.bufsize - if size is not None and size < self.bufsize: - chunksize = size - data = self.read(chunksize) - if not data: - break - pos = data.find(b'\n') + 1 - if pos: - chunks.append(data[:pos]) - remainder = data[pos:] - self.buffer += remainder - self.bytes_read -= len(remainder) - break - else: - chunks.append(data) - return b''.join(chunks) - - def readlines(self, sizehint=None): - """Read lines from the request body and return them.""" - if self.length is not None: - if sizehint is None: - sizehint = self.length - self.bytes_read - else: - sizehint = min(sizehint, self.length - self.bytes_read) - - lines = [] - seen = 0 - while True: - line = self.readline() - if not line: - break - lines.append(line) - seen += len(line) - if seen >= sizehint: - break - return lines - - def finish(self): - self.done = True - if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): - self.trailers = {} - - try: - for line in self.fp.read_trailer_lines(): - if line[0] in b' \t': - # It's a continuation line. - v = line.strip() - else: - try: - k, v = line.split(b':', 1) - except ValueError: - raise ValueError('Illegal header line.') - k = k.strip().title() - v = v.strip() - - if k in cheroot.server.comma_separated_headers: - existing = self.trailers.get(k) - if existing: - v = b', '.join((existing, v)) - self.trailers[k] = v - except Exception: - e = sys.exc_info()[1] - if e.__class__.__name__ == 'MaxSizeExceeded': - # Post data is too big - raise cherrypy.HTTPError( - 413, 'Maximum request length: %r' % e.args[1]) - else: - raise - - -class RequestBody(Entity): - - """The entity of the HTTP request.""" - - bufsize = 8 * 1024 - """The buffer size used when reading the socket.""" - - # Don't parse the request body at all if the client didn't provide - # a Content-Type header. See - # https://github.com/cherrypy/cherrypy/issues/790 - default_content_type = '' - """This defines a default ``Content-Type`` to use if no Content-Type header - is given. The empty string is used for RequestBody, which results in the - request body not being read or parsed at all. This is by design; a missing - ``Content-Type`` header in the HTTP request entity is an error at best, - and a security hole at worst. For multipart parts, however, the MIME spec - declares that a part with no Content-Type defaults to "text/plain" - (see :class:`Part<cherrypy._cpreqbody.Part>`). - """ - - maxbytes = None - """Raise ``MaxSizeExceeded`` if more bytes than this are read from - the socket. - """ - - def __init__(self, fp, headers, params=None, request_params=None): - Entity.__init__(self, fp, headers, params) - - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 - # When no explicit charset parameter is provided by the - # sender, media subtypes of the "text" type are defined - # to have a default charset value of "ISO-8859-1" when - # received via HTTP. - if self.content_type.value.startswith('text/'): - for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): - if c in self.attempt_charsets: - break - else: - self.attempt_charsets.append('ISO-8859-1') - - # Temporary fix while deprecating passing .parts as .params. - self.processors['multipart'] = _old_process_multipart - - if request_params is None: - request_params = {} - self.request_params = request_params - - def process(self): - """Process the request entity based on its Content-Type.""" - # "The presence of a message-body in a request is signaled by the - # inclusion of a Content-Length or Transfer-Encoding header field in - # the request's message-headers." - # It is possible to send a POST request with no body, for example; - # however, app developers are responsible in that case to set - # cherrypy.request.process_body to False so this method isn't called. - h = cherrypy.serving.request.headers - if 'Content-Length' not in h and 'Transfer-Encoding' not in h: - raise cherrypy.HTTPError(411) - - self.fp = SizedReader(self.fp, self.length, - self.maxbytes, bufsize=self.bufsize, - has_trailers='Trailer' in h) - super(RequestBody, self).process() - - # Body params should also be a part of the request_params - # add them in here. - request_params = self.request_params - for key, value in self.params.items(): - # Python 2 only: keyword arguments must be byte strings (type - # 'str'). - if sys.version_info < (3, 0): - if isinstance(key, six.text_type): - key = key.encode('ISO-8859-1') - - if key in request_params: - if not isinstance(request_params[key], list): - request_params[key] = [request_params[key]] - request_params[key].append(value) - else: - request_params[key] = value diff --git a/libraries/cherrypy/_cprequest.py b/libraries/cherrypy/_cprequest.py deleted file mode 100644 index 3cc0c811..00000000 --- a/libraries/cherrypy/_cprequest.py +++ /dev/null @@ -1,930 +0,0 @@ -import sys -import time - -import uuid - -import six -from six.moves.http_cookies import SimpleCookie, CookieError - -from more_itertools import consume - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy import _cpreqbody -from cherrypy._cperror import format_exc, bare_error -from cherrypy.lib import httputil, reprconf, encoding - - -class Hook(object): - - """A callback and its metadata: failsafe, priority, and kwargs.""" - - callback = None - """ - The bare callable that this Hook object is wrapping, which will - be called when the Hook is called.""" - - failsafe = False - """ - If True, the callback is guaranteed to run even if other callbacks - from the same call point raise exceptions.""" - - priority = 50 - """ - Defines the order of execution for a list of Hooks. Priority numbers - should be limited to the closed interval [0, 100], but values outside - this range are acceptable, as are fractional values.""" - - kwargs = {} - """ - A set of keyword arguments that will be passed to the - callable on each call.""" - - def __init__(self, callback, failsafe=None, priority=None, **kwargs): - self.callback = callback - - if failsafe is None: - failsafe = getattr(callback, 'failsafe', False) - self.failsafe = failsafe - - if priority is None: - priority = getattr(callback, 'priority', 50) - self.priority = priority - - self.kwargs = kwargs - - def __lt__(self, other): - """ - Hooks sort by priority, ascending, such that - hooks of lower priority are run first. - """ - return self.priority < other.priority - - def __call__(self): - """Run self.callback(**self.kwargs).""" - return self.callback(**self.kwargs) - - def __repr__(self): - cls = self.__class__ - return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)' - % (cls.__module__, cls.__name__, self.callback, - self.failsafe, self.priority, - ', '.join(['%s=%r' % (k, v) - for k, v in self.kwargs.items()]))) - - -class HookMap(dict): - - """A map of call points to lists of callbacks (Hook objects).""" - - def __new__(cls, points=None): - d = dict.__new__(cls) - for p in points or []: - d[p] = [] - return d - - def __init__(self, *a, **kw): - pass - - def attach(self, point, callback, failsafe=None, priority=None, **kwargs): - """Append a new Hook made from the supplied arguments.""" - self[point].append(Hook(callback, failsafe, priority, **kwargs)) - - def run(self, point): - """Execute all registered Hooks (callbacks) for the given point.""" - exc = None - hooks = self[point] - hooks.sort() - for hook in hooks: - # Some hooks are guaranteed to run even if others at - # the same hookpoint fail. We will still log the failure, - # but proceed on to the next hook. The only way - # to stop all processing from one of these hooks is - # to raise SystemExit and stop the whole server. - if exc is None or hook.failsafe: - try: - hook() - except (KeyboardInterrupt, SystemExit): - raise - except (cherrypy.HTTPError, cherrypy.HTTPRedirect, - cherrypy.InternalRedirect): - exc = sys.exc_info()[1] - except Exception: - exc = sys.exc_info()[1] - cherrypy.log(traceback=True, severity=40) - if exc: - raise exc - - def __copy__(self): - newmap = self.__class__() - # We can't just use 'update' because we want copies of the - # mutable values (each is a list) as well. - for k, v in self.items(): - newmap[k] = v[:] - return newmap - copy = __copy__ - - def __repr__(self): - cls = self.__class__ - return '%s.%s(points=%r)' % ( - cls.__module__, - cls.__name__, - list(self) - ) - - -# Config namespace handlers - -def hooks_namespace(k, v): - """Attach bare hooks declared in config.""" - # Use split again to allow multiple hooks for a single - # hookpoint per path (e.g. "hooks.before_handler.1"). - # Little-known fact you only get from reading source ;) - hookpoint = k.split('.', 1)[0] - if isinstance(v, six.string_types): - v = cherrypy.lib.reprconf.attributes(v) - if not isinstance(v, Hook): - v = Hook(v) - cherrypy.serving.request.hooks[hookpoint].append(v) - - -def request_namespace(k, v): - """Attach request attributes declared in config.""" - # Provides config entries to set request.body attrs (like - # attempt_charsets). - if k[:5] == 'body.': - setattr(cherrypy.serving.request.body, k[5:], v) - else: - setattr(cherrypy.serving.request, 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 - if k[:8] == 'headers.': - cherrypy.serving.response.headers[k.split('.', 1)[1]] = v - else: - setattr(cherrypy.serving.response, k, v) - - -def error_page_namespace(k, v): - """Attach error pages declared in config.""" - if k != 'default': - k = int(k) - cherrypy.serving.request.error_page[k] = v - - -hookpoints = ['on_start_resource', 'before_request_body', - 'before_handler', 'before_finalize', - 'on_end_resource', 'on_end_request', - 'before_error_response', 'after_error_response'] - - -class Request(object): - - """An HTTP request. - - This object represents the metadata of an HTTP request message; - that is, it contains attributes which describe the environment - in which the request URL, headers, and body were sent (if you - want tools to interpret the headers and body, those are elsewhere, - mostly in Tools). This 'metadata' consists of socket data, - transport characteristics, and the Request-Line. This object - also contains data regarding the configuration in effect for - the given URL, and the execution plan for generating a response. - """ - - prev = None - """ - The previous Request object (if any). This should be None - unless we are processing an InternalRedirect.""" - - # Conversation/connection attributes - local = httputil.Host('127.0.0.1', 80) - 'An httputil.Host(ip, port, hostname) object for the server socket.' - - remote = httputil.Host('127.0.0.1', 1111) - 'An httputil.Host(ip, port, hostname) object for the client socket.' - - scheme = 'http' - """ - The protocol used between client and server. In most cases, - this will be either 'http' or 'https'.""" - - server_protocol = 'HTTP/1.1' - """ - The HTTP version for which the HTTP server is at least - conditionally compliant.""" - - base = '' - """The (scheme://host) portion of the requested URL. - In some cases (e.g. when proxying via mod_rewrite), this may contain - path segments which cherrypy.url uses when constructing url's, but - which otherwise are ignored by CherryPy. Regardless, this value - MUST NOT end in a slash.""" - - # Request-Line attributes - request_line = '' - """ - The complete Request-Line received from the client. This is a - single string consisting of the request method, URI, and protocol - version (joined by spaces). Any final CRLF is removed.""" - - method = 'GET' - """ - Indicates the HTTP method to be performed on the resource identified - by the Request-URI. Common methods include GET, HEAD, POST, PUT, and - DELETE. CherryPy allows any extension method; however, various HTTP - servers and gateways may restrict the set of allowable methods. - CherryPy applications SHOULD restrict the set (on a per-URI basis).""" - - query_string = '' - """ - 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, - 'a=3&b=4'.""" - - query_string_encoding = 'utf8' - """ - The encoding expected for query string arguments after % HEX HEX decoding). - If a query string is provided that cannot be decoded with this encoding, - 404 is raised (since technically it's a different URI). If you want - arbitrary encodings to not error, set this to 'Latin-1'; you can then - encode back to bytes and re-decode to whatever encoding you like later. - """ - - protocol = (1, 1) - """The HTTP protocol version corresponding to the set - of features which should be allowed in the response. If BOTH - the client's request message AND the server's level of HTTP - compliance is HTTP/1.1, this attribute will be the tuple (1, 1). - If either is 1.0, this attribute will be the tuple (1, 0). - Lower HTTP protocol versions are not explicitly supported.""" - - params = {} - """ - A dict which combines query string (GET) and request entity (POST) - variables. This is populated in two stages: GET params are added - before the 'on_start_resource' hook, and POST params are added - between the 'before_request_body' and 'before_handler' hooks.""" - - # Message attributes - header_list = [] - """ - A list of the HTTP request headers as (name, value) tuples. - In general, you should use request.headers (a dict) instead.""" - - headers = httputil.HeaderMap() - """ - A dict-like object containing the request headers. Keys are header - names (in Title-Case format); however, you may get and set them in - a case-insensitive manner. That is, headers['Content-Type'] and - headers['content-type'] refer to the same value. Values are header - values (decoded according to :rfc:`2047` if necessary). See also: - httputil.HeaderMap, httputil.HeaderElement.""" - - cookie = SimpleCookie() - """See help(Cookie).""" - - rfile = None - """ - If the request included an entity (body), it will be available - as a stream in this attribute. However, the rfile will normally - be read for you between the 'before_request_body' hook and the - 'before_handler' hook, and the resulting string is placed into - either request.params or the request.body attribute. - - You may disable the automatic consumption of the rfile by setting - request.process_request_body to False, either in config for the desired - path, or in an 'on_start_resource' or 'before_request_body' hook. - - WARNING: In almost every case, you should not attempt to read from the - rfile stream after CherryPy's automatic mechanism has read it. If you - turn off the automatic parsing of rfile, you should read exactly the - number of bytes specified in request.headers['Content-Length']. - Ignoring either of these warnings may result in a hung request thread - or in corruption of the next (pipelined) request. - """ - - process_request_body = True - """ - If True, the rfile (if any) is automatically read and parsed, - and the result placed into request.params or request.body.""" - - methods_with_bodies = ('POST', 'PUT', 'PATCH') - """ - A sequence of HTTP methods for which CherryPy will automatically - attempt to read a body from the rfile. If you are going to change - this property, modify it on the configuration (recommended) - or on the "hook point" `on_start_resource`. - """ - - body = None - """ - If the request Content-Type is 'application/x-www-form-urlencoded' - or multipart, this will be None. Otherwise, this will be an instance - of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you - can .read()); this value is set between the 'before_request_body' and - 'before_handler' hooks (assuming that process_request_body is True).""" - - # Dispatch attributes - dispatch = cherrypy.dispatch.Dispatcher() - """ - The object which looks up the 'page handler' callable and collects - config for the current request based on the path_info, other - request attributes, and the application architecture. The core - calls the dispatcher as early as possible, passing it a 'path_info' - argument. - - The default dispatcher discovers the page handler by matching path_info - to a hierarchical arrangement of objects, starting at request.app.root. - See help(cherrypy.dispatch) for more information.""" - - script_name = '' - """ - The 'mount point' of the application which is handling this request. - - This attribute MUST NOT end in a slash. If the script_name refers to - the root of the URI, it MUST be an empty string (not "/"). - """ - - path_info = '/' - """ - The 'relative path' portion of the Request-URI. This is relative - to the script_name ('mount point') of the application which is - handling this request.""" - - login = None - """ - When authentication is used during the request processing this is - set to 'False' if it failed and to the 'username' value if it succeeded. - The default 'None' implies that no authentication happened.""" - - # Note that cherrypy.url uses "if request.app:" to determine whether - # the call is during a real HTTP request or not. So leave this None. - app = None - """The cherrypy.Application object which is handling this request.""" - - handler = None - """ - The function, method, or other callable which CherryPy will call to - produce the response. The discovery of the handler and the arguments - it will receive are determined by the request.dispatch object. - By default, the handler is discovered by walking a tree of objects - starting at request.app.root, and is then passed all HTTP params - (from the query string and POST body) as keyword arguments.""" - - toolmaps = {} - """ - A nested dict of all Toolboxes and Tools in effect for this request, - of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" - - config = None - """ - A flat dict of all configuration entries which apply to the - current request. These entries are collected from global config, - application config (based on request.path_info), and from handler - config (exactly how is governed by the request.dispatch object in - effect for this request; by default, handler config can be attached - anywhere in the tree between request.app.root and the final handler, - and inherits downward).""" - - is_index = None - """ - This will be True if the current request is mapped to an 'index' - resource handler (also, a 'default' handler if path_info ends with - a slash). The value may be used to automatically redirect the - user-agent to a 'more canonical' URL which either adds or removes - the trailing slash. See cherrypy.tools.trailing_slash.""" - - hooks = HookMap(hookpoints) - """ - A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. - Each key is a str naming the hook point, and each value is a list - of hooks which will be called at that hook point during this request. - The list of hooks is generally populated as early as possible (mostly - from Tools specified in config), but may be extended at any time. - See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" - - error_response = cherrypy.HTTPError(500).set_response - """ - The no-arg callable which will handle unexpected, untrapped errors - during request processing. This is not used for expected exceptions - (like NotFound, HTTPError, or HTTPRedirect) which are raised in - response to expected conditions (those should be customized either - via request.error_page or by overriding HTTPError.set_response). - By default, error_response uses HTTPError(500) to return a generic - error response to the user-agent.""" - - error_page = {} - """ - A dict of {error code: response filename or callable} pairs. - - The error code must be an int representing a given HTTP error code, - or the string 'default', which will be used if no matching entry - is found for a given numeric code. - - If a filename is provided, the file should contain a Python string- - formatting template, and can expect by default to receive format - values with the mapping keys %(status)s, %(message)s, %(traceback)s, - and %(version)s. The set of format mappings can be extended by - overriding HTTPError.set_response. - - If a callable is provided, it will be called by default with keyword - arguments 'status', 'message', 'traceback', and 'version', as for a - string-formatting template. The callable must return a string or - iterable of strings which will be set to response.body. It may also - override headers or perform any other processing. - - If no entry is given for an error code, and no 'default' entry exists, - a default template will be used. - """ - - show_tracebacks = True - """ - If True, unexpected errors encountered during request processing will - include a traceback in the response body.""" - - show_mismatched_params = True - """ - If True, mismatched parameters encountered during PageHandler invocation - processing will be included in the response body.""" - - throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) - """The sequence of exceptions which Request.run does not trap.""" - - throw_errors = False - """ - If True, Request.run will not trap any errors (except HTTPRedirect and - HTTPError, which are more properly called 'exceptions', not errors).""" - - closed = False - """True once the close method has been called, False otherwise.""" - - stage = None - """ - A string containing the stage reached in the request-handling process. - This is useful when debugging a live server with hung requests.""" - - unique_id = None - """A lazy object generating and memorizing UUID4 on ``str()`` render.""" - - namespaces = reprconf.NamespaceSet( - **{'hooks': hooks_namespace, - 'request': request_namespace, - 'response': response_namespace, - 'error_page': error_page_namespace, - 'tools': cherrypy.tools, - }) - - def __init__(self, local_host, remote_host, scheme='http', - server_protocol='HTTP/1.1'): - """Populate a new Request object. - - local_host should be an httputil.Host object with the server info. - remote_host should be an httputil.Host object with the client info. - scheme should be a string, either "http" or "https". - """ - self.local = local_host - self.remote = remote_host - self.scheme = scheme - self.server_protocol = server_protocol - - self.closed = False - - # Put a *copy* of the class error_page into self. - self.error_page = self.error_page.copy() - - # Put a *copy* of the class namespaces into self. - self.namespaces = self.namespaces.copy() - - self.stage = None - - self.unique_id = LazyUUID4() - - def close(self): - """Run cleanup code. (Core)""" - if not self.closed: - self.closed = True - self.stage = 'on_end_request' - self.hooks.run('on_end_request') - self.stage = 'close' - - def run(self, method, path, query_string, req_protocol, headers, rfile): - r"""Process the Request. (Core) - - method, path, query_string, and req_protocol should be pulled directly - from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). - - path - This should be %XX-unquoted, but query_string should not be. - - When using Python 2, they both MUST be byte strings, - not unicode strings. - - When using Python 3, they both MUST be unicode strings, - not byte strings, and preferably not bytes \x00-\xFF - disguised as unicode. - - headers - A list of (name, value) tuples. - - rfile - A file-like object containing the HTTP request entity. - - When run() is done, the returned object should have 3 attributes: - - * status, e.g. "200 OK" - * header_list, a list of (name, value) tuples - * body, an iterable yielding strings - - Consumer code (HTTP servers) should then access these response - attributes to build the outbound stream. - - """ - response = cherrypy.serving.response - self.stage = 'run' - try: - self.error_response = cherrypy.HTTPError(500).set_response - - self.method = method - path = path or '/' - self.query_string = query_string or '' - self.params = {} - - # Compare request and server HTTP protocol versions, in case our - # server does not support the requested protocol. Limit our output - # to min(req, server). We want the following output: - # request server actual written supported response - # protocol protocol response protocol feature set - # a 1.0 1.0 1.0 1.0 - # b 1.0 1.1 1.1 1.0 - # c 1.1 1.0 1.0 1.0 - # d 1.1 1.1 1.1 1.1 - # Notice that, in (b), the response will be "HTTP/1.1" even though - # the client only understands 1.0. RFC 2616 10.5.6 says we should - # only return 505 if the _major_ version is different. - rp = int(req_protocol[5]), int(req_protocol[7]) - sp = int(self.server_protocol[5]), int(self.server_protocol[7]) - self.protocol = min(rp, sp) - response.headers.protocol = self.protocol - - # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). - url = path - if query_string: - url += '?' + query_string - self.request_line = '%s %s %s' % (method, url, req_protocol) - - self.header_list = list(headers) - self.headers = httputil.HeaderMap() - - self.rfile = rfile - self.body = None - - self.cookie = SimpleCookie() - self.handler = None - - # path_info should be the path from the - # app root (script_name) to the handler. - self.script_name = self.app.script_name - self.path_info = pi = path[len(self.script_name):] - - self.stage = 'respond' - self.respond(pi) - - except self.throws: - raise - except Exception: - if self.throw_errors: - raise - else: - # Failure in setup, error handler or finalize. Bypass them. - # Can't use handle_error because we may not have hooks yet. - cherrypy.log(traceback=True, severity=40) - if self.show_tracebacks: - body = format_exc() - else: - body = '' - r = bare_error(body) - response.output_status, response.header_list, response.body = r - - if self.method == 'HEAD': - # HEAD requests MUST NOT return a message-body in the response. - response.body = [] - - try: - cherrypy.log.access() - except Exception: - cherrypy.log.error(traceback=True) - - return response - - def respond(self, path_info): - """Generate a response for the resource at self.path_info. (Core)""" - try: - try: - try: - self._do_respond(path_info) - except (cherrypy.HTTPRedirect, cherrypy.HTTPError): - inst = sys.exc_info()[1] - inst.set_response() - self.stage = 'before_finalize (HTTPError)' - self.hooks.run('before_finalize') - cherrypy.serving.response.finalize() - finally: - self.stage = 'on_end_resource' - self.hooks.run('on_end_resource') - except self.throws: - raise - except Exception: - if self.throw_errors: - raise - self.handle_error() - - def _do_respond(self, path_info): - response = cherrypy.serving.response - - if self.app is None: - raise cherrypy.NotFound() - - self.hooks = self.__class__.hooks.copy() - self.toolmaps = {} - - # Get the 'Host' header, so we can HTTPRedirect properly. - self.stage = 'process_headers' - self.process_headers() - - self.stage = 'get_resource' - self.get_resource(path_info) - - self.body = _cpreqbody.RequestBody( - self.rfile, self.headers, request_params=self.params) - - self.namespaces(self.config) - - self.stage = 'on_start_resource' - self.hooks.run('on_start_resource') - - # Parse the querystring - self.stage = 'process_query_string' - self.process_query_string() - - # Process the body - if self.process_request_body: - if self.method not in self.methods_with_bodies: - self.process_request_body = False - self.stage = 'before_request_body' - self.hooks.run('before_request_body') - if self.process_request_body: - self.body.process() - - # Run the handler - self.stage = 'before_handler' - self.hooks.run('before_handler') - if self.handler: - self.stage = 'handler' - response.body = self.handler() - - # Finalize - self.stage = 'before_finalize' - self.hooks.run('before_finalize') - response.finalize() - - def process_query_string(self): - """Parse the query string into Python structures. (Core)""" - try: - p = httputil.parse_query_string( - self.query_string, encoding=self.query_string_encoding) - except UnicodeDecodeError: - raise cherrypy.HTTPError( - 404, 'The given query string could not be processed. Query ' - 'strings for this resource must be encoded with %r.' % - self.query_string_encoding) - - # Python 2 only: keyword arguments must be byte strings (type 'str'). - if six.PY2: - for key, value in p.items(): - if isinstance(key, six.text_type): - del p[key] - p[key.encode(self.query_string_encoding)] = value - self.params.update(p) - - def process_headers(self): - """Parse HTTP header data into Python structures. (Core)""" - # Process the headers into self.headers - headers = self.headers - for name, value in self.header_list: - # Call title() now (and use dict.__method__(headers)) - # so title doesn't have to be called twice. - name = name.title() - value = value.strip() - - headers[name] = httputil.decode_TEXT_maybe(value) - - # Some clients, notably Konquoror, supply multiple - # cookies on different lines with the same key. To - # handle this case, store all cookies in self.cookie. - if name == 'Cookie': - try: - self.cookie.load(value) - except CookieError as exc: - raise cherrypy.HTTPError(400, str(exc)) - - if not dict.__contains__(headers, 'Host'): - # All Internet-based HTTP/1.1 servers MUST respond with a 400 - # (Bad Request) status code to any HTTP/1.1 request message - # which lacks a Host header field. - if self.protocol >= (1, 1): - msg = "HTTP/1.1 requires a 'Host' request header." - raise cherrypy.HTTPError(400, msg) - host = dict.get(headers, 'Host') - if not host: - host = self.local.name or self.local.ip - self.base = '%s://%s' % (self.scheme, host) - - def get_resource(self, path): - """Call a dispatcher (which sets self.handler and .config). (Core)""" - # First, see if there is a custom dispatch at this URI. Custom - # dispatchers can only be specified in app.config, not in _cp_config - # (since custom dispatchers may not even have an app.root). - dispatch = self.app.find_config( - path, 'request.dispatch', self.dispatch) - - # dispatch() should set self.handler and self.config - dispatch(path) - - def handle_error(self): - """Handle the last unanticipated exception. (Core)""" - try: - self.hooks.run('before_error_response') - if self.error_response: - self.error_response() - self.hooks.run('after_error_response') - cherrypy.serving.response.finalize() - except cherrypy.HTTPRedirect: - inst = sys.exc_info()[1] - inst.set_response() - cherrypy.serving.response.finalize() - - -class ResponseBody(object): - - """The body of the HTTP response (the response entity).""" - - unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' - 'if you wish to return unicode.') - - def __get__(self, obj, objclass=None): - if obj is None: - # When calling on the class instead of an instance... - return self - else: - return obj._body - - def __set__(self, obj, value): - # Convert the given value to an iterable object. - if isinstance(value, six.text_type): - raise ValueError(self.unicode_err) - elif isinstance(value, list): - # every item in a list must be bytes... - if any(isinstance(item, six.text_type) for item in value): - raise ValueError(self.unicode_err) - - obj._body = encoding.prepare_iter(value) - - -class Response(object): - - """An HTTP Response, including status, headers, and body.""" - - status = '' - """The HTTP Status-Code and Reason-Phrase.""" - - header_list = [] - """ - A list of the HTTP response headers as (name, value) tuples. - In general, you should use response.headers (a dict) instead. This - attribute is generated from response.headers and is not valid until - after the finalize phase.""" - - headers = httputil.HeaderMap() - """ - A dict-like object containing the response headers. Keys are header - names (in Title-Case format); however, you may get and set them in - a case-insensitive manner. That is, headers['Content-Type'] and - headers['content-type'] refer to the same value. Values are header - values (decoded according to :rfc:`2047` if necessary). - - .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` - """ - - cookie = SimpleCookie() - """See help(Cookie).""" - - body = ResponseBody() - """The body (entity) of the HTTP response.""" - - time = None - """The value of time.time() when created. Use in HTTP dates.""" - - stream = False - """If False, buffer the response body.""" - - def __init__(self): - self.status = None - self.header_list = None - self._body = [] - self.time = time.time() - - self.headers = httputil.HeaderMap() - # Since we know all our keys are titled strings, we can - # bypass HeaderMap.update and get a big speed boost. - dict.update(self.headers, { - 'Content-Type': 'text/html', - 'Server': 'CherryPy/' + cherrypy.__version__, - 'Date': httputil.HTTPDate(self.time), - }) - self.cookie = SimpleCookie() - - def collapse_body(self): - """Collapse self.body to a single string; replace it and return it.""" - new_body = b''.join(self.body) - self.body = new_body - return new_body - - def _flush_body(self): - """ - Discard self.body but consume any generator such that - any finalization can occur, such as is required by - caching.tee_output(). - """ - consume(iter(self.body)) - - def finalize(self): - """Transform headers (and cookies) into self.header_list. (Core)""" - try: - code, reason, _ = httputil.valid_status(self.status) - except ValueError: - raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) - - headers = self.headers - - self.status = '%s %s' % (code, reason) - self.output_status = ntob(str(code), 'ascii') + \ - b' ' + headers.encode(reason) - - if self.stream: - # The upshot: wsgiserver will chunk the response if - # you pop Content-Length (or set it explicitly to None). - # Note that lib.static sets C-L to the file's st_size. - if dict.get(headers, 'Content-Length') is None: - dict.pop(headers, 'Content-Length', None) - elif code < 200 or code in (204, 205, 304): - # "All 1xx (informational), 204 (no content), - # and 304 (not modified) responses MUST NOT - # include a message-body." - dict.pop(headers, 'Content-Length', None) - self._flush_body() - self.body = b'' - else: - # Responses which are not streamed should have a Content-Length, - # but allow user code to set Content-Length if desired. - if dict.get(headers, 'Content-Length') is None: - content = self.collapse_body() - dict.__setitem__(headers, 'Content-Length', len(content)) - - # Transform our header dict into a list of tuples. - self.header_list = h = headers.output() - - cookie = self.cookie.output() - if cookie: - for line in cookie.split('\r\n'): - name, value = line.split(': ', 1) - if isinstance(name, six.text_type): - name = name.encode('ISO-8859-1') - if isinstance(value, six.text_type): - value = headers.encode(value) - h.append((name, value)) - - -class LazyUUID4(object): - def __str__(self): - """Return UUID4 and keep it for future calls.""" - return str(self.uuid4) - - @property - def uuid4(self): - """Provide unique id on per-request basis using UUID4. - - It's evaluated lazily on render. - """ - try: - self._uuid4 - except AttributeError: - # evaluate on first access - self._uuid4 = uuid.uuid4() - - return self._uuid4 diff --git a/libraries/cherrypy/_cpserver.py b/libraries/cherrypy/_cpserver.py deleted file mode 100644 index 0f60e2c8..00000000 --- a/libraries/cherrypy/_cpserver.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Manage HTTP servers with CherryPy.""" - -import six - -import cherrypy -from cherrypy.lib.reprconf import attributes -from cherrypy._cpcompat import text_or_bytes -from cherrypy.process.servers import ServerAdapter - - -__all__ = ('Server', ) - - -class Server(ServerAdapter): - """An adapter for an HTTP server. - - You can set attributes (like socket_host and socket_port) - on *this* object (which is probably cherrypy.server), and call - quickstart. For example:: - - cherrypy.server.socket_port = 80 - cherrypy.quickstart() - """ - - socket_port = 8080 - """The TCP port on which to listen for connections.""" - - _socket_host = '127.0.0.1' - - @property - def socket_host(self): # noqa: D401; irrelevant for properties - """The hostname or IP address on which to listen for connections. - - Host values may be any IPv4 or IPv6 address, or any valid hostname. - The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if - your hosts file prefers IPv6). The string '0.0.0.0' is a special - IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' - is the similar IN6ADDR_ANY for IPv6. The empty string or None are - not allowed. - """ - return self._socket_host - - @socket_host.setter - def socket_host(self, value): - if value == '': - raise ValueError("The empty string ('') is not an allowed value. " - "Use '0.0.0.0' instead to listen on all active " - 'interfaces (INADDR_ANY).') - self._socket_host = value - - socket_file = None - """If given, the name of the UNIX socket to use instead of TCP/IP. - - When this option is not None, the `socket_host` and `socket_port` options - are ignored.""" - - socket_queue_size = 5 - """The 'backlog' argument to socket.listen(); specifies the maximum number - of queued connections (default 5).""" - - socket_timeout = 10 - """The timeout in seconds for accepted connections (default 10).""" - - accepted_queue_size = -1 - """The maximum number of requests which will be queued up before - the server refuses to accept it (default -1, meaning no limit).""" - - accepted_queue_timeout = 10 - """The timeout in seconds for attempting to add a request to the - queue when the queue is full (default 10).""" - - shutdown_timeout = 5 - """The time to wait for HTTP worker threads to clean up.""" - - protocol_version = 'HTTP/1.1' - """The version string to write in the Status-Line of all HTTP responses, - for example, "HTTP/1.1" (the default). Depending on the HTTP server used, - this should also limit the supported features used in the response.""" - - thread_pool = 10 - """The number of worker threads to start up in the pool.""" - - thread_pool_max = -1 - """The maximum size of the worker-thread pool. Use -1 to indicate no limit. - """ - - max_request_header_size = 500 * 1024 - """The maximum number of bytes allowable in the request headers. - If exceeded, the HTTP server should return "413 Request Entity Too Large". - """ - - max_request_body_size = 100 * 1024 * 1024 - """The maximum number of bytes allowable in the request body. If exceeded, - the HTTP server should return "413 Request Entity Too Large".""" - - instance = None - """If not None, this should be an HTTP server instance (such as - cheroot.wsgi.Server) which cherrypy.server will control. - Use this when you need - more control over object instantiation than is available in the various - configuration options.""" - - ssl_context = None - """When using PyOpenSSL, an instance of SSL.Context.""" - - ssl_certificate = None - """The filename of the SSL certificate to use.""" - - ssl_certificate_chain = None - """When using PyOpenSSL, the certificate chain to pass to - Context.load_verify_locations.""" - - ssl_private_key = None - """The filename of the private key to use with SSL.""" - - ssl_ciphers = None - """The ciphers list of SSL.""" - - if six.PY3: - ssl_module = 'builtin' - """The name of a registered SSL adaptation module to use with - the builtin WSGI server. Builtin options are: 'builtin' (to - use the SSL library built into recent versions of Python). - You may also register your own classes in the - cheroot.server.ssl_adapters dict.""" - else: - ssl_module = 'pyopenssl' - """The name of a registered SSL adaptation module to use with the - builtin WSGI server. Builtin options are 'builtin' (to use the SSL - library built into recent versions of Python) and 'pyopenssl' (to - use the PyOpenSSL project, which you must install separately). You - may also register your own classes in the cheroot.server.ssl_adapters - dict.""" - - statistics = False - """Turns statistics-gathering on or off for aware HTTP servers.""" - - nodelay = True - """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" - - wsgi_version = (1, 0) - """The WSGI version tuple to use with the builtin WSGI server. - The provided options are (1, 0) [which includes support for PEP 3333, - which declares it covers WSGI version 1.0.1 but still mandates the - wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. - You may create and register your own experimental versions of the WSGI - protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. - """ - - peercreds = False - """If True, peer cred lookup for UNIX domain socket will put to WSGI env. - - This information will then be available through WSGI env vars: - * X_REMOTE_PID - * X_REMOTE_UID - * X_REMOTE_GID - """ - - peercreds_resolve = False - """If True, username/group will be looked up in the OS from peercreds. - - This information will then be available through WSGI env vars: - * REMOTE_USER - * X_REMOTE_USER - * X_REMOTE_GROUP - """ - - def __init__(self): - """Initialize Server instance.""" - self.bus = cherrypy.engine - self.httpserver = None - self.interrupt = None - self.running = False - - def httpserver_from_self(self, httpserver=None): - """Return a (httpserver, bind_addr) pair based on self attributes.""" - if httpserver is None: - httpserver = self.instance - if httpserver is None: - from cherrypy import _cpwsgi_server - httpserver = _cpwsgi_server.CPWSGIServer(self) - if isinstance(httpserver, text_or_bytes): - # Is anyone using this? Can I add an arg? - httpserver = attributes(httpserver)(self) - return httpserver, self.bind_addr - - def start(self): - """Start the HTTP server.""" - if not self.httpserver: - self.httpserver, self.bind_addr = self.httpserver_from_self() - super(Server, self).start() - start.priority = 75 - - @property - def bind_addr(self): - """Return bind address. - - A (host, port) tuple for TCP sockets or a str for Unix domain sockts. - """ - if self.socket_file: - return self.socket_file - if self.socket_host is None and self.socket_port is None: - return None - return (self.socket_host, self.socket_port) - - @bind_addr.setter - def bind_addr(self, value): - if value is None: - self.socket_file = None - self.socket_host = None - self.socket_port = None - elif isinstance(value, text_or_bytes): - self.socket_file = value - self.socket_host = None - self.socket_port = None - else: - try: - self.socket_host, self.socket_port = value - self.socket_file = None - except ValueError: - raise ValueError('bind_addr must be a (host, port) tuple ' - '(for TCP sockets) or a string (for Unix ' - 'domain sockets), not %r' % value) - - def base(self): - """Return the base for this server. - - e.i. scheme://host[:port] or sock file - """ - if self.socket_file: - return self.socket_file - - host = self.socket_host - if host in ('0.0.0.0', '::'): - # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. - # Look up the host name, which should be the - # safest thing to spit out in a URL. - import socket - host = socket.gethostname() - - port = self.socket_port - - if self.ssl_certificate: - scheme = 'https' - if port != 443: - host += ':%s' % port - else: - scheme = 'http' - if port != 80: - host += ':%s' % port - - return '%s://%s' % (scheme, host) diff --git a/libraries/cherrypy/_cptools.py b/libraries/cherrypy/_cptools.py deleted file mode 100644 index 57460285..00000000 --- a/libraries/cherrypy/_cptools.py +++ /dev/null @@ -1,509 +0,0 @@ -"""CherryPy tools. A "tool" is any helper, adapted to CP. - -Tools are usually designed to be used in a variety of ways (although some -may only offer one if they choose): - - Library calls - All tools are callables that can be used wherever needed. - The arguments are straightforward and should be detailed within the - docstring. - - Function decorators - All tools, when called, may be used as decorators which configure - individual CherryPy page handlers (methods on the CherryPy tree). - That is, "@tools.anytool()" should "turn on" the tool via the - decorated function's _cp_config attribute. - - CherryPy config - If a tool exposes a "_setup" callable, it will be called - once per Request (if the feature is "turned on" via config). - -Tools may be implemented as any object with a namespace. The builtins -are generally either modules or instances of the tools.Tool class. -""" - -import six - -import cherrypy -from cherrypy._helper import expose - -from cherrypy.lib import cptools, encoding, static, jsontools -from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc -from cherrypy.lib import caching as _caching -from cherrypy.lib import auth_basic, auth_digest - - -def _getargs(func): - """Return the names of all static arguments to the given function.""" - # Use this instead of importing inspect for less mem overhead. - import types - if six.PY3: - if isinstance(func, types.MethodType): - func = func.__func__ - co = func.__code__ - else: - if isinstance(func, types.MethodType): - func = func.im_func - co = func.func_code - return co.co_varnames[:co.co_argcount] - - -_attr_error = ( - 'CherryPy Tools cannot be turned on directly. Instead, turn them ' - 'on via config, or use them as decorators on your page handlers.' -) - - -class Tool(object): - - """A registered function for use with CherryPy request-processing hooks. - - help(tool.callable) should give you more information about this Tool. - """ - - namespace = 'tools' - - def __init__(self, point, callable, name=None, priority=50): - self._point = point - self.callable = callable - self._name = name - self._priority = priority - self.__doc__ = self.callable.__doc__ - self._setargs() - - @property - def on(self): - raise AttributeError(_attr_error) - - @on.setter - def on(self, value): - raise AttributeError(_attr_error) - - def _setargs(self): - """Copy func parameter names to obj attributes.""" - try: - for arg in _getargs(self.callable): - setattr(self, arg, None) - except (TypeError, AttributeError): - if hasattr(self.callable, '__call__'): - for arg in _getargs(self.callable.__call__): - setattr(self, arg, None) - # IronPython 1.0 raises NotImplementedError because - # inspect.getargspec tries to access Python bytecode - # in co_code attribute. - except NotImplementedError: - pass - # IronPython 1B1 may raise IndexError in some cases, - # but if we trap it here it doesn't prevent CP from working. - except IndexError: - pass - - def _merged_args(self, d=None): - """Return a dict of configuration entries for this Tool.""" - if d: - conf = d.copy() - else: - conf = {} - - tm = cherrypy.serving.request.toolmaps[self.namespace] - if self._name in tm: - conf.update(tm[self._name]) - - if 'on' in conf: - del conf['on'] - - return conf - - def __call__(self, *args, **kwargs): - """Compile-time decorator (turn on the tool in config). - - For example:: - - @expose - @tools.proxy() - def whats_my_base(self): - return cherrypy.request.base - """ - if args: - raise TypeError('The %r Tool does not accept positional ' - 'arguments; you must use keyword arguments.' - % self._name) - - def tool_decorator(f): - if not hasattr(f, '_cp_config'): - f._cp_config = {} - subspace = self.namespace + '.' + self._name + '.' - f._cp_config[subspace + 'on'] = True - for k, v in kwargs.items(): - f._cp_config[subspace + k] = v - return f - return tool_decorator - - def _setup(self): - """Hook this tool into cherrypy.request. - - The standard CherryPy request object will automatically call this - method when the tool is "turned on" in config. - """ - conf = self._merged_args() - p = conf.pop('priority', None) - if p is None: - p = getattr(self.callable, 'priority', self._priority) - cherrypy.serving.request.hooks.attach(self._point, self.callable, - priority=p, **conf) - - -class HandlerTool(Tool): - - """Tool which is called 'before main', that may skip normal handlers. - - If the tool successfully handles the request (by setting response.body), - if should return True. This will cause CherryPy to skip any 'normal' page - handler. If the tool did not handle the request, it should return False - to tell CherryPy to continue on and call the normal page handler. If the - tool is declared AS a page handler (see the 'handler' method), returning - False will raise NotFound. - """ - - def __init__(self, callable, name=None): - Tool.__init__(self, 'before_handler', callable, name) - - def handler(self, *args, **kwargs): - """Use this tool as a CherryPy page handler. - - For example:: - - class Root: - nav = tools.staticdir.handler(section="/nav", dir="nav", - root=absDir) - """ - @expose - def handle_func(*a, **kw): - handled = self.callable(*args, **self._merged_args(kwargs)) - if not handled: - raise cherrypy.NotFound() - return cherrypy.serving.response.body - return handle_func - - def _wrapper(self, **kwargs): - if self.callable(**kwargs): - cherrypy.serving.request.handler = None - - def _setup(self): - """Hook this tool into cherrypy.request. - - The standard CherryPy request object will automatically call this - method when the tool is "turned on" in config. - """ - conf = self._merged_args() - p = conf.pop('priority', None) - if p is None: - p = getattr(self.callable, 'priority', self._priority) - cherrypy.serving.request.hooks.attach(self._point, self._wrapper, - priority=p, **conf) - - -class HandlerWrapperTool(Tool): - - """Tool which wraps request.handler in a provided wrapper function. - - The 'newhandler' arg must be a handler wrapper function that takes a - 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all - page handler - functions, it must return an iterable for use as cherrypy.response.body. - - For example, to allow your 'inner' page handlers to return dicts - which then get interpolated into a template:: - - def interpolator(next_handler, *args, **kwargs): - filename = cherrypy.request.config.get('template') - cherrypy.response.template = env.get_template(filename) - response_dict = next_handler(*args, **kwargs) - return cherrypy.response.template.render(**response_dict) - cherrypy.tools.jinja = HandlerWrapperTool(interpolator) - """ - - def __init__(self, newhandler, point='before_handler', name=None, - priority=50): - self.newhandler = newhandler - self._point = point - self._name = name - self._priority = priority - - def callable(self, *args, **kwargs): - innerfunc = cherrypy.serving.request.handler - - def wrap(*args, **kwargs): - return self.newhandler(innerfunc, *args, **kwargs) - cherrypy.serving.request.handler = wrap - - -class ErrorTool(Tool): - - """Tool which is used to replace the default request.error_response.""" - - def __init__(self, callable, name=None): - Tool.__init__(self, None, callable, name) - - def _wrapper(self): - self.callable(**self._merged_args()) - - def _setup(self): - """Hook this tool into cherrypy.request. - - The standard CherryPy request object will automatically call this - method when the tool is "turned on" in config. - """ - cherrypy.serving.request.error_response = self._wrapper - - -# Builtin tools # - - -class SessionTool(Tool): - - """Session Tool for CherryPy. - - sessions.locking - When 'implicit' (the default), the session will be locked for you, - just before running the page handler. - - When 'early', the session will be locked before reading the request - body. This is off by default for safety reasons; for example, - a large upload would block the session, denying an AJAX - progress meter - (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_). - - When 'explicit' (or any other value), you need to call - cherrypy.session.acquire_lock() yourself before using - session data. - """ - - def __init__(self): - # _sessions.init must be bound after headers are read - Tool.__init__(self, 'before_request_body', _sessions.init) - - def _lock_session(self): - cherrypy.serving.session.acquire_lock() - - def _setup(self): - """Hook this tool into cherrypy.request. - - The standard CherryPy request object will automatically call this - method when the tool is "turned on" in config. - """ - hooks = cherrypy.serving.request.hooks - - conf = self._merged_args() - - p = conf.pop('priority', None) - if p is None: - p = getattr(self.callable, 'priority', self._priority) - - hooks.attach(self._point, self.callable, priority=p, **conf) - - locking = conf.pop('locking', 'implicit') - if locking == 'implicit': - hooks.attach('before_handler', self._lock_session) - elif locking == 'early': - # Lock before the request body (but after _sessions.init runs!) - hooks.attach('before_request_body', self._lock_session, - priority=60) - else: - # Don't lock - pass - - hooks.attach('before_finalize', _sessions.save) - hooks.attach('on_end_request', _sessions.close) - - def regenerate(self): - """Drop the current session and make a new one (with a new id).""" - sess = cherrypy.serving.session - sess.regenerate() - - # Grab cookie-relevant tool args - relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure' - conf = dict( - (k, v) - for k, v in self._merged_args().items() - if k in relevant - ) - _sessions.set_response_cookie(**conf) - - -class XMLRPCController(object): - - """A Controller (page handler collection) for XML-RPC. - - To use it, have your controllers subclass this base class (it will - turn on the tool for you). - - You can also supply the following optional config entries:: - - tools.xmlrpc.encoding: 'utf-8' - tools.xmlrpc.allow_none: 0 - - XML-RPC is a rather discontinuous layer over HTTP; dispatching to the - appropriate handler must first be performed according to the URL, and - then a second dispatch step must take place according to the RPC method - specified in the request body. It also allows a superfluous "/RPC2" - prefix in the URL, supplies its own handler args in the body, and - requires a 200 OK "Fault" response instead of 404 when the desired - method is not found. - - Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. - This Controller acts as the dispatch target for the first half (based - on the URL); it then reads the RPC method from the request body and - does its own second dispatch step based on that method. It also reads - body params, and returns a Fault on error. - - The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 - in your URL's, you can safely skip turning on the XMLRPCDispatcher. - Otherwise, you need to use declare it in config:: - - request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() - """ - - # Note we're hard-coding this into the 'tools' namespace. We could do - # a huge amount of work to make it relocatable, but the only reason why - # would be if someone actually disabled the default_toolbox. Meh. - _cp_config = {'tools.xmlrpc.on': True} - - @expose - def default(self, *vpath, **params): - rpcparams, rpcmethod = _xmlrpc.process_body() - - subhandler = self - for attr in str(rpcmethod).split('.'): - subhandler = getattr(subhandler, attr, None) - - if subhandler and getattr(subhandler, 'exposed', False): - body = subhandler(*(vpath + rpcparams), **params) - - else: - # https://github.com/cherrypy/cherrypy/issues/533 - # if a method is not found, an xmlrpclib.Fault should be returned - # raising an exception here will do that; see - # cherrypy.lib.xmlrpcutil.on_error - raise Exception('method "%s" is not supported' % attr) - - conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {}) - _xmlrpc.respond(body, - conf.get('encoding', 'utf-8'), - conf.get('allow_none', 0)) - return cherrypy.serving.response.body - - -class SessionAuthTool(HandlerTool): - pass - - -class CachingTool(Tool): - - """Caching Tool for CherryPy.""" - - def _wrapper(self, **kwargs): - request = cherrypy.serving.request - if _caching.get(**kwargs): - request.handler = None - else: - if request.cacheable: - # Note the devious technique here of adding hooks on the fly - request.hooks.attach('before_finalize', _caching.tee_output, - priority=100) - _wrapper.priority = 90 - - def _setup(self): - """Hook caching into cherrypy.request.""" - conf = self._merged_args() - - p = conf.pop('priority', None) - cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, - priority=p, **conf) - - -class Toolbox(object): - - """A collection of Tools. - - This object also functions as a config namespace handler for itself. - Custom toolboxes should be added to each Application's toolboxes dict. - """ - - def __init__(self, namespace): - self.namespace = namespace - - def __setattr__(self, name, value): - # If the Tool._name is None, supply it from the attribute name. - if isinstance(value, Tool): - if value._name is None: - value._name = name - value.namespace = self.namespace - object.__setattr__(self, name, value) - - def __enter__(self): - """Populate request.toolmaps from tools specified in config.""" - cherrypy.serving.request.toolmaps[self.namespace] = map = {} - - def populate(k, v): - toolname, arg = k.split('.', 1) - bucket = map.setdefault(toolname, {}) - bucket[arg] = v - return populate - - def __exit__(self, exc_type, exc_val, exc_tb): - """Run tool._setup() for each tool in our toolmap.""" - map = cherrypy.serving.request.toolmaps.get(self.namespace) - if map: - for name, settings in map.items(): - if settings.get('on', False): - tool = getattr(self, name) - tool._setup() - - def register(self, point, **kwargs): - """ - Return a decorator which registers the function - at the given hook point. - """ - def decorator(func): - attr_name = kwargs.get('name', func.__name__) - tool = Tool(point, func, **kwargs) - setattr(self, attr_name, tool) - return func - return decorator - - -default_toolbox = _d = Toolbox('tools') -_d.session_auth = SessionAuthTool(cptools.session_auth) -_d.allow = Tool('on_start_resource', cptools.allow) -_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) -_d.response_headers = Tool('on_start_resource', cptools.response_headers) -_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) -_d.log_headers = Tool('before_error_response', cptools.log_request_headers) -_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) -_d.err_redirect = ErrorTool(cptools.redirect) -_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) -_d.decode = Tool('before_request_body', encoding.decode) -# the order of encoding, gzip, caching is important -_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) -_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) -_d.staticdir = HandlerTool(static.staticdir) -_d.staticfile = HandlerTool(static.staticfile) -_d.sessions = SessionTool() -_d.xmlrpc = ErrorTool(_xmlrpc.on_error) -_d.caching = CachingTool('before_handler', _caching.get, 'caching') -_d.expires = Tool('before_finalize', _caching.expires) -_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) -_d.referer = Tool('before_request_body', cptools.referer) -_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) -_d.flatten = Tool('before_finalize', cptools.flatten) -_d.accept = Tool('on_start_resource', cptools.accept) -_d.redirect = Tool('on_start_resource', cptools.redirect) -_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) -_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) -_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) -_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) -_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) -_d.params = Tool('before_handler', cptools.convert_params, priority=15) - -del _d, cptools, encoding, static diff --git a/libraries/cherrypy/_cptree.py b/libraries/cherrypy/_cptree.py deleted file mode 100644 index ceb54379..00000000 --- a/libraries/cherrypy/_cptree.py +++ /dev/null @@ -1,313 +0,0 @@ -"""CherryPy Application and Tree objects.""" - -import os - -import six - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools -from cherrypy.lib import httputil, reprconf - - -class Application(object): - """A CherryPy Application. - - Servers and gateways should not instantiate Request objects directly. - Instead, they should ask an Application object for a request object. - - An instance of this class may also be used as a WSGI callable - (WSGI application object) for itself. - """ - - root = None - """The top-most container of page handlers for this app. Handlers should - be arranged in a hierarchy of attributes, matching the expected URI - hierarchy; the default dispatcher then searches this hierarchy for a - matching handler. When using a dispatcher other than the default, - this value may be None.""" - - config = {} - """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict - of {key: value} pairs.""" - - namespaces = reprconf.NamespaceSet() - toolboxes = {'tools': cherrypy.tools} - - log = None - """A LogManager instance. See _cplogging.""" - - wsgiapp = None - """A CPWSGIApp instance. See _cpwsgi.""" - - request_class = _cprequest.Request - response_class = _cprequest.Response - - relative_urls = False - - def __init__(self, root, script_name='', config=None): - """Initialize Application with given root.""" - self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) - self.root = root - self.script_name = script_name - self.wsgiapp = _cpwsgi.CPWSGIApp(self) - - self.namespaces = self.namespaces.copy() - self.namespaces['log'] = lambda k, v: setattr(self.log, k, v) - self.namespaces['wsgi'] = self.wsgiapp.namespace_handler - - self.config = self.__class__.config.copy() - if config: - self.merge(config) - - def __repr__(self): - """Generate a representation of the Application instance.""" - return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__, - self.root, self.script_name) - - script_name_doc = """The URI "mount point" for this app. A mount point - is that portion of the URI which is constant for all URIs that are - serviced by this application; it does not include scheme, host, or proxy - ("virtual host") portions of the URI. - - For example, if script_name is "/my/cool/app", then the URL - "http://www.example.com/my/cool/app/page1" might be handled by a - "page1" method on the root object. - - The value of script_name MUST NOT end in a slash. If the script_name - refers to the root of the URI, it MUST be an empty string (not "/"). - - If script_name is explicitly set to None, then the script_name will be - provided for each call from request.wsgi_environ['SCRIPT_NAME']. - """ - - @property - def script_name(self): # noqa: D401; irrelevant for properties - """The URI "mount point" for this app. - - A mount point is that portion of the URI which is constant for all URIs - that are serviced by this application; it does not include scheme, - host, or proxy ("virtual host") portions of the URI. - - For example, if script_name is "/my/cool/app", then the URL - "http://www.example.com/my/cool/app/page1" might be handled by a - "page1" method on the root object. - - The value of script_name MUST NOT end in a slash. If the script_name - refers to the root of the URI, it MUST be an empty string (not "/"). - - If script_name is explicitly set to None, then the script_name will be - provided for each call from request.wsgi_environ['SCRIPT_NAME']. - """ - if self._script_name is not None: - return self._script_name - - # A `_script_name` with a value of None signals that the script name - # should be pulled from WSGI environ. - return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/') - - @script_name.setter - def script_name(self, value): - if value: - value = value.rstrip('/') - self._script_name = value - - def merge(self, config): - """Merge the given config into self.config.""" - _cpconfig.merge(self.config, config) - - # Handle namespaces specified in config. - self.namespaces(self.config.get('/', {})) - - def find_config(self, path, key, default=None): - """Return the most-specific value for key along path, or default.""" - trail = path or '/' - while trail: - nodeconf = self.config.get(trail, {}) - - if key in nodeconf: - return nodeconf[key] - - lastslash = trail.rfind('/') - if lastslash == -1: - break - elif lastslash == 0 and trail != '/': - trail = '/' - else: - trail = trail[:lastslash] - - return default - - def get_serving(self, local, remote, scheme, sproto): - """Create and return a Request and Response object.""" - req = self.request_class(local, remote, scheme, sproto) - req.app = self - - for name, toolbox in self.toolboxes.items(): - req.namespaces[name] = toolbox - - resp = self.response_class() - cherrypy.serving.load(req, resp) - cherrypy.engine.publish('acquire_thread') - cherrypy.engine.publish('before_request') - - return req, resp - - def release_serving(self): - """Release the current serving (request and response).""" - req = cherrypy.serving.request - - cherrypy.engine.publish('after_request') - - try: - req.close() - except Exception: - cherrypy.log(traceback=True, severity=40) - - cherrypy.serving.clear() - - def __call__(self, environ, start_response): - """Call a WSGI-callable.""" - return self.wsgiapp(environ, start_response) - - -class Tree(object): - """A registry of CherryPy applications, mounted at diverse points. - - An instance of this class may also be used as a WSGI callable - (WSGI application object), in which case it dispatches to all - mounted apps. - """ - - apps = {} - """ - A dict of the form {script name: application}, where "script name" - is a string declaring the URI mount point (no trailing slash), and - "application" is an instance of cherrypy.Application (or an arbitrary - WSGI callable if you happen to be using a WSGI server).""" - - def __init__(self): - """Initialize registry Tree.""" - self.apps = {} - - def mount(self, root, script_name='', config=None): - """Mount a new app from a root object, script_name, and config. - - root - An instance of a "controller class" (a collection of page - handler methods) which represents the root of the application. - This may also be an Application instance, or None if using - a dispatcher other than the default. - - script_name - A string containing the "mount point" of the application. - This should start with a slash, and be the path portion of the - URL at which to mount the given root. For example, if root.index() - will handle requests to "http://www.example.com:8080/dept/app1/", - then the script_name argument would be "/dept/app1". - - It MUST NOT end in a slash. If the script_name refers to the - root of the URI, it MUST be an empty string (not "/"). - - config - A file or dict containing application config. - """ - if script_name is None: - raise TypeError( - "The 'script_name' argument may not be None. Application " - 'objects may, however, possess a script_name of None (in ' - 'order to inpect the WSGI environ for SCRIPT_NAME upon each ' - 'request). You cannot mount such Applications on this Tree; ' - 'you must pass them to a WSGI server interface directly.') - - # Next line both 1) strips trailing slash and 2) maps "/" -> "". - script_name = script_name.rstrip('/') - - if isinstance(root, Application): - app = root - if script_name != '' and script_name != app.script_name: - raise ValueError( - 'Cannot specify a different script name and pass an ' - 'Application instance to cherrypy.mount') - script_name = app.script_name - else: - app = Application(root, script_name) - - # If mounted at "", add favicon.ico - needs_favicon = ( - script_name == '' - and root is not None - and not hasattr(root, 'favicon_ico') - ) - if needs_favicon: - favicon = os.path.join( - os.getcwd(), - os.path.dirname(__file__), - 'favicon.ico', - ) - root.favicon_ico = tools.staticfile.handler(favicon) - - if config: - app.merge(config) - - self.apps[script_name] = app - - return app - - def graft(self, wsgi_callable, script_name=''): - """Mount a wsgi callable at the given script_name.""" - # Next line both 1) strips trailing slash and 2) maps "/" -> "". - script_name = script_name.rstrip('/') - self.apps[script_name] = wsgi_callable - - def script_name(self, path=None): - """Return the script_name of the app at the given path, or None. - - If path is None, cherrypy.request is used. - """ - if path is None: - try: - request = cherrypy.serving.request - path = httputil.urljoin(request.script_name, - request.path_info) - except AttributeError: - return None - - while True: - if path in self.apps: - return path - - if path == '': - return None - - # Move one node up the tree and try again. - path = path[:path.rfind('/')] - - def __call__(self, environ, start_response): - """Pre-initialize WSGI env and call WSGI-callable.""" - # If you're calling this, then you're probably setting SCRIPT_NAME - # to '' (some WSGI servers always set SCRIPT_NAME to ''). - # Try to look up the app using the full path. - env1x = environ - if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) - path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), - env1x.get('PATH_INFO', '')) - sn = self.script_name(path or '/') - if sn is None: - start_response('404 Not Found', []) - return [] - - app = self.apps[sn] - - # Correct the SCRIPT_NAME and PATH_INFO environ entries. - environ = environ.copy() - if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - # Python 2/WSGI u.0: all strings MUST be of type unicode - enc = environ[ntou('wsgi.url_encoding')] - environ[ntou('SCRIPT_NAME')] = sn.decode(enc) - environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc) - else: - environ['SCRIPT_NAME'] = sn - environ['PATH_INFO'] = path[len(sn.rstrip('/')):] - return app(environ, start_response) diff --git a/libraries/cherrypy/_cpwsgi.py b/libraries/cherrypy/_cpwsgi.py deleted file mode 100644 index 0b4942ff..00000000 --- a/libraries/cherrypy/_cpwsgi.py +++ /dev/null @@ -1,467 +0,0 @@ -"""WSGI interface (see PEP 333 and 3333). - -Note that WSGI environ keys and values are 'native strings'; that is, -whatever the type of "" is. For Python 2, that's a byte string; for Python 3, -it's a unicode string. But PEP 3333 says: "even if Python's str type is -actually Unicode "under the hood", the content of native strings must -still be translatable to bytes via the Latin-1 encoding!" -""" - -import sys as _sys -import io - -import six - -import cherrypy as _cherrypy -from cherrypy._cpcompat import ntou -from cherrypy import _cperror -from cherrypy.lib import httputil -from cherrypy.lib import is_closable_iterator - - -def downgrade_wsgi_ux_to_1x(environ): - """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. - """ - env1x = {} - - url_encoding = environ[ntou('wsgi.url_encoding')] - for k, v in list(environ.items()): - if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: - v = v.encode(url_encoding) - elif isinstance(v, six.text_type): - v = v.encode('ISO-8859-1') - env1x[k.encode('ISO-8859-1')] = v - - return env1x - - -class VirtualHost(object): - - """Select a different WSGI application based on the Host header. - - This can be useful when running multiple sites within one CP server. - It allows several domains to point to different applications. For example:: - - root = Root() - RootApp = cherrypy.Application(root) - Domain2App = cherrypy.Application(root) - SecureApp = cherrypy.Application(Secure()) - - vhost = cherrypy._cpwsgi.VirtualHost( - RootApp, - domains={ - 'www.domain2.example': Domain2App, - 'www.domain2.example:443': SecureApp, - }, - ) - - cherrypy.tree.graft(vhost) - """ - default = None - """Required. The default WSGI application.""" - - use_x_forwarded_host = True - """If True (the default), any "X-Forwarded-Host" - request header will be used instead of the "Host" header. This - is commonly added by HTTP servers (such as Apache) when proxying.""" - - domains = {} - """A dict of {host header value: application} pairs. - The incoming "Host" request header is looked up in this dict, - and, if a match is found, the corresponding WSGI application - will be called instead of the default. Note that you often need - separate entries for "example.com" and "www.example.com". - In addition, "Host" headers may contain the port number. - """ - - def __init__(self, default, domains=None, use_x_forwarded_host=True): - self.default = default - self.domains = domains or {} - self.use_x_forwarded_host = use_x_forwarded_host - - def __call__(self, environ, start_response): - domain = environ.get('HTTP_HOST', '') - if self.use_x_forwarded_host: - domain = environ.get('HTTP_X_FORWARDED_HOST', domain) - - nextapp = self.domains.get(domain) - if nextapp is None: - nextapp = self.default - return nextapp(environ, start_response) - - -class InternalRedirector(object): - - """WSGI middleware that handles raised cherrypy.InternalRedirect.""" - - def __init__(self, nextapp, recursive=False): - self.nextapp = nextapp - self.recursive = recursive - - def __call__(self, environ, start_response): - redirections = [] - while True: - environ = environ.copy() - try: - return self.nextapp(environ, start_response) - except _cherrypy.InternalRedirect: - ir = _sys.exc_info()[1] - sn = environ.get('SCRIPT_NAME', '') - path = environ.get('PATH_INFO', '') - qs = environ.get('QUERY_STRING', '') - - # Add the *previous* path_info + qs to redirections. - old_uri = sn + path - if qs: - old_uri += '?' + qs - redirections.append(old_uri) - - if not self.recursive: - # Check to see if the new URI has been redirected to - # already - new_uri = sn + ir.path - if ir.query_string: - new_uri += '?' + ir.query_string - if new_uri in redirections: - ir.request.close() - tmpl = ( - 'InternalRedirector visited the same URL twice: %r' - ) - raise RuntimeError(tmpl % new_uri) - - # Munge the environment and try again. - environ['REQUEST_METHOD'] = 'GET' - environ['PATH_INFO'] = ir.path - environ['QUERY_STRING'] = ir.query_string - environ['wsgi.input'] = io.BytesIO() - environ['CONTENT_LENGTH'] = '0' - environ['cherrypy.previous_request'] = ir.request - - -class ExceptionTrapper(object): - - """WSGI middleware that traps exceptions.""" - - def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): - self.nextapp = nextapp - self.throws = throws - - def __call__(self, environ, start_response): - return _TrappedResponse( - self.nextapp, - environ, - start_response, - self.throws - ) - - -class _TrappedResponse(object): - - response = iter([]) - - def __init__(self, nextapp, environ, start_response, throws): - self.nextapp = nextapp - self.environ = environ - self.start_response = start_response - self.throws = throws - self.started_response = False - self.response = self.trap( - self.nextapp, self.environ, self.start_response, - ) - self.iter_response = iter(self.response) - - def __iter__(self): - self.started_response = True - return self - - def __next__(self): - return self.trap(next, self.iter_response) - - # todo: https://pythonhosted.org/six/#six.Iterator - if six.PY2: - next = __next__ - - def close(self): - if hasattr(self.response, 'close'): - self.response.close() - - def trap(self, func, *args, **kwargs): - try: - return func(*args, **kwargs) - except self.throws: - raise - except StopIteration: - raise - except Exception: - tb = _cperror.format_exc() - _cherrypy.log(tb, severity=40) - if not _cherrypy.request.show_tracebacks: - tb = '' - s, h, b = _cperror.bare_error(tb) - if six.PY3: - # What fun. - s = s.decode('ISO-8859-1') - h = [ - (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) - for k, v in h - ] - if self.started_response: - # Empty our iterable (so future calls raise StopIteration) - self.iter_response = iter([]) - else: - self.iter_response = iter(b) - - try: - self.start_response(s, h, _sys.exc_info()) - except Exception: - # "The application must not trap any exceptions raised by - # start_response, if it called start_response with exc_info. - # Instead, it should allow such exceptions to propagate - # back to the server or gateway." - # But we still log and call close() to clean up ourselves. - _cherrypy.log(traceback=True, severity=40) - raise - - if self.started_response: - return b''.join(b) - else: - return b - - -# WSGI-to-CP Adapter # - - -class AppResponse(object): - - """WSGI response iterable for CherryPy applications.""" - - def __init__(self, environ, start_response, cpapp): - self.cpapp = cpapp - try: - if six.PY2: - if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - environ = downgrade_wsgi_ux_to_1x(environ) - self.environ = environ - self.run() - - r = _cherrypy.serving.response - - outstatus = r.output_status - if not isinstance(outstatus, bytes): - raise TypeError('response.output_status is not a byte string.') - - outheaders = [] - for k, v in r.header_list: - if not isinstance(k, bytes): - tmpl = 'response.header_list key %r is not a byte string.' - raise TypeError(tmpl % k) - if not isinstance(v, bytes): - tmpl = ( - 'response.header_list value %r is not a byte string.' - ) - raise TypeError(tmpl % v) - outheaders.append((k, v)) - - if six.PY3: - # According to PEP 3333, when using Python 3, the response - # status and headers must be bytes masquerading as unicode; - # that is, they must be of type "str" but are restricted to - # code points in the "latin-1" set. - outstatus = outstatus.decode('ISO-8859-1') - outheaders = [ - (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) - for k, v in outheaders - ] - - self.iter_response = iter(r.body) - self.write = start_response(outstatus, outheaders) - except BaseException: - self.close() - raise - - def __iter__(self): - return self - - def __next__(self): - return next(self.iter_response) - - # todo: https://pythonhosted.org/six/#six.Iterator - if six.PY2: - next = __next__ - - def close(self): - """Close and de-reference the current request and response. (Core)""" - streaming = _cherrypy.serving.response.stream - self.cpapp.release_serving() - - # We avoid the expense of examining the iterator to see if it's - # closable unless we are streaming the response, as that's the - # only situation where we are going to have an iterator which - # may not have been exhausted yet. - if streaming and is_closable_iterator(self.iter_response): - iter_close = self.iter_response.close - try: - iter_close() - except Exception: - _cherrypy.log(traceback=True, severity=40) - - def run(self): - """Create a Request object using environ.""" - env = self.environ.get - - local = httputil.Host( - '', - int(env('SERVER_PORT', 80) or -1), - env('SERVER_NAME', ''), - ) - remote = httputil.Host( - env('REMOTE_ADDR', ''), - int(env('REMOTE_PORT', -1) or -1), - env('REMOTE_HOST', ''), - ) - scheme = env('wsgi.url_scheme') - sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1') - request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) - - # LOGON_USER is served by IIS, and is the name of the - # user after having been mapped to a local account. - # Both IIS and Apache set REMOTE_USER, when possible. - request.login = env('LOGON_USER') or env('REMOTE_USER') or None - request.multithread = self.environ['wsgi.multithread'] - request.multiprocess = self.environ['wsgi.multiprocess'] - request.wsgi_environ = self.environ - request.prev = env('cherrypy.previous_request', None) - - meth = self.environ['REQUEST_METHOD'] - - path = httputil.urljoin( - self.environ.get('SCRIPT_NAME', ''), - self.environ.get('PATH_INFO', ''), - ) - qs = self.environ.get('QUERY_STRING', '') - - path, qs = self.recode_path_qs(path, qs) or (path, qs) - - rproto = self.environ.get('SERVER_PROTOCOL') - headers = self.translate_headers(self.environ) - rfile = self.environ['wsgi.input'] - request.run(meth, path, qs, rproto, headers, rfile) - - headerNames = { - 'HTTP_CGI_AUTHORIZATION': 'Authorization', - 'CONTENT_LENGTH': 'Content-Length', - 'CONTENT_TYPE': 'Content-Type', - 'REMOTE_HOST': 'Remote-Host', - 'REMOTE_ADDR': 'Remote-Addr', - } - - def recode_path_qs(self, path, qs): - if not six.PY3: - return - - # This isn't perfect; if the given PATH_INFO is in the - # wrong encoding, it may fail to match the appropriate config - # section URI. But meh. - old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') - new_enc = self.cpapp.find_config( - self.environ.get('PATH_INFO', ''), - 'request.uri_encoding', 'utf-8', - ) - if new_enc.lower() == old_enc.lower(): - return - - # Even though the path and qs are unicode, the WSGI server - # is required by PEP 3333 to coerce them to ISO-8859-1 - # masquerading as unicode. So we have to encode back to - # bytes and then decode again using the "correct" encoding. - try: - return ( - path.encode(old_enc).decode(new_enc), - qs.encode(old_enc).decode(new_enc), - ) - except (UnicodeEncodeError, UnicodeDecodeError): - # Just pass them through without transcoding and hope. - pass - - def translate_headers(self, environ): - """Translate CGI-environ header names to HTTP header names.""" - for cgiName in environ: - # We assume all incoming header keys are uppercase already. - if cgiName in self.headerNames: - yield self.headerNames[cgiName], environ[cgiName] - elif cgiName[:5] == 'HTTP_': - # Hackish attempt at recovering original header names. - translatedHeader = cgiName[5:].replace('_', '-') - yield translatedHeader, environ[cgiName] - - -class CPWSGIApp(object): - - """A WSGI application object for a CherryPy Application.""" - - pipeline = [ - ('ExceptionTrapper', ExceptionTrapper), - ('InternalRedirector', InternalRedirector), - ] - """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a - constructor that takes an initial, positional 'nextapp' argument, - plus optional keyword arguments, and returns a WSGI application - (that takes environ and start_response arguments). The 'name' can - be any you choose, and will correspond to keys in self.config.""" - - head = None - """Rather than nest all apps in the pipeline on each call, it's only - done the first time, and the result is memoized into self.head. Set - this to None again if you change self.pipeline after calling self.""" - - config = {} - """A dict whose keys match names listed in the pipeline. Each - value is a further dict which will be passed to the corresponding - named WSGI callable (from the pipeline) as keyword arguments.""" - - response_class = AppResponse - """The class to instantiate and return as the next app in the WSGI chain. - """ - - def __init__(self, cpapp, pipeline=None): - self.cpapp = cpapp - self.pipeline = self.pipeline[:] - if pipeline: - self.pipeline.extend(pipeline) - self.config = self.config.copy() - - def tail(self, environ, start_response): - """WSGI application callable for the actual CherryPy application. - - You probably shouldn't call this; call self.__call__ instead, - so that any WSGI middleware in self.pipeline can run first. - """ - return self.response_class(environ, start_response, self.cpapp) - - def __call__(self, environ, start_response): - head = self.head - if head is None: - # Create and nest the WSGI apps in our pipeline (in reverse order). - # Then memoize the result in self.head. - head = self.tail - for name, callable in self.pipeline[::-1]: - conf = self.config.get(name, {}) - head = callable(head, **conf) - self.head = head - return head(environ, start_response) - - def namespace_handler(self, k, v): - """Config handler for the 'wsgi' namespace.""" - if k == 'pipeline': - # Note this allows multiple 'wsgi.pipeline' config entries - # (but each entry will be processed in a 'random' order). - # It should also allow developers to set default middleware - # in code (passed to self.__init__) that deployers can add to - # (but not remove) via config. - self.pipeline.extend(v) - elif k == 'response_class': - self.response_class = v - else: - name, arg = k.split('.', 1) - bucket = self.config.setdefault(name, {}) - bucket[arg] = v diff --git a/libraries/cherrypy/_cpwsgi_server.py b/libraries/cherrypy/_cpwsgi_server.py deleted file mode 100644 index 11dd846a..00000000 --- a/libraries/cherrypy/_cpwsgi_server.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -WSGI server interface (see PEP 333). - -This adds some CP-specific bits to the framework-agnostic cheroot package. -""" -import sys - -import cheroot.wsgi -import cheroot.server - -import cherrypy - - -class CPWSGIHTTPRequest(cheroot.server.HTTPRequest): - """Wrapper for cheroot.server.HTTPRequest. - - This is a layer, which preserves URI parsing mode like it which was - before Cheroot v5.8.0. - """ - - def __init__(self, server, conn): - """Initialize HTTP request container instance. - - Args: - server (cheroot.server.HTTPServer): - web server object receiving this request - conn (cheroot.server.HTTPConnection): - HTTP connection object for this request - """ - super(CPWSGIHTTPRequest, self).__init__( - server, conn, proxy_mode=True - ) - - -class CPWSGIServer(cheroot.wsgi.Server): - """Wrapper for cheroot.wsgi.Server. - - cheroot has been designed to not reference CherryPy in any way, - so that it can be used in other frameworks and applications. Therefore, - we wrap it here, so we can set our own mount points from cherrypy.tree - and apply some attributes from config -> cherrypy.server -> wsgi.Server. - """ - - fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' - version = fmt.format(**globals()) - - def __init__(self, server_adapter=cherrypy.server): - """Initialize CPWSGIServer instance. - - Args: - server_adapter (cherrypy._cpserver.Server): ... - """ - self.server_adapter = server_adapter - self.max_request_header_size = ( - self.server_adapter.max_request_header_size or 0 - ) - self.max_request_body_size = ( - self.server_adapter.max_request_body_size or 0 - ) - - server_name = (self.server_adapter.socket_host or - self.server_adapter.socket_file or - None) - - self.wsgi_version = self.server_adapter.wsgi_version - - super(CPWSGIServer, self).__init__( - server_adapter.bind_addr, cherrypy.tree, - self.server_adapter.thread_pool, - server_name, - max=self.server_adapter.thread_pool_max, - request_queue_size=self.server_adapter.socket_queue_size, - timeout=self.server_adapter.socket_timeout, - shutdown_timeout=self.server_adapter.shutdown_timeout, - accepted_queue_size=self.server_adapter.accepted_queue_size, - accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, - peercreds_enabled=self.server_adapter.peercreds, - peercreds_resolve_enabled=self.server_adapter.peercreds_resolve, - ) - self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest - - self.protocol = self.server_adapter.protocol_version - self.nodelay = self.server_adapter.nodelay - - if sys.version_info >= (3, 0): - ssl_module = self.server_adapter.ssl_module or 'builtin' - else: - ssl_module = self.server_adapter.ssl_module or 'pyopenssl' - if self.server_adapter.ssl_context: - adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) - self.ssl_adapter = adapter_class( - self.server_adapter.ssl_certificate, - self.server_adapter.ssl_private_key, - self.server_adapter.ssl_certificate_chain, - self.server_adapter.ssl_ciphers) - self.ssl_adapter.context = self.server_adapter.ssl_context - elif self.server_adapter.ssl_certificate: - adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) - self.ssl_adapter = adapter_class( - self.server_adapter.ssl_certificate, - self.server_adapter.ssl_private_key, - self.server_adapter.ssl_certificate_chain, - self.server_adapter.ssl_ciphers) - - self.stats['Enabled'] = getattr( - self.server_adapter, 'statistics', False) - - def error_log(self, msg='', level=20, traceback=False): - """Write given message to the error log.""" - cherrypy.engine.log(msg, level, traceback) diff --git a/libraries/cherrypy/_helper.py b/libraries/cherrypy/_helper.py deleted file mode 100644 index 314550cb..00000000 --- a/libraries/cherrypy/_helper.py +++ /dev/null @@ -1,344 +0,0 @@ -"""Helper functions for CP apps.""" - -import six -from six.moves import urllib - -from cherrypy._cpcompat import text_or_bytes - -import cherrypy - - -def expose(func=None, alias=None): - """Expose the function or class. - - Optionally provide an alias or set of aliases. - """ - def expose_(func): - func.exposed = True - if alias is not None: - if isinstance(alias, text_or_bytes): - parents[alias.replace('.', '_')] = func - else: - for a in alias: - parents[a.replace('.', '_')] = func - return func - - import sys - import types - decoratable_types = types.FunctionType, types.MethodType, type, - if six.PY2: - # Old-style classes are type types.ClassType. - decoratable_types += types.ClassType, - if isinstance(func, decoratable_types): - if alias is None: - # @expose - func.exposed = True - return func - else: - # func = expose(func, alias) - parents = sys._getframe(1).f_locals - return expose_(func) - elif func is None: - if alias is None: - # @expose() - parents = sys._getframe(1).f_locals - return expose_ - else: - # @expose(alias="alias") or - # @expose(alias=["alias1", "alias2"]) - parents = sys._getframe(1).f_locals - return expose_ - else: - # @expose("alias") or - # @expose(["alias1", "alias2"]) - parents = sys._getframe(1).f_locals - alias = func - return expose_ - - -def popargs(*args, **kwargs): - """Decorate _cp_dispatch. - - (cherrypy.dispatch.Dispatcher.dispatch_method_name) - - Optional keyword argument: handler=(Object or Function) - - Provides a _cp_dispatch function that pops off path segments into - cherrypy.request.params under the names specified. The dispatch - is then forwarded on to the next vpath element. - - Note that any existing (and exposed) member function of the class that - popargs is applied to will override that value of the argument. For - instance, if you have a method named "list" on the class decorated with - popargs, then accessing "/list" will call that function instead of popping - it off as the requested parameter. This restriction applies to all - _cp_dispatch functions. The only way around this restriction is to create - a "blank class" whose only function is to provide _cp_dispatch. - - If there are path elements after the arguments, or more arguments - are requested than are available in the vpath, then the 'handler' - keyword argument specifies the next object to handle the parameterized - request. If handler is not specified or is None, then self is used. - If handler is a function rather than an instance, then that function - will be called with the args specified and the return value from that - function used as the next object INSTEAD of adding the parameters to - cherrypy.request.args. - - This decorator may be used in one of two ways: - - As a class decorator: - @cherrypy.popargs('year', 'month', 'day') - class Blog: - def index(self, year=None, month=None, day=None): - #Process the parameters here; any url like - #/, /2009, /2009/12, or /2009/12/31 - #will fill in the appropriate parameters. - - def create(self): - #This link will still be available at /create. Defined functions - #take precedence over arguments. - - Or as a member of a class: - class Blog: - _cp_dispatch = cherrypy.popargs('year', 'month', 'day') - #... - - The handler argument may be used to mix arguments with built in functions. - For instance, the following setup allows different activities at the - day, month, and year level: - - class DayHandler: - def index(self, year, month, day): - #Do something with this day; probably list entries - - def delete(self, year, month, day): - #Delete all entries for this day - - @cherrypy.popargs('day', handler=DayHandler()) - class MonthHandler: - def index(self, year, month): - #Do something with this month; probably list entries - - def delete(self, year, month): - #Delete all entries for this month - - @cherrypy.popargs('month', handler=MonthHandler()) - class YearHandler: - def index(self, year): - #Do something with this year - - #... - - @cherrypy.popargs('year', handler=YearHandler()) - class Root: - def index(self): - #... - - """ - # Since keyword arg comes after *args, we have to process it ourselves - # for lower versions of python. - - handler = None - handler_call = False - for k, v in kwargs.items(): - if k == 'handler': - handler = v - else: - tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'" - raise TypeError(tm.format(k)) - - import inspect - - if handler is not None \ - and (hasattr(handler, '__call__') or inspect.isclass(handler)): - handler_call = True - - def decorated(cls_or_self=None, vpath=None): - if inspect.isclass(cls_or_self): - # cherrypy.popargs is a class decorator - cls = cls_or_self - name = cherrypy.dispatch.Dispatcher.dispatch_method_name - setattr(cls, name, decorated) - return cls - - # We're in the actual function - self = cls_or_self - parms = {} - for arg in args: - if not vpath: - break - parms[arg] = vpath.pop(0) - - if handler is not None: - if handler_call: - return handler(**parms) - else: - cherrypy.request.params.update(parms) - return handler - - cherrypy.request.params.update(parms) - - # If we are the ultimate handler, then to prevent our _cp_dispatch - # from being called again, we will resolve remaining elements through - # getattr() directly. - if vpath: - return getattr(self, vpath.pop(0), None) - else: - return self - - return decorated - - -def url(path='', qs='', script_name=None, base=None, relative=None): - """Create an absolute URL for the given path. - - If 'path' starts with a slash ('/'), this will return - (base + script_name + path + qs). - If it does not start with a slash, this returns - (base + script_name [+ request.path_info] + path + qs). - - If script_name is None, cherrypy.request will be used - to find a script_name, if available. - - If base is None, cherrypy.request.base will be used (if available). - Note that you can use cherrypy.tools.proxy to change this. - - Finally, note that this function can be used to obtain an absolute URL - for the current request path (minus the querystring) by passing no args. - If you call url(qs=cherrypy.request.query_string), you should get the - original browser URL (assuming no internal redirections). - - If relative is None or not provided, request.app.relative_urls will - be used (if available, else False). If False, the output will be an - absolute URL (including the scheme, host, vhost, and script_name). - If True, the output will instead be a URL that is relative to the - current request path, perhaps including '..' atoms. If relative is - the string 'server', the output will instead be a URL that is - relative to the server root; i.e., it will start with a slash. - """ - if isinstance(qs, (tuple, list, dict)): - qs = urllib.parse.urlencode(qs) - if qs: - qs = '?' + qs - - if cherrypy.request.app: - if not path.startswith('/'): - # Append/remove trailing slash from path_info as needed - # (this is to support mistyped URL's without redirecting; - # if you want to redirect, use tools.trailing_slash). - pi = cherrypy.request.path_info - if cherrypy.request.is_index is True: - if not pi.endswith('/'): - pi = pi + '/' - elif cherrypy.request.is_index is False: - if pi.endswith('/') and pi != '/': - pi = pi[:-1] - - if path == '': - path = pi - else: - path = urllib.parse.urljoin(pi, path) - - if script_name is None: - script_name = cherrypy.request.script_name - if base is None: - base = cherrypy.request.base - - newurl = base + script_name + normalize_path(path) + qs - else: - # No request.app (we're being called outside a request). - # We'll have to guess the base from server.* attributes. - # This will produce very different results from the above - # if you're using vhosts or tools.proxy. - if base is None: - base = cherrypy.server.base() - - path = (script_name or '') + path - newurl = base + normalize_path(path) + qs - - # At this point, we should have a fully-qualified absolute URL. - - if relative is None: - relative = getattr(cherrypy.request.app, 'relative_urls', False) - - # See http://www.ietf.org/rfc/rfc2396.txt - if relative == 'server': - # "A relative reference beginning with a single slash character is - # termed an absolute-path reference, as defined by <abs_path>..." - # This is also sometimes called "server-relative". - newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) - elif relative: - # "A relative reference that does not begin with a scheme name - # or a slash character is termed a relative-path reference." - old = url(relative=False).split('/')[:-1] - new = newurl.split('/') - while old and new: - a, b = old[0], new[0] - if a != b: - break - old.pop(0) - new.pop(0) - new = (['..'] * len(old)) + new - newurl = '/'.join(new) - - return newurl - - -def normalize_path(path): - """Resolve given path from relative into absolute form.""" - if './' not in path: - return path - - # Normalize the URL by removing ./ and ../ - atoms = [] - for atom in path.split('/'): - if atom == '.': - pass - elif atom == '..': - # Don't pop from empty list - # (i.e. ignore redundant '..') - if atoms: - atoms.pop() - elif atom: - atoms.append(atom) - - newpath = '/'.join(atoms) - # Preserve leading '/' - if path.startswith('/'): - newpath = '/' + newpath - - return newpath - - -#### -# Inlined from jaraco.classes 1.4.3 -# Ref #1673 -class _ClassPropertyDescriptor(object): - """Descript for read-only class-based property. - - Turns a classmethod-decorated func into a read-only property of that class - type (means the value cannot be set). - """ - - def __init__(self, fget, fset=None): - """Initialize a class property descriptor. - - Instantiated by ``_helper.classproperty``. - """ - self.fget = fget - self.fset = fset - - def __get__(self, obj, klass=None): - """Return property value.""" - if klass is None: - klass = type(obj) - return self.fget.__get__(obj, klass)() - - -def classproperty(func): # noqa: D401; irrelevant for properties - """Decorator like classmethod to implement a static class property.""" - if not isinstance(func, (classmethod, staticmethod)): - func = classmethod(func) - - return _ClassPropertyDescriptor(func) -#### diff --git a/libraries/cherrypy/daemon.py b/libraries/cherrypy/daemon.py deleted file mode 100644 index 74488c06..00000000 --- a/libraries/cherrypy/daemon.py +++ /dev/null @@ -1,107 +0,0 @@ -"""The CherryPy daemon.""" - -import sys - -import cherrypy -from cherrypy.process import plugins, servers -from cherrypy import Application - - -def start(configfiles=None, daemonize=False, environment=None, - fastcgi=False, scgi=False, pidfile=None, imports=None, - cgi=False): - """Subscribe all engine plugins and start the engine.""" - sys.path = [''] + sys.path - for i in imports or []: - exec('import %s' % i) - - for c in configfiles or []: - cherrypy.config.update(c) - # If there's only one app mounted, merge config into it. - if len(cherrypy.tree.apps) == 1: - for app in cherrypy.tree.apps.values(): - if isinstance(app, Application): - app.merge(c) - - engine = cherrypy.engine - - if environment is not None: - cherrypy.config.update({'environment': environment}) - - # Only daemonize if asked to. - if daemonize: - # Don't print anything to stdout/sterr. - cherrypy.config.update({'log.screen': False}) - plugins.Daemonizer(engine).subscribe() - - if pidfile: - plugins.PIDFile(engine, pidfile).subscribe() - - if hasattr(engine, 'signal_handler'): - engine.signal_handler.subscribe() - if hasattr(engine, 'console_control_handler'): - engine.console_control_handler.subscribe() - - if (fastcgi and (scgi or cgi)) or (scgi and cgi): - cherrypy.log.error('You may only specify one of the cgi, fastcgi, and ' - 'scgi options.', 'ENGINE') - sys.exit(1) - elif fastcgi or scgi or cgi: - # Turn off autoreload when using *cgi. - cherrypy.config.update({'engine.autoreload.on': False}) - # Turn off the default HTTP server (which is subscribed by default). - cherrypy.server.unsubscribe() - - addr = cherrypy.server.bind_addr - cls = ( - servers.FlupFCGIServer if fastcgi else - servers.FlupSCGIServer if scgi else - servers.FlupCGIServer - ) - f = cls(application=cherrypy.tree, bindAddress=addr) - s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) - s.subscribe() - - # Always start the engine; this will start all other services - try: - engine.start() - except Exception: - # Assume the error has been logged already via bus.log. - sys.exit(1) - else: - engine.block() - - -def run(): - """Run cherryd CLI.""" - from optparse import OptionParser - - p = OptionParser() - p.add_option('-c', '--config', action='append', dest='config', - help='specify config file(s)') - p.add_option('-d', action='store_true', dest='daemonize', - help='run the server as a daemon') - p.add_option('-e', '--environment', dest='environment', default=None, - help='apply the given config environment') - p.add_option('-f', action='store_true', dest='fastcgi', - help='start a fastcgi server instead of the default HTTP ' - 'server') - p.add_option('-s', action='store_true', dest='scgi', - help='start a scgi server instead of the default HTTP server') - p.add_option('-x', action='store_true', dest='cgi', - help='start a cgi server instead of the default HTTP server') - p.add_option('-i', '--import', action='append', dest='imports', - help='specify modules to import') - p.add_option('-p', '--pidfile', dest='pidfile', default=None, - help='store the process id in the given file') - p.add_option('-P', '--Path', action='append', dest='Path', - help='add the given paths to sys.path') - options, args = p.parse_args() - - if options.Path: - for p in options.Path: - sys.path.insert(0, p) - - start(options.config, options.daemonize, - options.environment, options.fastcgi, options.scgi, - options.pidfile, options.imports, options.cgi) diff --git a/libraries/cherrypy/favicon.ico b/libraries/cherrypy/favicon.ico deleted file mode 100644 index f0d7e61badad3f332cf1e663efb97c0b5be80f5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1406 zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K zC3>Y)vZNK!rhR_zJd<bU|M}kMoO{0Cd!GB;KkoU0NLVT=2)RAxa?lm%Cxjr;TL_un z3VorFg&_WomX;PQBmFpaDwn?M(PWhuk)2hAmSzH76vOeACShffNm)rfo~iK|`@7<l z97Rw-F{L$?MC3~`w{s_5nul444-L&tm<D=KQCCfMeH|u7X&CxBQCVF}Ku!kswmEpk zq+ns~j;2N&v4wf)=_FFu*od~b4Gs0p1m~oYTv$M3V++>tfiw$m?BBmX0|pFW;J|@s zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO` z$*x_y*u8r<d-m*M@7}%a+qVyEYisuJ-_L;q2e7fR!PeFmJ3BiL9z4jQLx-@px99NT z!yGws1P2EPjvhVAv17+Le*8F&j*gr-aRMhNCr+L`Dg2pJoIZV;GiT1=?Cgw-iwmx< zuDH3m;qLCv*|TTy@bJLX(-SW*FV3Ai$NBT;xp3hE-rn9^ym*mImoDMs<HP04m$`D~ z3ckL+T)ldgYuB!E{rYwM{QS6a;|4cx-sIM;Tim{V8-IU)?%cUUKtKS2fq~q;dzYY~ zAnx6}M{sa3At52$zki?5&``p{!Uzu!Cn6$($jC^dqN0e7jwU81hS=Cx;^N|nkB=uI zA%O=E9`NwtLmoYP#N)@0NlZ*6DJh93Po9vRoJ>ke3QwOtB{embw6rwR)6;qO>=_vu z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQ<p`f6E zw{PF_?%g}yzkkn%4<9HjEac<Ij}#RZQCwV1Nl6K%rKOaWl~G<^&ZkeG`26`ZU%q^y zqN0MYU%&G0+czpJE2*lgqPn`8nwlDFYis%b{X2Dab=23_)6mc$%v2*yO-(d6HzS*U z0dgG$&-ej}LNBpc{SO)MBTEYMVzF4hjZi6d5R#;wK*i!-5`O<pW+8GyxA@oY?c};D z_D~ayM<VcE>T(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz? zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg Kbp8+jKgeIP@z;_7 diff --git a/libraries/cherrypy/lib/__init__.py b/libraries/cherrypy/lib/__init__.py deleted file mode 100644 index f815f76a..00000000 --- a/libraries/cherrypy/lib/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -"""CherryPy Library.""" - - -def is_iterator(obj): - """Detect if the object provided implements the iterator protocol. - - (i.e. like a generator). - - This will return False for objects which are iterable, - but not iterators themselves. - """ - from types import GeneratorType - if isinstance(obj, GeneratorType): - return True - elif not hasattr(obj, '__iter__'): - return False - else: - # Types which implement the protocol must return themselves when - # invoking 'iter' upon them. - return iter(obj) is obj - - -def is_closable_iterator(obj): - """Detect if the given object is both closable and iterator.""" - # Not an iterator. - if not is_iterator(obj): - return False - - # A generator - the easiest thing to deal with. - import inspect - if inspect.isgenerator(obj): - return True - - # A custom iterator. Look for a close method... - if not (hasattr(obj, 'close') and callable(obj.close)): - return False - - # ... which doesn't require any arguments. - try: - inspect.getcallargs(obj.close) - except TypeError: - return False - else: - return True - - -class file_generator(object): - """Yield the given input (a file object) in chunks (default 64k). - - (Core) - """ - - def __init__(self, input, chunkSize=65536): - """Initialize file_generator with file ``input`` for chunked access.""" - self.input = input - self.chunkSize = chunkSize - - def __iter__(self): - """Return iterator.""" - return self - - def __next__(self): - """Return next chunk of file.""" - chunk = self.input.read(self.chunkSize) - if chunk: - return chunk - else: - if hasattr(self.input, 'close'): - self.input.close() - raise StopIteration() - next = __next__ - - -def file_generator_limited(fileobj, count, chunk_size=65536): - """Yield the given file object in chunks. - - Stopps after `count` bytes has been emitted. - Default chunk size is 64kB. (Core) - """ - remaining = count - while remaining > 0: - chunk = fileobj.read(min(chunk_size, remaining)) - chunklen = len(chunk) - if chunklen == 0: - return - remaining -= chunklen - yield chunk - - -def set_vary_header(response, header_name): - """Add a Vary header to a response.""" - varies = response.headers.get('Vary', '') - varies = [x.strip() for x in varies.split(',') if x.strip()] - if header_name not in varies: - varies.append(header_name) - response.headers['Vary'] = ', '.join(varies) diff --git a/libraries/cherrypy/lib/auth_basic.py b/libraries/cherrypy/lib/auth_basic.py deleted file mode 100644 index ad379a26..00000000 --- a/libraries/cherrypy/lib/auth_basic.py +++ /dev/null @@ -1,120 +0,0 @@ -# This file is part of CherryPy <http://www.cherrypy.org/> -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 -"""HTTP Basic Authentication tool. - -This module provides a CherryPy 3.x tool which implements -the server-side of HTTP Basic Access Authentication, as described in -:rfc:`2617`. - -Example usage, using the built-in checkpassword_dict function which uses a dict -as the credentials store:: - - userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} - checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) - basic_auth = {'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'earth', - 'tools.auth_basic.checkpassword': checkpassword, - 'tools.auth_basic.accept_charset': 'UTF-8', - } - app_config = { '/' : basic_auth } - -""" - -import binascii -import unicodedata -import base64 - -import cherrypy -from cherrypy._cpcompat import ntou, tonative - - -__author__ = 'visteya' -__date__ = 'April 2009' - - -def checkpassword_dict(user_password_dict): - """Returns a checkpassword function which checks credentials - against a dictionary of the form: {username : password}. - - If you want a simple dictionary-based authentication scheme, use - checkpassword_dict(my_credentials_dict) as the value for the - checkpassword argument to basic_auth(). - """ - def checkpassword(realm, user, password): - p = user_password_dict.get(user) - return p and p == password or False - - return checkpassword - - -def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'): - """A CherryPy tool which hooks at before_handler to perform - HTTP Basic Access Authentication, as specified in :rfc:`2617` - and :rfc:`7617`. - - If the request has an 'authorization' header with a 'Basic' scheme, this - tool attempts to authenticate the credentials supplied in that header. If - the request has no 'authorization' header, or if it does but the scheme is - not 'Basic', or if authentication fails, the tool sends a 401 response with - a 'WWW-Authenticate' Basic header. - - realm - A string containing the authentication realm. - - checkpassword - A callable which checks the authentication credentials. - Its signature is checkpassword(realm, username, password). where - username and password are the values obtained from the request's - 'authorization' header. If authentication succeeds, checkpassword - returns True, else it returns False. - - """ - - fallback_charset = 'ISO-8859-1' - - if '"' in realm: - raise ValueError('Realm cannot contain the " (quote) character.') - request = cherrypy.serving.request - - auth_header = request.headers.get('authorization') - if auth_header is not None: - # split() error, base64.decodestring() error - msg = 'Bad Request' - with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg): - scheme, params = auth_header.split(' ', 1) - if scheme.lower() == 'basic': - charsets = accept_charset, fallback_charset - decoded_params = base64.b64decode(params.encode('ascii')) - decoded_params = _try_decode(decoded_params, charsets) - decoded_params = ntou(decoded_params) - decoded_params = unicodedata.normalize('NFC', decoded_params) - decoded_params = tonative(decoded_params) - username, password = decoded_params.split(':', 1) - if checkpassword(realm, username, password): - if debug: - cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') - request.login = username - return # successful authentication - - charset = accept_charset.upper() - charset_declaration = ( - (', charset="%s"' % charset) - if charset != fallback_charset - else '' - ) - # Respond with 401 status and a WWW-Authenticate header - cherrypy.serving.response.headers['www-authenticate'] = ( - 'Basic realm="%s"%s' % (realm, charset_declaration) - ) - raise cherrypy.HTTPError( - 401, 'You are not authorized to access that resource') - - -def _try_decode(subject, charsets): - for charset in charsets[:-1]: - try: - return tonative(subject, charset) - except ValueError: - pass - return tonative(subject, charsets[-1]) diff --git a/libraries/cherrypy/lib/auth_digest.py b/libraries/cherrypy/lib/auth_digest.py deleted file mode 100644 index 9b4f55c8..00000000 --- a/libraries/cherrypy/lib/auth_digest.py +++ /dev/null @@ -1,464 +0,0 @@ -# This file is part of CherryPy <http://www.cherrypy.org/> -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 -"""HTTP Digest Authentication tool. - -An implementation of the server-side of HTTP Digest Access -Authentication, which is described in :rfc:`2617`. - -Example usage, using the built-in get_ha1_dict_plain function which uses a dict -of plaintext passwords as the credentials store:: - - userpassdict = {'alice' : '4x5istwelve'} - get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) - digest_auth = {'tools.auth_digest.on': True, - 'tools.auth_digest.realm': 'wonderland', - 'tools.auth_digest.get_ha1': get_ha1, - 'tools.auth_digest.key': 'a565c27146791cfb', - 'tools.auth_digest.accept_charset': 'UTF-8', - } - app_config = { '/' : digest_auth } -""" - -import time -import functools -from hashlib import md5 - -from six.moves.urllib.request import parse_http_list, parse_keqv_list - -import cherrypy -from cherrypy._cpcompat import ntob, tonative - - -__author__ = 'visteya' -__date__ = 'April 2009' - - -def md5_hex(s): - return md5(ntob(s, 'utf-8')).hexdigest() - - -qop_auth = 'auth' -qop_auth_int = 'auth-int' -valid_qops = (qop_auth, qop_auth_int) - -valid_algorithms = ('MD5', 'MD5-sess') - -FALLBACK_CHARSET = 'ISO-8859-1' -DEFAULT_CHARSET = 'UTF-8' - - -def TRACE(msg): - cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') - -# Three helper functions for users of the tool, providing three variants -# of get_ha1() functions for three different kinds of credential stores. - - -def get_ha1_dict_plain(user_password_dict): - """Returns a get_ha1 function which obtains a plaintext password from a - dictionary of the form: {username : password}. - - If you want a simple dictionary-based authentication scheme, with plaintext - passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the - get_ha1 argument to digest_auth(). - """ - def get_ha1(realm, username): - password = user_password_dict.get(username) - if password: - return md5_hex('%s:%s:%s' % (username, realm, password)) - return None - - return get_ha1 - - -def get_ha1_dict(user_ha1_dict): - """Returns a get_ha1 function which obtains a HA1 password hash from a - dictionary of the form: {username : HA1}. - - If you want a dictionary-based authentication scheme, but with - pre-computed HA1 hashes instead of plain-text passwords, use - get_ha1_dict(my_userha1_dict) as the value for the get_ha1 - argument to digest_auth(). - """ - def get_ha1(realm, username): - return user_ha1_dict.get(username) - - return get_ha1 - - -def get_ha1_file_htdigest(filename): - """Returns a get_ha1 function which obtains a HA1 password hash from a - flat file with lines of the same format as that produced by the Apache - htdigest utility. For example, for realm 'wonderland', username 'alice', - and password '4x5istwelve', the htdigest line would be:: - - alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c - - If you want to use an Apache htdigest file as the credentials store, - then use get_ha1_file_htdigest(my_htdigest_file) as the value for the - get_ha1 argument to digest_auth(). It is recommended that the filename - argument be an absolute path, to avoid problems. - """ - 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() - return result - - return get_ha1 - - -def synthesize_nonce(s, key, timestamp=None): - """Synthesize a nonce value which resists spoofing and can be checked - for staleness. Returns a string suitable as the value for 'nonce' in - the www-authenticate header. - - s - A string related to the resource, such as the hostname of the server. - - key - A secret string known only to the server. - - timestamp - An integer seconds-since-the-epoch timestamp - - """ - if timestamp is None: - timestamp = int(time.time()) - h = md5_hex('%s:%s:%s' % (timestamp, s, key)) - nonce = '%s:%s' % (timestamp, h) - return nonce - - -def H(s): - """The hash function H""" - return md5_hex(s) - - -def _try_decode_header(header, charset): - global FALLBACK_CHARSET - - for enc in (charset, FALLBACK_CHARSET): - try: - return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc) - except ValueError as ve: - last_err = ve - else: - raise last_err - - -class HttpDigestAuthorization(object): - """ - Parses a Digest Authorization header and performs - re-calculation of the digest. - """ - - scheme = 'digest' - - def errmsg(self, s): - return 'Digest Authorization header: %s' % s - - @classmethod - def matches(cls, header): - scheme, _, _ = header.partition(' ') - return scheme.lower() == cls.scheme - - def __init__( - self, auth_header, http_method, - debug=False, accept_charset=DEFAULT_CHARSET[:], - ): - self.http_method = http_method - self.debug = debug - - if not self.matches(auth_header): - raise ValueError('Authorization scheme is not "Digest"') - - self.auth_header = _try_decode_header(auth_header, accept_charset) - - scheme, params = self.auth_header.split(' ', 1) - - # make a dict of the params - items = parse_http_list(params) - paramsd = parse_keqv_list(items) - - self.realm = paramsd.get('realm') - self.username = paramsd.get('username') - self.nonce = paramsd.get('nonce') - self.uri = paramsd.get('uri') - self.method = paramsd.get('method') - self.response = paramsd.get('response') # the response digest - self.algorithm = paramsd.get('algorithm', 'MD5').upper() - self.cnonce = paramsd.get('cnonce') - self.opaque = paramsd.get('opaque') - self.qop = paramsd.get('qop') # qop - self.nc = paramsd.get('nc') # nonce count - - # perform some correctness checks - if self.algorithm not in valid_algorithms: - raise ValueError( - self.errmsg("Unsupported value for algorithm: '%s'" % - self.algorithm)) - - has_reqd = ( - self.username and - self.realm and - self.nonce and - self.uri and - self.response - ) - if not has_reqd: - raise ValueError( - self.errmsg('Not all required parameters are present.')) - - if self.qop: - if self.qop not in valid_qops: - raise ValueError( - self.errmsg("Unsupported value for qop: '%s'" % self.qop)) - if not (self.cnonce and self.nc): - raise ValueError( - self.errmsg('If qop is sent then ' - 'cnonce and nc MUST be present')) - else: - if self.cnonce or self.nc: - raise ValueError( - self.errmsg('If qop is not sent, ' - 'neither cnonce nor nc can be present')) - - def __str__(self): - return 'authorization : %s' % self.auth_header - - def validate_nonce(self, s, key): - """Validate the nonce. - Returns True if nonce was generated by synthesize_nonce() and the - timestamp is not spoofed, else returns False. - - s - A string related to the resource, such as the hostname of - the server. - - key - A secret string known only to the server. - - Both s and key must be the same values which were used to synthesize - the nonce we are trying to validate. - """ - try: - timestamp, hashpart = self.nonce.split(':', 1) - s_timestamp, s_hashpart = synthesize_nonce( - s, key, timestamp).split(':', 1) - is_valid = s_hashpart == hashpart - if self.debug: - TRACE('validate_nonce: %s' % is_valid) - return is_valid - except ValueError: # split() error - pass - return False - - def is_nonce_stale(self, max_age_seconds=600): - """Returns True if a validated nonce is stale. The nonce contains a - timestamp in plaintext and also a secure hash of the timestamp. - You should first validate the nonce to ensure the plaintext - timestamp is not spoofed. - """ - try: - timestamp, hashpart = self.nonce.split(':', 1) - if int(timestamp) + max_age_seconds > int(time.time()): - return False - except ValueError: # int() error - pass - if self.debug: - TRACE('nonce is stale') - return True - - def HA2(self, entity_body=''): - """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" - # RFC 2617 3.2.2.3 - # If the "qop" directive's value is "auth" or is unspecified, - # then A2 is: - # A2 = method ":" digest-uri-value - # - # If the "qop" value is "auth-int", then A2 is: - # A2 = method ":" digest-uri-value ":" H(entity-body) - if self.qop is None or self.qop == 'auth': - a2 = '%s:%s' % (self.http_method, self.uri) - elif self.qop == 'auth-int': - a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) - else: - # in theory, this should never happen, since I validate qop in - # __init__() - raise ValueError(self.errmsg('Unrecognized value for qop!')) - return H(a2) - - def request_digest(self, ha1, entity_body=''): - """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. - - ha1 - The HA1 string obtained from the credentials store. - - entity_body - If 'qop' is set to 'auth-int', then A2 includes a hash - of the "entity body". The entity body is the part of the - message which follows the HTTP headers. See :rfc:`2617` section - 4.3. This refers to the entity the user agent sent in the - request which has the Authorization header. Typically GET - requests don't have an entity, and POST requests do. - - """ - ha2 = self.HA2(entity_body) - # Request-Digest -- RFC 2617 3.2.2.1 - if self.qop: - req = '%s:%s:%s:%s:%s' % ( - self.nonce, self.nc, self.cnonce, self.qop, ha2) - else: - req = '%s:%s' % (self.nonce, ha2) - - # RFC 2617 3.2.2.2 - # - # If the "algorithm" directive's value is "MD5" or is unspecified, - # then A1 is: - # A1 = unq(username-value) ":" unq(realm-value) ":" passwd - # - # If the "algorithm" directive's value is "MD5-sess", then A1 is - # calculated only once - on the first request by the client following - # receipt of a WWW-Authenticate challenge from the server. - # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) - # ":" unq(nonce-value) ":" unq(cnonce-value) - if self.algorithm == 'MD5-sess': - ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) - - digest = H('%s:%s' % (ha1, req)) - return digest - - -def _get_charset_declaration(charset): - global FALLBACK_CHARSET - charset = charset.upper() - return ( - (', charset="%s"' % charset) - if charset != FALLBACK_CHARSET - else '' - ) - - -def www_authenticate( - realm, key, algorithm='MD5', nonce=None, qop=qop_auth, - stale=False, accept_charset=DEFAULT_CHARSET[:], -): - """Constructs a WWW-Authenticate header for Digest authentication.""" - if qop not in valid_qops: - raise ValueError("Unsupported value for qop: '%s'" % qop) - if algorithm not in valid_algorithms: - raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) - - HEADER_PATTERN = ( - 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s' - ) - - if nonce is None: - nonce = synthesize_nonce(realm, key) - - stale_param = ', stale="true"' if stale else '' - - charset_declaration = _get_charset_declaration(accept_charset) - - return HEADER_PATTERN % ( - realm, nonce, algorithm, qop, stale_param, charset_declaration, - ) - - -def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'): - """A CherryPy tool that hooks at before_handler to perform - HTTP Digest Access Authentication, as specified in :rfc:`2617`. - - If the request has an 'authorization' header with a 'Digest' scheme, - this tool authenticates the credentials supplied in that header. - If the request has no 'authorization' header, or if it does but the - scheme is not "Digest", or if authentication fails, the tool sends - a 401 response with a 'WWW-Authenticate' Digest header. - - realm - A string containing the authentication realm. - - get_ha1 - A callable that looks up a username in a credentials store - and returns the HA1 string, which is defined in the RFC to be - MD5(username : realm : password). The function's signature is: - ``get_ha1(realm, username)`` - where username is obtained from the request's 'authorization' header. - If username is not found in the credentials store, get_ha1() returns - None. - - key - A secret string known only to the server, used in the synthesis - of nonces. - - """ - request = cherrypy.serving.request - - auth_header = request.headers.get('authorization') - - respond_401 = functools.partial( - _respond_401, realm, key, accept_charset, debug) - - if not HttpDigestAuthorization.matches(auth_header or ''): - respond_401() - - msg = 'The Authorization header could not be parsed.' - with cherrypy.HTTPError.handle(ValueError, 400, msg): - auth = HttpDigestAuthorization( - auth_header, request.method, - debug=debug, accept_charset=accept_charset, - ) - - if debug: - TRACE(str(auth)) - - if not auth.validate_nonce(realm, key): - respond_401() - - ha1 = get_ha1(realm, auth.username) - - if ha1 is None: - respond_401() - - # note that for request.body to be available we need to - # hook in at before_handler, not on_start_resource like - # 3.1.x digest_auth does. - digest = auth.request_digest(ha1, entity_body=request.body) - if digest != auth.response: - respond_401() - - # authenticated - if debug: - TRACE('digest matches auth.response') - # Now check if nonce is stale. - # The choice of ten minutes' lifetime for nonce is somewhat - # arbitrary - if auth.is_nonce_stale(max_age_seconds=600): - respond_401(stale=True) - - request.login = auth.username - if debug: - TRACE('authentication of %s successful' % auth.username) - - -def _respond_401(realm, key, accept_charset, debug, **kwargs): - """ - Respond with 401 status and a WWW-Authenticate header - """ - header = www_authenticate( - realm, key, - accept_charset=accept_charset, - **kwargs - ) - if debug: - TRACE(header) - cherrypy.serving.response.headers['WWW-Authenticate'] = header - raise cherrypy.HTTPError( - 401, 'You are not authorized to access that resource') diff --git a/libraries/cherrypy/lib/caching.py b/libraries/cherrypy/lib/caching.py deleted file mode 100644 index fed325a6..00000000 --- a/libraries/cherrypy/lib/caching.py +++ /dev/null @@ -1,482 +0,0 @@ -""" -CherryPy implements a simple caching system as a pluggable Tool. This tool -tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there -yet, but it's probably good enough for most sites. - -In general, GET responses are cached (along with selecting headers) and, if -another request arrives for the same resource, the caching Tool will return 304 -Not Modified if possible, or serve the cached response otherwise. It also sets -request.cached to True if serving a cached representation, and sets -request.cacheable to False (so it doesn't get cached again). - -If POST, PUT, or DELETE requests are made for a cached resource, they -invalidate (delete) any cached response. - -Usage -===== - -Configuration file example:: - - [/] - tools.caching.on = True - tools.caching.delay = 3600 - -You may use a class other than the default -:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config -entry ``cache_class``; supply the full dotted name of the replacement class -as the config value. It must implement the basic methods ``get``, ``put``, -``delete``, and ``clear``. - -You may set any attribute, including overriding methods, on the cache -instance by providing them in config. The above sets the -:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example. -""" - -import datetime -import sys -import threading -import time - -import six - -import cherrypy -from cherrypy.lib import cptools, httputil -from cherrypy._cpcompat import Event - - -class Cache(object): - - """Base class for Cache implementations.""" - - def get(self): - """Return the current variant if in the cache, else None.""" - raise NotImplemented - - def put(self, obj, size): - """Store the current variant in the cache.""" - raise NotImplemented - - def delete(self): - """Remove ALL cached variants of the current resource.""" - raise NotImplemented - - def clear(self): - """Reset the cache to its initial, empty state.""" - raise NotImplemented - - -# ------------------------------ Memory Cache ------------------------------- # -class AntiStampedeCache(dict): - - """A storage system for cached items which reduces stampede collisions.""" - - def wait(self, key, timeout=5, debug=False): - """Return the cached value for the given key, or None. - - If timeout is not None, and the value is already - being calculated by another thread, wait until the given timeout has - elapsed. If the value is available before the timeout expires, it is - returned. If not, None is returned, and a sentinel placed in the cache - to signal other threads to wait. - - If timeout is None, no waiting is performed nor sentinels used. - """ - value = self.get(key) - if isinstance(value, Event): - if timeout is None: - # Ignore the other thread and recalc it ourselves. - if debug: - cherrypy.log('No timeout', 'TOOLS.CACHING') - return None - - # Wait until it's done or times out. - if debug: - cherrypy.log('Waiting up to %s seconds' % - timeout, 'TOOLS.CACHING') - value.wait(timeout) - if value.result is not None: - # The other thread finished its calculation. Use it. - if debug: - cherrypy.log('Result!', 'TOOLS.CACHING') - return value.result - # Timed out. Stick an Event in the slot so other threads wait - # on this one to finish calculating the value. - if debug: - cherrypy.log('Timed out', 'TOOLS.CACHING') - e = threading.Event() - e.result = None - dict.__setitem__(self, key, e) - - return None - elif value is None: - # Stick an Event in the slot so other threads wait - # on this one to finish calculating the value. - if debug: - cherrypy.log('Timed out', 'TOOLS.CACHING') - e = threading.Event() - e.result = None - dict.__setitem__(self, key, e) - return value - - def __setitem__(self, key, value): - """Set the cached value for the given key.""" - existing = self.get(key) - dict.__setitem__(self, key, value) - if isinstance(existing, Event): - # Set Event.result so other threads waiting on it have - # immediate access without needing to poll the cache again. - existing.result = value - existing.set() - - -class MemoryCache(Cache): - - """An in-memory cache for varying response content. - - Each key in self.store is a URI, and each value is an AntiStampedeCache. - The response for any given URI may vary based on the values of - "selecting request headers"; that is, those named in the Vary - response header. We assume the list of header names to be constant - for each URI throughout the lifetime of the application, and store - that list in ``self.store[uri].selecting_headers``. - - The items contained in ``self.store[uri]`` have keys which are tuples of - request header values (in the same order as the names in its - selecting_headers), and values which are the actual responses. - """ - - maxobjects = 1000 - """The maximum number of cached objects; defaults to 1000.""" - - maxobj_size = 100000 - """The maximum size of each cached object in bytes; defaults to 100 KB.""" - - maxsize = 10000000 - """The maximum size of the entire cache in bytes; defaults to 10 MB.""" - - delay = 600 - """Seconds until the cached content expires; defaults to 600 (10 minutes). - """ - - antistampede_timeout = 5 - """Seconds to wait for other threads to release a cache lock.""" - - expire_freq = 0.1 - """Seconds to sleep between cache expiration sweeps.""" - - debug = False - - def __init__(self): - self.clear() - - # Run self.expire_cache in a separate daemon thread. - t = threading.Thread(target=self.expire_cache, name='expire_cache') - self.expiration_thread = t - t.daemon = True - t.start() - - def clear(self): - """Reset the cache to its initial, empty state.""" - self.store = {} - self.expirations = {} - self.tot_puts = 0 - self.tot_gets = 0 - self.tot_hist = 0 - self.tot_expires = 0 - self.tot_non_modified = 0 - self.cursize = 0 - - def expire_cache(self): - """Continuously examine cached objects, expiring stale ones. - - This function is designed to be run in its own daemon thread, - referenced at ``self.expiration_thread``. - """ - # It's possible that "time" will be set to None - # arbitrarily, so we check "while time" to avoid exceptions. - # See tickets #99 and #180 for more information. - while time: - now = time.time() - # Must make a copy of expirations so it doesn't change size - # during iteration - items = list(six.iteritems(self.expirations)) - for expiration_time, objects in items: - if expiration_time <= now: - for obj_size, uri, sel_header_values in objects: - try: - del self.store[uri][tuple(sel_header_values)] - self.tot_expires += 1 - self.cursize -= obj_size - except KeyError: - # the key may have been deleted elsewhere - pass - del self.expirations[expiration_time] - time.sleep(self.expire_freq) - - def get(self): - """Return the current variant if in the cache, else None.""" - request = cherrypy.serving.request - self.tot_gets += 1 - - uri = cherrypy.url(qs=request.query_string) - uricache = self.store.get(uri) - if uricache is None: - return None - - header_values = [request.headers.get(h, '') - for h in uricache.selecting_headers] - variant = uricache.wait(key=tuple(sorted(header_values)), - timeout=self.antistampede_timeout, - debug=self.debug) - if variant is not None: - self.tot_hist += 1 - return variant - - def put(self, variant, size): - """Store the current variant in the cache.""" - request = cherrypy.serving.request - response = cherrypy.serving.response - - uri = cherrypy.url(qs=request.query_string) - uricache = self.store.get(uri) - if uricache is None: - uricache = AntiStampedeCache() - uricache.selecting_headers = [ - e.value for e in response.headers.elements('Vary')] - self.store[uri] = uricache - - if len(self.store) < self.maxobjects: - total_size = self.cursize + size - - # checks if there's space for the object - if (size < self.maxobj_size and total_size < self.maxsize): - # add to the expirations list - expiration_time = response.time + self.delay - bucket = self.expirations.setdefault(expiration_time, []) - bucket.append((size, uri, uricache.selecting_headers)) - - # add to the cache - header_values = [request.headers.get(h, '') - for h in uricache.selecting_headers] - uricache[tuple(sorted(header_values))] = variant - self.tot_puts += 1 - self.cursize = total_size - - def delete(self): - """Remove ALL cached variants of the current resource.""" - uri = cherrypy.url(qs=cherrypy.serving.request.query_string) - self.store.pop(uri, None) - - -def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs): - """Try to obtain cached output. If fresh enough, raise HTTPError(304). - - If POST, PUT, or DELETE: - * invalidates (deletes) any cached response for this resource - * sets request.cached = False - * sets request.cacheable = False - - else if a cached copy exists: - * sets request.cached = True - * sets request.cacheable = False - * sets response.headers to the cached values - * checks the cached Last-Modified response header against the - current If-(Un)Modified-Since request headers; raises 304 - if necessary. - * sets response.status and response.body to the cached values - * returns True - - otherwise: - * sets request.cached = False - * sets request.cacheable = True - * returns False - """ - request = cherrypy.serving.request - response = cherrypy.serving.response - - if not hasattr(cherrypy, '_cache'): - # Make a process-wide Cache object. - cherrypy._cache = kwargs.pop('cache_class', MemoryCache)() - - # Take all remaining kwargs and set them on the Cache object. - for k, v in kwargs.items(): - setattr(cherrypy._cache, k, v) - cherrypy._cache.debug = debug - - # POST, PUT, DELETE should invalidate (delete) the cached copy. - # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. - if request.method in invalid_methods: - if debug: - cherrypy.log('request.method %r in invalid_methods %r' % - (request.method, invalid_methods), 'TOOLS.CACHING') - cherrypy._cache.delete() - request.cached = False - request.cacheable = False - return False - - if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: - request.cached = False - request.cacheable = True - return False - - cache_data = cherrypy._cache.get() - request.cached = bool(cache_data) - request.cacheable = not request.cached - if request.cached: - # Serve the cached copy. - max_age = cherrypy._cache.delay - for v in [e.value for e in request.headers.elements('Cache-Control')]: - atoms = v.split('=', 1) - directive = atoms.pop(0) - if directive == 'max-age': - if len(atoms) != 1 or not atoms[0].isdigit(): - raise cherrypy.HTTPError( - 400, 'Invalid Cache-Control header') - max_age = int(atoms[0]) - break - elif directive == 'no-cache': - if debug: - cherrypy.log( - 'Ignoring cache due to Cache-Control: no-cache', - 'TOOLS.CACHING') - request.cached = False - request.cacheable = True - return False - - if debug: - cherrypy.log('Reading response from cache', 'TOOLS.CACHING') - s, h, b, create_time = cache_data - age = int(response.time - create_time) - if (age > max_age): - if debug: - cherrypy.log('Ignoring cache due to age > %d' % max_age, - 'TOOLS.CACHING') - request.cached = False - request.cacheable = True - return False - - # Copy the response headers. See - # https://github.com/cherrypy/cherrypy/issues/721. - response.headers = rh = httputil.HeaderMap() - for k in h: - dict.__setitem__(rh, k, dict.__getitem__(h, k)) - - # Add the required Age header - response.headers['Age'] = str(age) - - try: - # Note that validate_since depends on a Last-Modified header; - # this was put into the cached copy, and should have been - # resurrected just above (response.headers = cache_data[1]). - cptools.validate_since() - except cherrypy.HTTPRedirect: - x = sys.exc_info()[1] - if x.status == 304: - cherrypy._cache.tot_non_modified += 1 - raise - - # serve it & get out from the request - response.status = s - response.body = b - else: - if debug: - cherrypy.log('request is not cached', 'TOOLS.CACHING') - return request.cached - - -def tee_output(): - """Tee response output to cache storage. Internal.""" - # Used by CachingTool by attaching to request.hooks - - request = cherrypy.serving.request - if 'no-store' in request.headers.values('Cache-Control'): - return - - def tee(body): - """Tee response.body into a list.""" - if ('no-cache' in response.headers.values('Pragma') or - 'no-store' in response.headers.values('Cache-Control')): - for chunk in body: - yield chunk - return - - output = [] - for chunk in body: - output.append(chunk) - yield chunk - - # Save the cache data, but only if the body isn't empty. - # e.g. a 304 Not Modified on a static file response will - # have an empty body. - # If the body is empty, delete the cache because it - # contains a stale Threading._Event object that will - # stall all consecutive requests until the _Event times - # out - body = b''.join(output) - if not body: - cherrypy._cache.delete() - else: - cherrypy._cache.put((response.status, response.headers or {}, - body, response.time), len(body)) - - response = cherrypy.serving.response - response.body = tee(response.body) - - -def expires(secs=0, force=False, debug=False): - """Tool for influencing cache mechanisms using the 'Expires' header. - - secs - Must be either an int or a datetime.timedelta, and indicates the - number of seconds between response.time and when the response should - expire. The 'Expires' header will be set to response.time + secs. - If secs is zero, the 'Expires' header is set one year in the past, and - the following "cache prevention" headers are also set: - - * Pragma: no-cache - * Cache-Control': no-cache, must-revalidate - - force - If False, the following headers are checked: - - * Etag - * Last-Modified - * Age - * Expires - - If any are already present, none of the above response headers are set. - - """ - - response = cherrypy.serving.response - headers = response.headers - - cacheable = False - if not force: - # some header names that indicate that the response can be cached - for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): - if indicator in headers: - cacheable = True - break - - if not cacheable and not force: - if debug: - cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') - else: - if debug: - cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') - if isinstance(secs, datetime.timedelta): - secs = (86400 * secs.days) + secs.seconds - - if secs == 0: - if force or ('Pragma' not in headers): - headers['Pragma'] = 'no-cache' - if cherrypy.serving.request.protocol >= (1, 1): - if force or 'Cache-Control' not in headers: - headers['Cache-Control'] = 'no-cache, must-revalidate' - # Set an explicit Expires date in the past. - expiry = httputil.HTTPDate(1169942400.0) - else: - expiry = httputil.HTTPDate(response.time + secs) - if force or 'Expires' not in headers: - headers['Expires'] = expiry diff --git a/libraries/cherrypy/lib/covercp.py b/libraries/cherrypy/lib/covercp.py deleted file mode 100644 index 0bafca13..00000000 --- a/libraries/cherrypy/lib/covercp.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Code-coverage tools for CherryPy. - -To use this module, or the coverage tools in the test suite, -you need to download 'coverage.py', either Gareth Rees' `original -implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_ -or Ned Batchelder's `enhanced version: -<http://www.nedbatchelder.com/code/modules/coverage.html>`_ - -To turn on coverage tracing, use the following code:: - - cherrypy.engine.subscribe('start', covercp.start) - -DO NOT subscribe anything on the 'start_thread' channel, as previously -recommended. Calling start once in the main thread should be sufficient -to start coverage on all threads. Calling start again in each thread -effectively clears any coverage data gathered up to that point. - -Run your code, then use the ``covercp.serve()`` function to browse the -results in a web browser. If you run this module from the command line, -it will call ``serve()`` for you. -""" - -import re -import sys -import cgi -import os -import os.path - -from six.moves import urllib - -import cherrypy - - -localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache') - -the_coverage = None -try: - from coverage import coverage - the_coverage = coverage(data_file=localFile) - - def start(): - the_coverage.start() -except ImportError: - # Setting the_coverage to None will raise errors - # that need to be trapped downstream. - the_coverage = None - - import warnings - warnings.warn( - 'No code coverage will be performed; ' - 'coverage.py could not be imported.') - - def start(): - pass -start.priority = 20 - -TEMPLATE_MENU = """<html> -<head> - <title>CherryPy Coverage Menu</title> - <style> - body {font: 9pt Arial, serif;} - #tree { - font-size: 8pt; - font-family: Andale Mono, monospace; - white-space: pre; - } - #tree a:active, a:focus { - background-color: black; - padding: 1px; - color: white; - border: 0px solid #9999FF; - -moz-outline-style: none; - } - .fail { color: red;} - .pass { color: #888;} - #pct { text-align: right;} - h3 { - font-size: small; - font-weight: bold; - font-style: italic; - margin-top: 5px; - } - input { border: 1px solid #ccc; padding: 2px; } - .directory { - color: #933; - font-style: italic; - font-weight: bold; - font-size: 10pt; - } - .file { - color: #400; - } - a { text-decoration: none; } - #crumbs { - color: white; - font-size: 8pt; - font-family: Andale Mono, monospace; - width: 100%; - background-color: black; - } - #crumbs a { - color: #f88; - } - #options { - line-height: 2.3em; - border: 1px solid black; - background-color: #eee; - padding: 4px; - } - #exclude { - width: 100%; - margin-bottom: 3px; - border: 1px solid #999; - } - #submit { - background-color: black; - color: white; - border: 0; - margin-bottom: -9px; - } - </style> -</head> -<body> -<h2>CherryPy Coverage</h2>""" - -TEMPLATE_FORM = """ -<div id="options"> -<form action='menu' method=GET> - <input type='hidden' name='base' value='%(base)s' /> - Show percentages - <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br /> - Hide files over - <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br /> - Exclude files matching<br /> - <input type='text' id='exclude' name='exclude' - value='%(exclude)s' size='20' /> - <br /> - - <input type='submit' value='Change view' id="submit"/> -</form> -</div>""" - -TEMPLATE_FRAMESET = """<html> -<head><title>CherryPy coverage data</title></head> -<frameset cols='250, 1*'> - <frame src='menu?base=%s' /> - <frame name='main' src='' /> -</frameset> -</html> -""" - -TEMPLATE_COVERAGE = """<html> -<head> - <title>Coverage for %(name)s</title> - <style> - h2 { margin-bottom: .25em; } - p { margin: .25em; } - .covered { color: #000; background-color: #fff; } - .notcovered { color: #fee; background-color: #500; } - .excluded { color: #00f; background-color: #fff; } - table .covered, table .notcovered, table .excluded - { font-family: Andale Mono, monospace; - font-size: 10pt; white-space: pre; } - - .lineno { background-color: #eee;} - .notcovered .lineno { background-color: #000;} - table { border-collapse: collapse; - </style> -</head> -<body> -<h2>%(name)s</h2> -<p>%(fullpath)s</p> -<p>Coverage: %(pc)s%%</p>""" - -TEMPLATE_LOC_COVERED = """<tr class="covered"> - <td class="lineno">%s </td> - <td>%s</td> -</tr>\n""" -TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered"> - <td class="lineno">%s </td> - <td>%s</td> -</tr>\n""" -TEMPLATE_LOC_EXCLUDED = """<tr class="excluded"> - <td class="lineno">%s </td> - <td>%s</td> -</tr>\n""" - -TEMPLATE_ITEM = ( - "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n" -) - - -def _percent(statements, missing): - s = len(statements) - e = s - len(missing) - if s > 0: - return int(round(100.0 * e / s)) - return 0 - - -def _show_branch(root, base, path, pct=0, showpct=False, exclude='', - coverage=the_coverage): - - # Show the directory name and any of our children - dirs = [k for k, v in root.items() if v] - dirs.sort() - for name in dirs: - newpath = os.path.join(path, name) - - if newpath.lower().startswith(base): - relpath = newpath[len(base):] - yield '| ' * relpath.count(os.sep) - yield ( - "<a class='directory' " - "href='menu?base=%s&exclude=%s'>%s</a>\n" % - (newpath, urllib.parse.quote_plus(exclude), name) - ) - - for chunk in _show_branch( - root[name], base, newpath, pct, showpct, - exclude, coverage=coverage - ): - yield chunk - - # Now list the files - if path.lower().startswith(base): - relpath = path[len(base):] - files = [k for k, v in root.items() if not v] - files.sort() - for name in files: - newpath = os.path.join(path, name) - - pc_str = '' - if showpct: - try: - _, statements, _, missing, _ = coverage.analysis2(newpath) - except Exception: - # Yes, we really want to pass on all errors. - pass - else: - pc = _percent(statements, missing) - pc_str = ('%3d%% ' % pc).replace(' ', ' ') - if pc < float(pct) or pc == -1: - pc_str = "<span class='fail'>%s</span>" % pc_str - else: - pc_str = "<span class='pass'>%s</span>" % pc_str - - yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1), - pc_str, newpath, name) - - -def _skip_file(path, exclude): - if exclude: - return bool(re.search(exclude, path)) - - -def _graft(path, tree): - d = tree - - p = path - atoms = [] - while True: - p, tail = os.path.split(p) - if not tail: - break - atoms.append(tail) - atoms.append(p) - if p != '/': - atoms.append('/') - - atoms.reverse() - for node in atoms: - if node: - d = d.setdefault(node, {}) - - -def get_tree(base, exclude, coverage=the_coverage): - """Return covered module names as a nested dict.""" - tree = {} - runs = coverage.data.executed_files() - for path in runs: - if not _skip_file(path, exclude) and not os.path.isdir(path): - _graft(path, tree) - return tree - - -class CoverStats(object): - - def __init__(self, coverage, root=None): - self.coverage = coverage - if root is None: - # Guess initial depth. Files outside this path will not be - # reachable from the web interface. - root = os.path.dirname(cherrypy.__file__) - self.root = root - - @cherrypy.expose - def index(self): - return TEMPLATE_FRAMESET % self.root.lower() - - @cherrypy.expose - def menu(self, base='/', pct='50', showpct='', - exclude=r'python\d\.\d|test|tut\d|tutorial'): - - # The coverage module uses all-lower-case names. - base = base.lower().rstrip(os.sep) - - yield TEMPLATE_MENU - yield TEMPLATE_FORM % locals() - - # Start by showing links for parent paths - yield "<div id='crumbs'>" - path = '' - atoms = base.split(os.sep) - atoms.pop() - for atom in atoms: - path += atom + os.sep - yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" - % (path, urllib.parse.quote_plus(exclude), atom, os.sep)) - yield '</div>' - - yield "<div id='tree'>" - - # Then display the tree - tree = get_tree(base, exclude, self.coverage) - if not tree: - yield '<p>No modules covered.</p>' - else: - for chunk in _show_branch(tree, base, '/', pct, - showpct == 'checked', exclude, - coverage=self.coverage): - yield chunk - - yield '</div>' - yield '</body></html>' - - def annotated_file(self, filename, statements, excluded, missing): - source = open(filename, 'r') - buffer = [] - for lineno, line in enumerate(source.readlines()): - lineno += 1 - line = line.strip('\n\r') - empty_the_buffer = True - if lineno in excluded: - template = TEMPLATE_LOC_EXCLUDED - elif lineno in missing: - template = TEMPLATE_LOC_NOT_COVERED - elif lineno in statements: - template = TEMPLATE_LOC_COVERED - else: - empty_the_buffer = False - buffer.append((lineno, line)) - if empty_the_buffer: - for lno, pastline in buffer: - yield template % (lno, cgi.escape(pastline)) - buffer = [] - yield template % (lineno, cgi.escape(line)) - - @cherrypy.expose - def report(self, name): - filename, statements, excluded, missing, _ = self.coverage.analysis2( - name) - pc = _percent(statements, missing) - yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), - fullpath=name, - pc=pc) - yield '<table>\n' - for line in self.annotated_file(filename, statements, excluded, - missing): - yield line - yield '</table>' - yield '</body>' - yield '</html>' - - -def serve(path=localFile, port=8080, root=None): - if coverage is None: - raise ImportError('The coverage module could not be imported.') - from coverage import coverage - cov = coverage(data_file=path) - cov.load() - - cherrypy.config.update({'server.socket_port': int(port), - 'server.thread_pool': 10, - 'environment': 'production', - }) - cherrypy.quickstart(CoverStats(cov, root)) - - -if __name__ == '__main__': - serve(*tuple(sys.argv[1:])) diff --git a/libraries/cherrypy/lib/cpstats.py b/libraries/cherrypy/lib/cpstats.py deleted file mode 100644 index ae9f7475..00000000 --- a/libraries/cherrypy/lib/cpstats.py +++ /dev/null @@ -1,696 +0,0 @@ -"""CPStats, a package for collecting and reporting on program statistics. - -Overview -======== - -Statistics about program operation are an invaluable monitoring and debugging -tool. Unfortunately, the gathering and reporting of these critical values is -usually ad-hoc. This package aims to add a centralized place for gathering -statistical performance data, a structure for recording that data which -provides for extrapolation of that data into more useful information, -and a method of serving that data to both human investigators and -monitoring software. Let's examine each of those in more detail. - -Data Gathering --------------- - -Just as Python's `logging` module provides a common importable for gathering -and sending messages, performance statistics would benefit from a similar -common mechanism, and one that does *not* require each package which wishes -to collect stats to import a third-party module. Therefore, we choose to -re-use the `logging` module by adding a `statistics` object to it. - -That `logging.statistics` object is a nested dict. It is not a custom class, -because that would: - - 1. require libraries and applications to import a third-party module in - order to participate - 2. inhibit innovation in extrapolation approaches and in reporting tools, and - 3. be slow. - -There are, however, some specifications regarding the structure of the dict.:: - - { - +----"SQLAlchemy": { - | "Inserts": 4389745, - | "Inserts per Second": - | lambda s: s["Inserts"] / (time() - s["Start"]), - | C +---"Table Statistics": { - | o | "widgets": {-----------+ - N | l | "Rows": 1.3M, | Record - a | l | "Inserts": 400, | - m | e | },---------------------+ - e | c | "froobles": { - s | t | "Rows": 7845, - p | i | "Inserts": 0, - a | o | }, - c | n +---}, - e | "Slow Queries": - | [{"Query": "SELECT * FROM widgets;", - | "Processing Time": 47.840923343, - | }, - | ], - +----}, - } - -The `logging.statistics` dict has four levels. The topmost level is nothing -more than a set of names to introduce modularity, usually along the lines of -package names. If the SQLAlchemy project wanted to participate, for example, -it might populate the item `logging.statistics['SQLAlchemy']`, whose value -would be a second-layer dict we call a "namespace". Namespaces help multiple -packages to avoid collisions over key names, and make reports easier to read, -to boot. The maintainers of SQLAlchemy should feel free to use more than one -namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case -or other syntax constraints on the namespace names; they should be chosen -to be maximally readable by humans (neither too short nor too long). - -Each namespace, then, is a dict of named statistical values, such as -'Requests/sec' or 'Uptime'. You should choose names which will look -good on a report: spaces and capitalization are just fine. - -In addition to scalars, values in a namespace MAY be a (third-layer) -dict, or a list, called a "collection". For example, the CherryPy -:class:`StatsTool` keeps track of what each request is doing (or has most -recently done) in a 'Requests' collection, where each key is a thread ID; each -value in the subdict MUST be a fourth dict (whew!) of statistical data about -each thread. We call each subdict in the collection a "record". Similarly, -the :class:`StatsTool` also keeps a list of slow queries, where each record -contains data about each slow query, in order. - -Values in a namespace or record may also be functions, which brings us to: - -Extrapolation -------------- - -The collection of statistical data needs to be fast, as close to unnoticeable -as possible to the host program. That requires us to minimize I/O, for example, -but in Python it also means we need to minimize function calls. So when you -are designing your namespace and record values, try to insert the most basic -scalar values you already have on hand. - -When it comes time to report on the gathered data, however, we usually have -much more freedom in what we can calculate. Therefore, whenever reporting -tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents -of `logging.statistics` for reporting, they first call -`extrapolate_statistics` (passing the whole `statistics` dict as the only -argument). This makes a deep copy of the statistics dict so that the -reporting tool can both iterate over it and even change it without harming -the original. But it also expands any functions in the dict by calling them. -For example, you might have a 'Current Time' entry in the namespace with the -value "lambda scope: time.time()". The "scope" parameter is the current -namespace dict (or record, if we're currently expanding one of those -instead), allowing you access to existing static entries. If you're truly -evil, you can even modify more than one entry at a time. - -However, don't try to calculate an entry and then use its value in further -extrapolations; the order in which the functions are called is not guaranteed. -This can lead to a certain amount of duplicated work (or a redesign of your -schema), but that's better than complicating the spec. - -After the whole thing has been extrapolated, it's time for: - -Reporting ---------- - -The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates -it all, and then transforms it to HTML for easy viewing. Each namespace gets -its own header and attribute table, plus an extra table for each collection. -This is NOT part of the statistics specification; other tools can format how -they like. - -You can control which columns are output and how they are formatted by updating -StatsPage.formatting, which is a dict that mirrors the keys and nesting of -`logging.statistics`. The difference is that, instead of data values, it has -formatting values. Use None for a given key to indicate to the StatsPage that a -given column should not be output. Use a string with formatting -(such as '%.3f') to interpolate the value(s), or use a callable (such as -lambda v: v.isoformat()) for more advanced formatting. Any entry which is not -mentioned in the formatting dict is output unchanged. - -Monitoring ----------- - -Although the HTML output takes pains to assign unique id's to each <td> with -statistical data, you're probably better off fetching /cpstats/data, which -outputs the whole (extrapolated) `logging.statistics` dict in JSON format. -That is probably easier to parse, and doesn't have any formatting controls, -so you get the "original" data in a consistently-serialized format. -Note: there's no treatment yet for datetime objects. Try time.time() instead -for now if you can. Nagios will probably thank you. - -Turning Collection Off ----------------------- - -It is recommended each namespace have an "Enabled" item which, if False, -stops collection (but not reporting) of statistical data. Applications -SHOULD provide controls to pause and resume collection by setting these -entries to False or True, if present. - - -Usage -===== - -To collect statistics on CherryPy applications:: - - from cherrypy.lib import cpstats - appconfig['/']['tools.cpstats.on'] = True - -To collect statistics on your own code:: - - import logging - # Initialize the repository - if not hasattr(logging, 'statistics'): logging.statistics = {} - # Initialize my namespace - mystats = logging.statistics.setdefault('My Stuff', {}) - # Initialize my namespace's scalars and collections - mystats.update({ - 'Enabled': True, - 'Start Time': time.time(), - 'Important Events': 0, - 'Events/Second': lambda s: ( - (s['Important Events'] / (time.time() - s['Start Time']))), - }) - ... - for event in events: - ... - # Collect stats - if mystats.get('Enabled', False): - mystats['Important Events'] += 1 - -To report statistics:: - - root.cpstats = cpstats.StatsPage() - -To format statistics reports:: - - See 'Reporting', above. - -""" - -import logging -import os -import sys -import threading -import time - -import six - -import cherrypy -from cherrypy._cpcompat import json - -# ------------------------------- Statistics -------------------------------- # - -if not hasattr(logging, 'statistics'): - logging.statistics = {} - - -def extrapolate_statistics(scope): - """Return an extrapolated copy of the given scope.""" - c = {} - for k, v in list(scope.items()): - if isinstance(v, dict): - v = extrapolate_statistics(v) - elif isinstance(v, (list, tuple)): - v = [extrapolate_statistics(record) for record in v] - elif hasattr(v, '__call__'): - v = v(scope) - c[k] = v - return c - - -# -------------------- CherryPy Applications Statistics --------------------- # - -appstats = logging.statistics.setdefault('CherryPy Applications', {}) -appstats.update({ - 'Enabled': True, - 'Bytes Read/Request': lambda s: ( - s['Total Requests'] and - (s['Total Bytes Read'] / float(s['Total Requests'])) or - 0.0 - ), - 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), - 'Bytes Written/Request': lambda s: ( - s['Total Requests'] and - (s['Total Bytes Written'] / float(s['Total Requests'])) or - 0.0 - ), - 'Bytes Written/Second': lambda s: ( - s['Total Bytes Written'] / s['Uptime'](s) - ), - 'Current Time': lambda s: time.time(), - 'Current Requests': 0, - 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), - 'Server Version': cherrypy.__version__, - 'Start Time': time.time(), - 'Total Bytes Read': 0, - 'Total Bytes Written': 0, - 'Total Requests': 0, - 'Total Time': 0, - 'Uptime': lambda s: time.time() - s['Start Time'], - 'Requests': {}, -}) - - -def proc_time(s): - return time.time() - s['Start Time'] - - -class ByteCountWrapper(object): - - """Wraps a file-like object, counting the number of bytes read.""" - - def __init__(self, rfile): - self.rfile = rfile - self.bytes_read = 0 - - def read(self, size=-1): - data = self.rfile.read(size) - self.bytes_read += len(data) - return data - - def readline(self, size=-1): - data = self.rfile.readline(size) - self.bytes_read += len(data) - return data - - def readlines(self, sizehint=0): - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline() - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline() - return lines - - def close(self): - self.rfile.close() - - def __iter__(self): - return self - - def next(self): - data = self.rfile.next() - self.bytes_read += len(data) - return data - - -def average_uriset_time(s): - return s['Count'] and (s['Sum'] / s['Count']) or 0 - - -def _get_threading_ident(): - if sys.version_info >= (3, 3): - return threading.get_ident() - return threading._get_ident() - - -class StatsTool(cherrypy.Tool): - - """Record various information about the current request.""" - - def __init__(self): - cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) - - def _setup(self): - """Hook this tool into cherrypy.request. - - The standard CherryPy request object will automatically call this - method when the tool is "turned on" in config. - """ - if appstats.get('Enabled', False): - cherrypy.Tool._setup(self) - self.record_start() - - def record_start(self): - """Record the beginning of a request.""" - request = cherrypy.serving.request - if not hasattr(request.rfile, 'bytes_read'): - request.rfile = ByteCountWrapper(request.rfile) - request.body.fp = request.rfile - - r = request.remote - - appstats['Current Requests'] += 1 - appstats['Total Requests'] += 1 - appstats['Requests'][_get_threading_ident()] = { - 'Bytes Read': None, - 'Bytes Written': None, - # Use a lambda so the ip gets updated by tools.proxy later - 'Client': lambda s: '%s:%s' % (r.ip, r.port), - 'End Time': None, - 'Processing Time': proc_time, - 'Request-Line': request.request_line, - 'Response Status': None, - 'Start Time': time.time(), - } - - def record_stop( - self, uriset=None, slow_queries=1.0, slow_queries_count=100, - debug=False, **kwargs): - """Record the end of a request.""" - resp = cherrypy.serving.response - w = appstats['Requests'][_get_threading_ident()] - - r = cherrypy.request.rfile.bytes_read - w['Bytes Read'] = r - appstats['Total Bytes Read'] += r - - if resp.stream: - w['Bytes Written'] = 'chunked' - else: - cl = int(resp.headers.get('Content-Length', 0)) - w['Bytes Written'] = cl - appstats['Total Bytes Written'] += cl - - w['Response Status'] = getattr( - resp, 'output_status', None) or resp.status - - w['End Time'] = time.time() - p = w['End Time'] - w['Start Time'] - w['Processing Time'] = p - appstats['Total Time'] += p - - appstats['Current Requests'] -= 1 - - if debug: - cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') - - if uriset: - rs = appstats.setdefault('URI Set Tracking', {}) - r = rs.setdefault(uriset, { - 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, - 'Avg': average_uriset_time}) - if r['Min'] is None or p < r['Min']: - r['Min'] = p - if r['Max'] is None or p > r['Max']: - r['Max'] = p - r['Count'] += 1 - r['Sum'] += p - - if slow_queries and p > slow_queries: - sq = appstats.setdefault('Slow Queries', []) - sq.append(w.copy()) - if len(sq) > slow_queries_count: - sq.pop(0) - - -cherrypy.tools.cpstats = StatsTool() - - -# ---------------------- CherryPy Statistics Reporting ---------------------- # - -thisdir = os.path.abspath(os.path.dirname(__file__)) - -missing = object() - - -def locale_date(v): - return time.strftime('%c', time.gmtime(v)) - - -def iso_format(v): - return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) - - -def pause_resume(ns): - def _pause_resume(enabled): - pause_disabled = '' - resume_disabled = '' - if enabled: - resume_disabled = 'disabled="disabled" ' - else: - pause_disabled = 'disabled="disabled" ' - return """ - <form action="pause" method="POST" style="display:inline"> - <input type="hidden" name="namespace" value="%s" /> - <input type="submit" value="Pause" %s/> - </form> - <form action="resume" method="POST" style="display:inline"> - <input type="hidden" name="namespace" value="%s" /> - <input type="submit" value="Resume" %s/> - </form> - """ % (ns, pause_disabled, ns, resume_disabled) - return _pause_resume - - -class StatsPage(object): - - formatting = { - 'CherryPy Applications': { - 'Enabled': pause_resume('CherryPy Applications'), - 'Bytes Read/Request': '%.3f', - 'Bytes Read/Second': '%.3f', - 'Bytes Written/Request': '%.3f', - 'Bytes Written/Second': '%.3f', - 'Current Time': iso_format, - 'Requests/Second': '%.3f', - 'Start Time': iso_format, - 'Total Time': '%.3f', - 'Uptime': '%.3f', - 'Slow Queries': { - 'End Time': None, - 'Processing Time': '%.3f', - 'Start Time': iso_format, - }, - 'URI Set Tracking': { - 'Avg': '%.3f', - 'Max': '%.3f', - 'Min': '%.3f', - 'Sum': '%.3f', - }, - 'Requests': { - 'Bytes Read': '%s', - 'Bytes Written': '%s', - 'End Time': None, - 'Processing Time': '%.3f', - 'Start Time': None, - }, - }, - 'CherryPy WSGIServer': { - 'Enabled': pause_resume('CherryPy WSGIServer'), - 'Connections/second': '%.3f', - 'Start time': iso_format, - }, - } - - @cherrypy.expose - def index(self): - # Transform the raw data into pretty output for HTML - yield """ -<html> -<head> - <title>Statistics</title> -<style> - -th, td { - padding: 0.25em 0.5em; - border: 1px solid #666699; -} - -table { - border-collapse: collapse; -} - -table.stats1 { - width: 100%; -} - -table.stats1 th { - font-weight: bold; - text-align: right; - background-color: #CCD5DD; -} - -table.stats2, h2 { - margin-left: 50px; -} - -table.stats2 th { - font-weight: bold; - text-align: center; - background-color: #CCD5DD; -} - -</style> -</head> -<body> -""" - for title, scalars, collections in self.get_namespaces(): - yield """ -<h1>%s</h1> - -<table class='stats1'> - <tbody> -""" % title - for i, (key, value) in enumerate(scalars): - colnum = i % 3 - if colnum == 0: - yield """ - <tr>""" - yield ( - """ - <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % - vars() - ) - if colnum == 2: - yield """ - </tr>""" - - if colnum == 0: - yield """ - <th></th><td></td> - <th></th><td></td> - </tr>""" - elif colnum == 1: - yield """ - <th></th><td></td> - </tr>""" - yield """ - </tbody> -</table>""" - - for subtitle, headers, subrows in collections: - yield """ -<h2>%s</h2> -<table class='stats2'> - <thead> - <tr>""" % subtitle - for key in headers: - yield """ - <th>%s</th>""" % key - yield """ - </tr> - </thead> - <tbody>""" - for subrow in subrows: - yield """ - <tr>""" - for value in subrow: - yield """ - <td>%s</td>""" % value - yield """ - </tr>""" - yield """ - </tbody> -</table>""" - yield """ -</body> -</html> -""" - - def get_namespaces(self): - """Yield (title, scalars, collections) for each namespace.""" - s = extrapolate_statistics(logging.statistics) - for title, ns in sorted(s.items()): - scalars = [] - collections = [] - ns_fmt = self.formatting.get(title, {}) - for k, v in sorted(ns.items()): - fmt = ns_fmt.get(k, {}) - if isinstance(v, dict): - headers, subrows = self.get_dict_collection(v, fmt) - collections.append((k, ['ID'] + headers, subrows)) - elif isinstance(v, (list, tuple)): - headers, subrows = self.get_list_collection(v, fmt) - collections.append((k, headers, subrows)) - else: - format = ns_fmt.get(k, missing) - if format is None: - # Don't output this column. - continue - if hasattr(format, '__call__'): - v = format(v) - elif format is not missing: - v = format % v - scalars.append((k, v)) - yield title, scalars, collections - - def get_dict_collection(self, v, formatting): - """Return ([headers], [rows]) for the given collection.""" - # E.g., the 'Requests' dict. - headers = [] - vals = six.itervalues(v) - for record in vals: - for k3 in record: - format = formatting.get(k3, missing) - if format is None: - # Don't output this column. - continue - if k3 not in headers: - headers.append(k3) - headers.sort() - - subrows = [] - for k2, record in sorted(v.items()): - subrow = [k2] - for k3 in headers: - v3 = record.get(k3, '') - format = formatting.get(k3, missing) - if format is None: - # Don't output this column. - continue - if hasattr(format, '__call__'): - v3 = format(v3) - elif format is not missing: - v3 = format % v3 - subrow.append(v3) - subrows.append(subrow) - - return headers, subrows - - def get_list_collection(self, v, formatting): - """Return ([headers], [subrows]) for the given collection.""" - # E.g., the 'Slow Queries' list. - headers = [] - for record in v: - for k3 in record: - format = formatting.get(k3, missing) - if format is None: - # Don't output this column. - continue - if k3 not in headers: - headers.append(k3) - headers.sort() - - subrows = [] - for record in v: - subrow = [] - for k3 in headers: - v3 = record.get(k3, '') - format = formatting.get(k3, missing) - if format is None: - # Don't output this column. - continue - if hasattr(format, '__call__'): - v3 = format(v3) - elif format is not missing: - v3 = format % v3 - subrow.append(v3) - subrows.append(subrow) - - return headers, subrows - - if json is not None: - @cherrypy.expose - def data(self): - s = extrapolate_statistics(logging.statistics) - cherrypy.response.headers['Content-Type'] = 'application/json' - return json.dumps(s, sort_keys=True, indent=4) - - @cherrypy.expose - def pause(self, namespace): - logging.statistics.get(namespace, {})['Enabled'] = False - raise cherrypy.HTTPRedirect('./') - pause.cp_config = {'tools.allow.on': True, - 'tools.allow.methods': ['POST']} - - @cherrypy.expose - def resume(self, namespace): - logging.statistics.get(namespace, {})['Enabled'] = True - raise cherrypy.HTTPRedirect('./') - resume.cp_config = {'tools.allow.on': True, - 'tools.allow.methods': ['POST']} diff --git a/libraries/cherrypy/lib/cptools.py b/libraries/cherrypy/lib/cptools.py deleted file mode 100644 index 1c079634..00000000 --- a/libraries/cherrypy/lib/cptools.py +++ /dev/null @@ -1,640 +0,0 @@ -"""Functions for builtin CherryPy tools.""" - -import logging -import re -from hashlib import md5 - -import six -from six.moves import urllib - -import cherrypy -from cherrypy._cpcompat import text_or_bytes -from cherrypy.lib import httputil as _httputil -from cherrypy.lib import is_iterator - - -# Conditional HTTP request support # - -def validate_etags(autotags=False, debug=False): - """Validate the current ETag against If-Match, If-None-Match headers. - - If autotags is True, an ETag response-header value will be provided - from an MD5 hash of the response body (unless some other code has - already provided an ETag header). If False (the default), the ETag - will not be automatic. - - WARNING: the autotags feature is not designed for URL's which allow - methods other than GET. For example, if a POST to the same URL returns - no content, the automatic ETag will be incorrect, breaking a fundamental - use for entity tags in a possibly destructive fashion. Likewise, if you - raise 304 Not Modified, the response body will be empty, the ETag hash - will be incorrect, and your application will break. - See :rfc:`2616` Section 14.24. - """ - response = cherrypy.serving.response - - # Guard against being run twice. - if hasattr(response, 'ETag'): - return - - status, reason, msg = _httputil.valid_status(response.status) - - etag = response.headers.get('ETag') - - # Automatic ETag generation. See warning in docstring. - if etag: - if debug: - cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') - elif not autotags: - if debug: - cherrypy.log('Autotags off', 'TOOLS.ETAGS') - elif status != 200: - if debug: - cherrypy.log('Status not 200', 'TOOLS.ETAGS') - else: - etag = response.collapse_body() - etag = '"%s"' % md5(etag).hexdigest() - if debug: - cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') - response.headers['ETag'] = etag - - response.ETag = etag - - # "If the request would, without the If-Match header field, result in - # anything other than a 2xx or 412 status, then the If-Match header - # MUST be ignored." - if debug: - cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') - if status >= 200 and status <= 299: - request = cherrypy.serving.request - - conditions = request.headers.elements('If-Match') or [] - conditions = [str(x) for x in conditions] - if debug: - cherrypy.log('If-Match conditions: %s' % repr(conditions), - 'TOOLS.ETAGS') - if conditions and not (conditions == ['*'] or etag in conditions): - raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did ' - 'not match %r' % (etag, conditions)) - - conditions = request.headers.elements('If-None-Match') or [] - conditions = [str(x) for x in conditions] - if debug: - cherrypy.log('If-None-Match conditions: %s' % repr(conditions), - 'TOOLS.ETAGS') - if conditions == ['*'] or etag in conditions: - if debug: - cherrypy.log('request.method: %s' % - request.method, 'TOOLS.ETAGS') - if request.method in ('GET', 'HEAD'): - raise cherrypy.HTTPRedirect([], 304) - else: - raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r ' - 'matched %r' % (etag, conditions)) - - -def validate_since(): - """Validate the current Last-Modified against If-Modified-Since headers. - - If no code has set the Last-Modified response header, then no validation - will be performed. - """ - response = cherrypy.serving.response - lastmod = response.headers.get('Last-Modified') - if lastmod: - status, reason, msg = _httputil.valid_status(response.status) - - request = cherrypy.serving.request - - since = request.headers.get('If-Unmodified-Since') - if since and since != lastmod: - if (status >= 200 and status <= 299) or status == 412: - raise cherrypy.HTTPError(412) - - since = request.headers.get('If-Modified-Since') - if since and since == lastmod: - if (status >= 200 and status <= 299) or status == 304: - if request.method in ('GET', 'HEAD'): - raise cherrypy.HTTPRedirect([], 304) - else: - raise cherrypy.HTTPError(412) - - -# Tool code # - -def allow(methods=None, debug=False): - """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). - - The given methods are case-insensitive, and may be in any order. - If only one method is allowed, you may supply a single string; - if more than one, supply a list of strings. - - Regardless of whether the current method is allowed or not, this - also emits an 'Allow' response header, containing the given methods. - """ - if not isinstance(methods, (tuple, list)): - methods = [methods] - methods = [m.upper() for m in methods if m] - if not methods: - methods = ['GET', 'HEAD'] - elif 'GET' in methods and 'HEAD' not in methods: - methods.append('HEAD') - - cherrypy.response.headers['Allow'] = ', '.join(methods) - if cherrypy.request.method not in methods: - if debug: - cherrypy.log('request.method %r not in methods %r' % - (cherrypy.request.method, methods), 'TOOLS.ALLOW') - raise cherrypy.HTTPError(405) - else: - if debug: - cherrypy.log('request.method %r in methods %r' % - (cherrypy.request.method, methods), 'TOOLS.ALLOW') - - -def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', - scheme='X-Forwarded-Proto', debug=False): - """Change the base URL (scheme://host[:port][/path]). - - For running a CP server behind Apache, lighttpd, or other HTTP server. - - For Apache and lighttpd, you should leave the 'local' argument at the - default value of 'X-Forwarded-Host'. For Squid, you probably want to set - tools.proxy.local = 'Origin'. - - If you want the new request.base to include path info (not just the host), - you must explicitly set base to the full base path, and ALSO set 'local' - to '', so that the X-Forwarded-Host request header (which never includes - path info) does not override it. Regardless, the value for 'base' MUST - NOT end in a slash. - - cherrypy.request.remote.ip (the IP address of the client) will be - rewritten if the header specified by the 'remote' arg is valid. - By default, 'remote' is set to 'X-Forwarded-For'. If you do not - want to rewrite remote.ip, set the 'remote' arg to an empty string. - """ - - request = cherrypy.serving.request - - if scheme: - s = request.headers.get(scheme, None) - if debug: - cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') - if s == 'on' and 'ssl' in scheme.lower(): - # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header - scheme = 'https' - else: - # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' - scheme = s - if not scheme: - scheme = request.base[:request.base.find('://')] - - if local: - lbase = request.headers.get(local, None) - if debug: - cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') - if lbase is not None: - base = lbase.split(',')[0] - if not base: - default = urllib.parse.urlparse(request.base).netloc - base = request.headers.get('Host', default) - - if base.find('://') == -1: - # add http:// or https:// if needed - base = scheme + '://' + base - - request.base = base - - if remote: - xff = request.headers.get(remote) - if debug: - cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') - if xff: - if remote == 'X-Forwarded-For': - # Grab the first IP in a comma-separated list. Ref #1268. - xff = next(ip.strip() for ip in xff.split(',')) - request.remote.ip = xff - - -def ignore_headers(headers=('Range',), debug=False): - """Delete request headers whose field names are included in 'headers'. - - This is a useful tool for working behind certain HTTP servers; - for example, Apache duplicates the work that CP does for 'Range' - headers, and will doubly-truncate the response. - """ - request = cherrypy.serving.request - for name in headers: - if name in request.headers: - if debug: - cherrypy.log('Ignoring request header %r' % name, - 'TOOLS.IGNORE_HEADERS') - del request.headers[name] - - -def response_headers(headers=None, debug=False): - """Set headers on the response.""" - if debug: - cherrypy.log('Setting response headers: %s' % repr(headers), - 'TOOLS.RESPONSE_HEADERS') - for name, value in (headers or []): - cherrypy.serving.response.headers[name] = value - - -response_headers.failsafe = True - - -def referer(pattern, accept=True, accept_missing=False, error=403, - message='Forbidden Referer header.', debug=False): - """Raise HTTPError if Referer header does/does not match the given pattern. - - pattern - A regular expression pattern to test against the Referer. - - accept - If True, the Referer must match the pattern; if False, - the Referer must NOT match the pattern. - - accept_missing - If True, permit requests with no Referer header. - - error - The HTTP error code to return to the client on failure. - - message - A string to include in the response body on failure. - - """ - try: - ref = cherrypy.serving.request.headers['Referer'] - match = bool(re.match(pattern, ref)) - if debug: - cherrypy.log('Referer %r matches %r' % (ref, pattern), - 'TOOLS.REFERER') - if accept == match: - return - except KeyError: - if debug: - cherrypy.log('No Referer header', 'TOOLS.REFERER') - if accept_missing: - return - - raise cherrypy.HTTPError(error, message) - - -class SessionAuth(object): - - """Assert that the user is logged in.""" - - session_key = 'username' - debug = False - - def check_username_and_password(self, username, password): - pass - - def anonymous(self): - """Provide a temporary user name for anonymous users.""" - pass - - def on_login(self, username): - pass - - def on_logout(self, username): - pass - - def on_check(self, username): - pass - - def login_screen(self, from_page='..', username='', error_msg='', - **kwargs): - return (six.text_type("""<html><body> -Message: %(error_msg)s -<form method="post" action="do_login"> - Login: <input type="text" name="username" value="%(username)s" size="10" /> - <br /> - Password: <input type="password" name="password" size="10" /> - <br /> - <input type="hidden" name="from_page" value="%(from_page)s" /> - <br /> - <input type="submit" /> -</form> -</body></html>""") % vars()).encode('utf-8') - - def do_login(self, username, password, from_page='..', **kwargs): - """Login. May raise redirect, or return True if request handled.""" - response = cherrypy.serving.response - error_msg = self.check_username_and_password(username, password) - if error_msg: - body = self.login_screen(from_page, username, error_msg) - response.body = body - if 'Content-Length' in response.headers: - # Delete Content-Length header so finalize() recalcs it. - del response.headers['Content-Length'] - return True - else: - cherrypy.serving.request.login = username - cherrypy.session[self.session_key] = username - self.on_login(username) - raise cherrypy.HTTPRedirect(from_page or '/') - - def do_logout(self, from_page='..', **kwargs): - """Logout. May raise redirect, or return True if request handled.""" - sess = cherrypy.session - username = sess.get(self.session_key) - sess[self.session_key] = None - if username: - cherrypy.serving.request.login = None - self.on_logout(username) - raise cherrypy.HTTPRedirect(from_page) - - def do_check(self): - """Assert username. Raise redirect, or return True if request handled. - """ - sess = cherrypy.session - request = cherrypy.serving.request - response = cherrypy.serving.response - - username = sess.get(self.session_key) - if not username: - sess[self.session_key] = username = self.anonymous() - self._debug_message('No session[username], trying anonymous') - if not username: - url = cherrypy.url(qs=request.query_string) - self._debug_message( - 'No username, routing to login_screen with from_page %(url)r', - locals(), - ) - response.body = self.login_screen(url) - if 'Content-Length' in response.headers: - # Delete Content-Length header so finalize() recalcs it. - del response.headers['Content-Length'] - return True - self._debug_message('Setting request.login to %(username)r', locals()) - request.login = username - self.on_check(username) - - def _debug_message(self, template, context={}): - if not self.debug: - return - cherrypy.log(template % context, 'TOOLS.SESSAUTH') - - def run(self): - request = cherrypy.serving.request - response = cherrypy.serving.response - - path = request.path_info - if path.endswith('login_screen'): - self._debug_message('routing %(path)r to login_screen', locals()) - response.body = self.login_screen() - return True - elif path.endswith('do_login'): - if request.method != 'POST': - response.headers['Allow'] = 'POST' - self._debug_message('do_login requires POST') - raise cherrypy.HTTPError(405) - self._debug_message('routing %(path)r to do_login', locals()) - return self.do_login(**request.params) - elif path.endswith('do_logout'): - if request.method != 'POST': - response.headers['Allow'] = 'POST' - raise cherrypy.HTTPError(405) - self._debug_message('routing %(path)r to do_logout', locals()) - return self.do_logout(**request.params) - else: - self._debug_message('No special path, running do_check') - return self.do_check() - - -def session_auth(**kwargs): - sa = SessionAuth() - for k, v in kwargs.items(): - setattr(sa, k, v) - return sa.run() - - -session_auth.__doc__ = ( - """Session authentication hook. - - Any attribute of the SessionAuth class may be overridden via a keyword arg - to this function: - - """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__) - for k in dir(SessionAuth) if not k.startswith('__')]) -) - - -def log_traceback(severity=logging.ERROR, debug=False): - """Write the last error's traceback to the cherrypy error log.""" - cherrypy.log('', 'HTTP', severity=severity, traceback=True) - - -def log_request_headers(debug=False): - """Write request headers to the cherrypy error log.""" - h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list] - cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP') - - -def log_hooks(debug=False): - """Write request.hooks to the cherrypy error log.""" - request = cherrypy.serving.request - - msg = [] - # Sort by the standard points if possible. - from cherrypy import _cprequest - points = _cprequest.hookpoints - for k in request.hooks.keys(): - if k not in points: - points.append(k) - - for k in points: - msg.append(' %s:' % k) - v = request.hooks.get(k, []) - v.sort() - for h in v: - msg.append(' %r' % h) - cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + - ':\n' + '\n'.join(msg), 'HTTP') - - -def redirect(url='', internal=True, debug=False): - """Raise InternalRedirect or HTTPRedirect to the given url.""" - if debug: - cherrypy.log('Redirecting %sto: %s' % - ({True: 'internal ', False: ''}[internal], url), - 'TOOLS.REDIRECT') - if internal: - raise cherrypy.InternalRedirect(url) - else: - raise cherrypy.HTTPRedirect(url) - - -def trailing_slash(missing=True, extra=False, status=None, debug=False): - """Redirect if path_info has (missing|extra) trailing slash.""" - request = cherrypy.serving.request - pi = request.path_info - - if debug: - cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % - (request.is_index, missing, extra, pi), - 'TOOLS.TRAILING_SLASH') - if request.is_index is True: - if missing: - if not pi.endswith('/'): - new_url = cherrypy.url(pi + '/', request.query_string) - raise cherrypy.HTTPRedirect(new_url, status=status or 301) - elif request.is_index is False: - if extra: - # If pi == '/', don't redirect to ''! - if pi.endswith('/') and pi != '/': - new_url = cherrypy.url(pi[:-1], request.query_string) - raise cherrypy.HTTPRedirect(new_url, status=status or 301) - - -def flatten(debug=False): - """Wrap response.body in a generator that recursively iterates over body. - - This allows cherrypy.response.body to consist of 'nested generators'; - that is, a set of generators that yield generators. - """ - def flattener(input): - numchunks = 0 - for x in input: - if not is_iterator(x): - numchunks += 1 - yield x - else: - for y in flattener(x): - numchunks += 1 - yield y - if debug: - cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') - response = cherrypy.serving.response - response.body = flattener(response.body) - - -def accept(media=None, debug=False): - """Return the client's preferred media-type (from the given Content-Types). - - If 'media' is None (the default), no test will be performed. - - If 'media' is provided, it should be the Content-Type value (as a string) - or values (as a list or tuple of strings) which the current resource - can emit. The client's acceptable media ranges (as declared in the - Accept request header) will be matched in order to these Content-Type - values; the first such string is returned. That is, the return value - will always be one of the strings provided in the 'media' arg (or None - if 'media' is None). - - If no match is found, then HTTPError 406 (Not Acceptable) is raised. - Note that most web browsers send */* as a (low-quality) acceptable - media range, which should match any Content-Type. In addition, "...if - no Accept header field is present, then it is assumed that the client - accepts all media types." - - Matching types are checked in order of client preference first, - and then in the order of the given 'media' values. - - Note that this function does not honor accept-params (other than "q"). - """ - if not media: - return - if isinstance(media, text_or_bytes): - media = [media] - request = cherrypy.serving.request - - # Parse the Accept request header, and try to match one - # of the requested media-ranges (in order of preference). - ranges = request.headers.elements('Accept') - if not ranges: - # Any media type is acceptable. - if debug: - cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') - return media[0] - else: - # Note that 'ranges' is sorted in order of preference - for element in ranges: - if element.qvalue > 0: - if element.value == '*/*': - # Matches any type or subtype - if debug: - cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') - return media[0] - elif element.value.endswith('/*'): - # Matches any subtype - mtype = element.value[:-1] # Keep the slash - for m in media: - if m.startswith(mtype): - if debug: - cherrypy.log('Match due to %s' % element.value, - 'TOOLS.ACCEPT') - return m - else: - # Matches exact value - if element.value in media: - if debug: - cherrypy.log('Match due to %s' % element.value, - 'TOOLS.ACCEPT') - return element.value - - # No suitable media-range found. - ah = request.headers.get('Accept') - if ah is None: - msg = 'Your client did not send an Accept header.' - else: - msg = 'Your client sent this Accept header: %s.' % ah - msg += (' But this resource only emits these media types: %s.' % - ', '.join(media)) - raise cherrypy.HTTPError(406, msg) - - -class MonitoredHeaderMap(_httputil.HeaderMap): - - def transform_key(self, key): - self.accessed_headers.add(key) - return super(MonitoredHeaderMap, self).transform_key(key) - - def __init__(self): - self.accessed_headers = set() - super(MonitoredHeaderMap, self).__init__() - - -def autovary(ignore=None, debug=False): - """Auto-populate the Vary response header based on request.header access. - """ - request = cherrypy.serving.request - - req_h = request.headers - request.headers = MonitoredHeaderMap() - request.headers.update(req_h) - if ignore is None: - ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) - - def set_response_header(): - resp_h = cherrypy.serving.response.headers - v = set([e.value for e in resp_h.elements('Vary')]) - if debug: - cherrypy.log( - 'Accessed headers: %s' % request.headers.accessed_headers, - 'TOOLS.AUTOVARY') - v = v.union(request.headers.accessed_headers) - v = v.difference(ignore) - v = list(v) - v.sort() - resp_h['Vary'] = ', '.join(v) - request.hooks.attach('before_finalize', set_response_header, 95) - - -def convert_params(exception=ValueError, error=400): - """Convert request params based on function annotations, with error handling. - - exception - Exception class to catch. - - status - The HTTP error code to return to the client on failure. - """ - request = cherrypy.serving.request - types = request.handler.callable.__annotations__ - with cherrypy.HTTPError.handle(exception, error): - for key in set(types).intersection(request.params): - request.params[key] = types[key](request.params[key]) diff --git a/libraries/cherrypy/lib/encoding.py b/libraries/cherrypy/lib/encoding.py deleted file mode 100644 index 3d001ca6..00000000 --- a/libraries/cherrypy/lib/encoding.py +++ /dev/null @@ -1,436 +0,0 @@ -import struct -import time -import io - -import six - -import cherrypy -from cherrypy._cpcompat import text_or_bytes -from cherrypy.lib import file_generator -from cherrypy.lib import is_closable_iterator -from cherrypy.lib import set_vary_header - - -def decode(encoding=None, default_encoding='utf-8'): - """Replace or extend the list of charsets used to decode a request entity. - - Either argument may be a single string or a list of strings. - - encoding - If not None, restricts the set of charsets attempted while decoding - a request entity to the given set (even if a different charset is - given in the Content-Type request header). - - default_encoding - Only in effect if the 'encoding' argument is not given. - If given, the set of charsets attempted while decoding a request - entity is *extended* with the given value(s). - - """ - body = cherrypy.request.body - if encoding is not None: - if not isinstance(encoding, list): - encoding = [encoding] - body.attempt_charsets = encoding - elif default_encoding: - if not isinstance(default_encoding, list): - default_encoding = [default_encoding] - body.attempt_charsets = body.attempt_charsets + default_encoding - - -class UTF8StreamEncoder: - def __init__(self, iterator): - self._iterator = iterator - - def __iter__(self): - return self - - def next(self): - return self.__next__() - - def __next__(self): - res = next(self._iterator) - if isinstance(res, six.text_type): - res = res.encode('utf-8') - return res - - def close(self): - if is_closable_iterator(self._iterator): - self._iterator.close() - - def __getattr__(self, attr): - if attr.startswith('__'): - raise AttributeError(self, attr) - return getattr(self._iterator, attr) - - -class ResponseEncoder: - - default_encoding = 'utf-8' - failmsg = 'Response body could not be encoded with %r.' - encoding = None - errors = 'strict' - text_only = True - add_charset = True - debug = False - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - - self.attempted_charsets = set() - request = cherrypy.serving.request - if request.handler is not None: - # Replace request.handler with self - if self.debug: - cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') - self.oldhandler = request.handler - request.handler = self - - def encode_stream(self, encoding): - """Encode a streaming response body. - - Use a generator wrapper, and just pray it works as the stream is - being written out. - """ - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - - def encoder(body): - for chunk in body: - if isinstance(chunk, six.text_type): - chunk = chunk.encode(encoding, self.errors) - yield chunk - self.body = encoder(self.body) - return True - - def encode_string(self, encoding): - """Encode a buffered response body.""" - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - body = [] - for chunk in self.body: - if isinstance(chunk, six.text_type): - try: - chunk = chunk.encode(encoding, self.errors) - except (LookupError, UnicodeError): - return False - body.append(chunk) - self.body = body - return True - - def find_acceptable_charset(self): - request = cherrypy.serving.request - response = cherrypy.serving.response - - if self.debug: - cherrypy.log('response.stream %r' % - response.stream, 'TOOLS.ENCODE') - if response.stream: - encoder = self.encode_stream - else: - encoder = self.encode_string - if 'Content-Length' in response.headers: - # Delete Content-Length header so finalize() recalcs it. - # Encoded strings may be of different lengths from their - # unicode equivalents, and even from each other. For example: - # >>> t = u"\u7007\u3040" - # >>> len(t) - # 2 - # >>> len(t.encode("UTF-8")) - # 6 - # >>> len(t.encode("utf7")) - # 8 - del response.headers['Content-Length'] - - # Parse the Accept-Charset request header, and try to provide one - # of the requested charsets (in order of user preference). - encs = request.headers.elements('Accept-Charset') - charsets = [enc.value.lower() for enc in encs] - if self.debug: - cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') - - if self.encoding is not None: - # If specified, force this encoding to be used, or fail. - encoding = self.encoding.lower() - if self.debug: - cherrypy.log('Specified encoding %r' % - encoding, 'TOOLS.ENCODE') - if (not charsets) or '*' in charsets or encoding in charsets: - if self.debug: - cherrypy.log('Attempting encoding %r' % - encoding, 'TOOLS.ENCODE') - if encoder(encoding): - return encoding - else: - if not encs: - if self.debug: - cherrypy.log('Attempting default encoding %r' % - self.default_encoding, 'TOOLS.ENCODE') - # Any character-set is acceptable. - if encoder(self.default_encoding): - return self.default_encoding - else: - raise cherrypy.HTTPError(500, self.failmsg % - self.default_encoding) - else: - for element in encs: - if element.qvalue > 0: - if element.value == '*': - # Matches any charset. Try our default. - if self.debug: - cherrypy.log('Attempting default encoding due ' - 'to %r' % element, 'TOOLS.ENCODE') - if encoder(self.default_encoding): - return self.default_encoding - else: - encoding = element.value - if self.debug: - cherrypy.log('Attempting encoding %s (qvalue >' - '0)' % element, 'TOOLS.ENCODE') - if encoder(encoding): - return encoding - - if '*' not in charsets: - # If no "*" is present in an Accept-Charset field, then all - # character sets not explicitly mentioned get a quality - # value of 0, except for ISO-8859-1, which gets a quality - # value of 1 if not explicitly mentioned. - iso = 'iso-8859-1' - if iso not in charsets: - if self.debug: - cherrypy.log('Attempting ISO-8859-1 encoding', - 'TOOLS.ENCODE') - if encoder(iso): - return iso - - # No suitable encoding found. - ac = request.headers.get('Accept-Charset') - if ac is None: - msg = 'Your client did not send an Accept-Charset header.' - else: - msg = 'Your client sent this Accept-Charset header: %s.' % ac - _charsets = ', '.join(sorted(self.attempted_charsets)) - msg += ' We tried these charsets: %s.' % (_charsets,) - raise cherrypy.HTTPError(406, msg) - - def __call__(self, *args, **kwargs): - response = cherrypy.serving.response - self.body = self.oldhandler(*args, **kwargs) - - self.body = prepare_iter(self.body) - - ct = response.headers.elements('Content-Type') - if self.debug: - cherrypy.log('Content-Type: %r' % [str(h) - for h in ct], 'TOOLS.ENCODE') - if ct and self.add_charset: - ct = ct[0] - if self.text_only: - if ct.value.lower().startswith('text/'): - if self.debug: - cherrypy.log( - 'Content-Type %s starts with "text/"' % ct, - 'TOOLS.ENCODE') - do_find = True - else: - if self.debug: - cherrypy.log('Not finding because Content-Type %s ' - 'does not start with "text/"' % ct, - 'TOOLS.ENCODE') - do_find = False - else: - if self.debug: - cherrypy.log('Finding because not text_only', - 'TOOLS.ENCODE') - do_find = True - - if do_find: - # Set "charset=..." param on response Content-Type header - ct.params['charset'] = self.find_acceptable_charset() - if self.debug: - cherrypy.log('Setting Content-Type %s' % ct, - 'TOOLS.ENCODE') - response.headers['Content-Type'] = str(ct) - - return self.body - - -def prepare_iter(value): - """ - Ensure response body is iterable and resolves to False when empty. - """ - if isinstance(value, text_or_bytes): - # strings get wrapped in a list because iterating over a single - # item list is much faster than iterating over every character - # in a long string. - if value: - value = [value] - else: - # [''] doesn't evaluate to False, so replace it with []. - value = [] - # Don't use isinstance here; io.IOBase which has an ABC takes - # 1000 times as long as, say, isinstance(value, str) - elif hasattr(value, 'read'): - value = file_generator(value) - elif value is None: - value = [] - return value - - -# GZIP - - -def compress(body, compress_level): - """Compress 'body' at the given compress_level.""" - import zlib - - # See http://www.gzip.org/zlib/rfc-gzip.html - yield b'\x1f\x8b' # ID1 and ID2: gzip marker - yield b'\x08' # CM: compression method - yield b'\x00' # FLG: none set - # MTIME: 4 bytes - yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16)) - yield b'\x02' # XFL: max compression, slowest algo - yield b'\xff' # OS: unknown - - crc = zlib.crc32(b'') - size = 0 - zobj = zlib.compressobj(compress_level, - zlib.DEFLATED, -zlib.MAX_WBITS, - zlib.DEF_MEM_LEVEL, 0) - for line in body: - size += len(line) - crc = zlib.crc32(line, crc) - yield zobj.compress(line) - yield zobj.flush() - - # CRC32: 4 bytes - yield struct.pack('<L', crc & int('FFFFFFFF', 16)) - # ISIZE: 4 bytes - yield struct.pack('<L', size & int('FFFFFFFF', 16)) - - -def decompress(body): - import gzip - - zbuf = io.BytesIO() - zbuf.write(body) - zbuf.seek(0) - zfile = gzip.GzipFile(mode='rb', fileobj=zbuf) - data = zfile.read() - zfile.close() - return data - - -def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], - debug=False): - """Try to gzip the response body if Content-Type in mime_types. - - cherrypy.response.headers['Content-Type'] must be set to one of the - values in the mime_types arg before calling this function. - - The provided list of mime-types must be of one of the following form: - * `type/subtype` - * `type/*` - * `type/*+subtype` - - No compression is performed if any of the following hold: - * The client sends no Accept-Encoding request header - * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header - * No 'gzip' or 'x-gzip' with a qvalue > 0 is present - * The 'identity' value is given with a qvalue > 0. - - """ - request = cherrypy.serving.request - response = cherrypy.serving.response - - set_vary_header(response, 'Accept-Encoding') - - if not response.body: - # Response body is empty (might be a 304 for instance) - if debug: - cherrypy.log('No response body', context='TOOLS.GZIP') - return - - # If returning cached content (which should already have been gzipped), - # don't re-zip. - if getattr(request, 'cached', False): - if debug: - cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') - return - - acceptable = request.headers.elements('Accept-Encoding') - if not acceptable: - # If no Accept-Encoding field is present in a request, - # the server MAY assume that the client will accept any - # content coding. In this case, if "identity" is one of - # the available content-codings, then the server SHOULD use - # the "identity" content-coding, unless it has additional - # information that a different content-coding is meaningful - # to the client. - if debug: - cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') - return - - ct = response.headers.get('Content-Type', '').split(';')[0] - for coding in acceptable: - if coding.value == 'identity' and coding.qvalue != 0: - if debug: - cherrypy.log('Non-zero identity qvalue: %s' % coding, - context='TOOLS.GZIP') - return - if coding.value in ('gzip', 'x-gzip'): - if coding.qvalue == 0: - if debug: - cherrypy.log('Zero gzip qvalue: %s' % coding, - context='TOOLS.GZIP') - return - - if ct not in mime_types: - # If the list of provided mime-types contains tokens - # such as 'text/*' or 'application/*+xml', - # we go through them and find the most appropriate one - # based on the given content-type. - # The pattern matching is only caring about the most - # common cases, as stated above, and doesn't support - # for extra parameters. - found = False - if '/' in ct: - ct_media_type, ct_sub_type = ct.split('/') - for mime_type in mime_types: - if '/' in mime_type: - media_type, sub_type = mime_type.split('/') - if ct_media_type == media_type: - if sub_type == '*': - found = True - break - elif '+' in sub_type and '+' in ct_sub_type: - ct_left, ct_right = ct_sub_type.split('+') - left, right = sub_type.split('+') - if left == '*' and ct_right == right: - found = True - break - - if not found: - if debug: - cherrypy.log('Content-Type %s not in mime_types %r' % - (ct, mime_types), context='TOOLS.GZIP') - return - - if debug: - cherrypy.log('Gzipping', context='TOOLS.GZIP') - # Return a generator that compresses the page - response.headers['Content-Encoding'] = 'gzip' - response.body = compress(response.body, compress_level) - if 'Content-Length' in response.headers: - # Delete Content-Length header so finalize() recalcs it. - del response.headers['Content-Length'] - - return - - if debug: - cherrypy.log('No acceptable encoding found.', context='GZIP') - cherrypy.HTTPError(406, 'identity, gzip').set_response() diff --git a/libraries/cherrypy/lib/gctools.py b/libraries/cherrypy/lib/gctools.py deleted file mode 100644 index 26746d78..00000000 --- a/libraries/cherrypy/lib/gctools.py +++ /dev/null @@ -1,218 +0,0 @@ -import gc -import inspect -import sys -import time - -try: - import objgraph -except ImportError: - objgraph = None - -import cherrypy -from cherrypy import _cprequest, _cpwsgi -from cherrypy.process.plugins import SimplePlugin - - -class ReferrerTree(object): - - """An object which gathers all referrers of an object to a given depth.""" - - peek_length = 40 - - def __init__(self, ignore=None, maxdepth=2, maxparents=10): - self.ignore = ignore or [] - self.ignore.append(inspect.currentframe().f_back) - self.maxdepth = maxdepth - self.maxparents = maxparents - - def ascend(self, obj, depth=1): - """Return a nested list containing referrers of the given object.""" - depth += 1 - parents = [] - - # Gather all referrers in one step to minimize - # cascading references due to repr() logic. - refs = gc.get_referrers(obj) - self.ignore.append(refs) - if len(refs) > self.maxparents: - return [('[%s referrers]' % len(refs), [])] - - try: - ascendcode = self.ascend.__code__ - except AttributeError: - ascendcode = self.ascend.im_func.func_code - for parent in refs: - if inspect.isframe(parent) and parent.f_code is ascendcode: - continue - if parent in self.ignore: - continue - if depth <= self.maxdepth: - parents.append((parent, self.ascend(parent, depth))) - else: - parents.append((parent, [])) - - return parents - - def peek(self, s): - """Return s, restricted to a sane length.""" - if len(s) > (self.peek_length + 3): - half = self.peek_length // 2 - return s[:half] + '...' + s[-half:] - else: - return s - - def _format(self, obj, descend=True): - """Return a string representation of a single object.""" - if inspect.isframe(obj): - filename, lineno, func, context, index = inspect.getframeinfo(obj) - return "<frame of function '%s'>" % func - - if not descend: - return self.peek(repr(obj)) - - if isinstance(obj, dict): - return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False), - self._format(v, descend=False)) - for k, v in obj.items()]) + '}' - elif isinstance(obj, list): - return '[' + ', '.join([self._format(item, descend=False) - for item in obj]) + ']' - elif isinstance(obj, tuple): - return '(' + ', '.join([self._format(item, descend=False) - for item in obj]) + ')' - - r = self.peek(repr(obj)) - if isinstance(obj, (str, int, float)): - return r - return '%s: %s' % (type(obj), r) - - def format(self, tree): - """Return a list of string reprs from a nested list of referrers.""" - output = [] - - def ascend(branch, depth=1): - for parent, grandparents in branch: - output.append((' ' * depth) + self._format(parent)) - if grandparents: - ascend(grandparents, depth + 1) - ascend(tree) - return output - - -def get_instances(cls): - return [x for x in gc.get_objects() if isinstance(x, cls)] - - -class RequestCounter(SimplePlugin): - - def start(self): - self.count = 0 - - def before_request(self): - self.count += 1 - - def after_request(self): - self.count -= 1 - - -request_counter = RequestCounter(cherrypy.engine) -request_counter.subscribe() - - -def get_context(obj): - if isinstance(obj, _cprequest.Request): - return 'path=%s;stage=%s' % (obj.path_info, obj.stage) - elif isinstance(obj, _cprequest.Response): - return 'status=%s' % obj.status - elif isinstance(obj, _cpwsgi.AppResponse): - return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '') - elif hasattr(obj, 'tb_lineno'): - return 'tb_lineno=%s' % obj.tb_lineno - return '' - - -class GCRoot(object): - - """A CherryPy page handler for testing reference leaks.""" - - classes = [ - (_cprequest.Request, 2, 2, - 'Should be 1 in this request thread and 1 in the main thread.'), - (_cprequest.Response, 2, 2, - 'Should be 1 in this request thread and 1 in the main thread.'), - (_cpwsgi.AppResponse, 1, 1, - 'Should be 1 in this request thread only.'), - ] - - @cherrypy.expose - def index(self): - return 'Hello, world!' - - @cherrypy.expose - def stats(self): - output = ['Statistics:'] - - for trial in range(10): - if request_counter.count > 0: - break - time.sleep(0.5) - else: - output.append('\nNot all requests closed properly.') - - # gc_collect isn't perfectly synchronous, because it may - # break reference cycles that then take time to fully - # finalize. Call it thrice and hope for the best. - gc.collect() - gc.collect() - unreachable = gc.collect() - if unreachable: - if objgraph is not None: - final = objgraph.by_type('Nondestructible') - if final: - objgraph.show_backrefs(final, filename='finalizers.png') - - trash = {} - for x in gc.garbage: - trash[type(x)] = trash.get(type(x), 0) + 1 - if trash: - output.insert(0, '\n%s unreachable objects:' % unreachable) - trash = [(v, k) for k, v in trash.items()] - trash.sort() - for pair in trash: - output.append(' ' + repr(pair)) - - # Check declared classes to verify uncollected instances. - # These don't have to be part of a cycle; they can be - # any objects that have unanticipated referrers that keep - # them from being collected. - allobjs = {} - for cls, minobj, maxobj, msg in self.classes: - allobjs[cls] = get_instances(cls) - - for cls, minobj, maxobj, msg in self.classes: - objs = allobjs[cls] - lenobj = len(objs) - if lenobj < minobj or lenobj > maxobj: - if minobj == maxobj: - output.append( - '\nExpected %s %r references, got %s.' % - (minobj, cls, lenobj)) - else: - output.append( - '\nExpected %s to %s %r references, got %s.' % - (minobj, maxobj, cls, lenobj)) - - for obj in objs: - if objgraph is not None: - ig = [id(objs), id(inspect.currentframe())] - fname = 'graph_%s_%s.png' % (cls.__name__, id(obj)) - objgraph.show_backrefs( - obj, extra_ignore=ig, max_depth=4, too_many=20, - filename=fname, extra_info=get_context) - output.append('\nReferrers for %s (refcount=%s):' % - (repr(obj), sys.getrefcount(obj))) - t = ReferrerTree(ignore=[objs], maxdepth=3) - tree = t.ascend(obj) - output.extend(t.format(tree)) - - return '\n'.join(output) diff --git a/libraries/cherrypy/lib/httputil.py b/libraries/cherrypy/lib/httputil.py deleted file mode 100644 index b68d8dd5..00000000 --- a/libraries/cherrypy/lib/httputil.py +++ /dev/null @@ -1,581 +0,0 @@ -"""HTTP library functions. - -This module contains functions for building an HTTP application -framework: any one, not just one whose name starts with "Ch". ;) If you -reference any modules from some popular framework inside *this* module, -FuManChu will personally hang you up by your thumbs and submit you -to a public caning. -""" - -import functools -import email.utils -import re -from binascii import b2a_base64 -from cgi import parse_header -from email.header import decode_header - -import six -from six.moves import range, builtins, map -from six.moves.BaseHTTPServer import BaseHTTPRequestHandler - -import cherrypy -from cherrypy._cpcompat import ntob, ntou -from cherrypy._cpcompat import unquote_plus - -response_codes = BaseHTTPRequestHandler.responses.copy() - -# From https://github.com/cherrypy/cherrypy/issues/361 -response_codes[500] = ('Internal Server Error', - 'The server encountered an unexpected condition ' - 'which prevented it from fulfilling the request.') -response_codes[503] = ('Service Unavailable', - 'The server is currently unable to handle the ' - 'request due to a temporary overloading or ' - 'maintenance of the server.') - - -HTTPDate = functools.partial(email.utils.formatdate, usegmt=True) - - -def urljoin(*atoms): - r"""Return the given path \*atoms, joined into a single URL. - - This will correctly join a SCRIPT_NAME and PATH_INFO into the - original URL, even if either atom is blank. - """ - url = '/'.join([x for x in atoms if x]) - while '//' in url: - url = url.replace('//', '/') - # Special-case the final url of "", and return "/" instead. - return url or '/' - - -def urljoin_bytes(*atoms): - """Return the given path `*atoms`, joined into a single URL. - - This will correctly join a SCRIPT_NAME and PATH_INFO into the - original URL, even if either atom is blank. - """ - url = b'/'.join([x for x in atoms if x]) - while b'//' in url: - url = url.replace(b'//', b'/') - # Special-case the final url of "", and return "/" instead. - return url or b'/' - - -def protocol_from_http(protocol_str): - """Return a protocol tuple from the given 'HTTP/x.y' string.""" - return int(protocol_str[5]), int(protocol_str[7]) - - -def get_ranges(headervalue, content_length): - """Return a list of (start, stop) indices from a Range header, or None. - - Each (start, stop) tuple will be composed of two ints, which are suitable - for use in a slicing operation. That is, the header "Range: bytes=3-6", - if applied against a Python string, is requesting resource[3:7]. This - function will return the list [(3, 7)]. - - If this function returns an empty list, you should return HTTP 416. - """ - - if not headervalue: - return None - - result = [] - bytesunit, byteranges = headervalue.split('=', 1) - for brange in byteranges.split(','): - start, stop = [x.strip() for x in brange.split('-', 1)] - if start: - if not stop: - stop = content_length - 1 - start, stop = int(start), int(stop) - if start >= content_length: - # From rfc 2616 sec 14.16: - # "If the server receives a request (other than one - # including an If-Range request-header field) with an - # unsatisfiable Range request-header field (that is, - # all of whose byte-range-spec values have a first-byte-pos - # value greater than the current length of the selected - # resource), it SHOULD return a response code of 416 - # (Requested range not satisfiable)." - continue - if stop < start: - # From rfc 2616 sec 14.16: - # "If the server ignores a byte-range-spec because it - # is syntactically invalid, the server SHOULD treat - # the request as if the invalid Range header field - # did not exist. (Normally, this means return a 200 - # response containing the full entity)." - return None - result.append((start, stop + 1)) - else: - if not stop: - # See rfc quote above. - return None - # Negative subscript (last N bytes) - # - # RFC 2616 Section 14.35.1: - # If the entity is shorter than the specified suffix-length, - # the entire entity-body is used. - if int(stop) > content_length: - result.append((0, content_length)) - else: - result.append((content_length - int(stop), content_length)) - - return result - - -class HeaderElement(object): - - """An element (with parameters) from an HTTP header's element list.""" - - def __init__(self, value, params=None): - self.value = value - if params is None: - params = {} - self.params = params - - def __cmp__(self, other): - return builtins.cmp(self.value, other.value) - - def __lt__(self, other): - return self.value < other.value - - def __str__(self): - p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)] - return str('%s%s' % (self.value, ''.join(p))) - - def __bytes__(self): - return ntob(self.__str__()) - - def __unicode__(self): - return ntou(self.__str__()) - - @staticmethod - def parse(elementstr): - """Transform 'token;key=val' to ('token', {'key': 'val'}).""" - initial_value, params = parse_header(elementstr) - return initial_value, params - - @classmethod - def from_str(cls, elementstr): - """Construct an instance from a string of the form 'token;key=val'.""" - ival, params = cls.parse(elementstr) - return cls(ival, params) - - -q_separator = re.compile(r'; *q *=') - - -class AcceptElement(HeaderElement): - - """An element (with parameters) from an Accept* header's element list. - - AcceptElement objects are comparable; the more-preferred object will be - "less than" the less-preferred object. They are also therefore sortable; - if you sort a list of AcceptElement objects, they will be listed in - priority order; the most preferred value will be first. Yes, it should - have been the other way around, but it's too late to fix now. - """ - - @classmethod - def from_str(cls, elementstr): - qvalue = None - # The first "q" parameter (if any) separates the initial - # media-range parameter(s) (if any) from the accept-params. - atoms = q_separator.split(elementstr, 1) - media_range = atoms.pop(0).strip() - if atoms: - # The qvalue for an Accept header can have extensions. The other - # headers cannot, but it's easier to parse them as if they did. - qvalue = HeaderElement.from_str(atoms[0].strip()) - - media_type, params = cls.parse(media_range) - if qvalue is not None: - params['q'] = qvalue - return cls(media_type, params) - - @property - def qvalue(self): - 'The qvalue, or priority, of this value.' - val = self.params.get('q', '1') - if isinstance(val, HeaderElement): - val = val.value - try: - return float(val) - except ValueError as val_err: - """Fail client requests with invalid quality value. - - Ref: https://github.com/cherrypy/cherrypy/issues/1370 - """ - six.raise_from( - cherrypy.HTTPError( - 400, - 'Malformed HTTP header: `{}`'. - format(str(self)), - ), - val_err, - ) - - def __cmp__(self, other): - diff = builtins.cmp(self.qvalue, other.qvalue) - if diff == 0: - diff = builtins.cmp(str(self), str(other)) - return diff - - def __lt__(self, other): - if self.qvalue == other.qvalue: - return str(self) < str(other) - else: - return self.qvalue < other.qvalue - - -RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') - - -def header_elements(fieldname, fieldvalue): - """Return a sorted HeaderElement list from a comma-separated header string. - """ - if not fieldvalue: - return [] - - result = [] - for element in RE_HEADER_SPLIT.split(fieldvalue): - if fieldname.startswith('Accept') or fieldname == 'TE': - hv = AcceptElement.from_str(element) - else: - hv = HeaderElement.from_str(element) - result.append(hv) - - return list(reversed(sorted(result))) - - -def decode_TEXT(value): - r""" - Decode :rfc:`2047` TEXT - - >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') - True - """ - atoms = decode_header(value) - decodedvalue = '' - for atom, charset in atoms: - if charset is not None: - atom = atom.decode(charset) - decodedvalue += atom - return decodedvalue - - -def decode_TEXT_maybe(value): - """ - Decode the text but only if '=?' appears in it. - """ - return decode_TEXT(value) if '=?' in value else value - - -def valid_status(status): - """Return legal HTTP status Code, Reason-phrase and Message. - - The status arg must be an int, a str that begins with an int - or the constant from ``http.client`` stdlib module. - - If status has no reason-phrase is supplied, a default reason- - phrase will be provided. - - >>> from six.moves import http_client - >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler - >>> valid_status(http_client.ACCEPTED) == ( - ... int(http_client.ACCEPTED), - ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED] - True - """ - - if not status: - status = 200 - - code, reason = status, None - if isinstance(status, six.string_types): - code, _, reason = status.partition(' ') - reason = reason.strip() or None - - try: - code = int(code) - except (TypeError, ValueError): - raise ValueError('Illegal response status from server ' - '(%s is non-numeric).' % repr(code)) - - if code < 100 or code > 599: - raise ValueError('Illegal response status from server ' - '(%s is out of range).' % repr(code)) - - if code not in response_codes: - # code is unknown but not illegal - default_reason, message = '', '' - else: - default_reason, message = response_codes[code] - - if reason is None: - reason = default_reason - - return code, reason, message - - -# NOTE: the parse_qs functions that follow are modified version of those -# in the python3.0 source - we need to pass through an encoding to the unquote -# method, but the default parse_qs function doesn't allow us to. These do. - -def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): - """Parse a query given as a string argument. - - Arguments: - - qs: URL-encoded query string to be parsed - - keep_blank_values: flag indicating whether blank values in - URL encoded queries should be treated as blank strings. A - true value indicates that blanks should be retained as blank - strings. The default false value indicates that blank values - are to be ignored and treated as if they were not included. - - strict_parsing: flag indicating what to do with parsing errors. If - false (the default), errors are silently ignored. If true, - errors raise a ValueError exception. - - Returns a dict, as G-d intended. - """ - pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] - d = {} - for name_value in pairs: - if not name_value and not strict_parsing: - continue - nv = name_value.split('=', 1) - if len(nv) != 2: - if strict_parsing: - raise ValueError('bad query field: %r' % (name_value,)) - # Handle case of a control-name with no equal sign - if keep_blank_values: - nv.append('') - else: - continue - if len(nv[1]) or keep_blank_values: - name = unquote_plus(nv[0], encoding, errors='strict') - value = unquote_plus(nv[1], encoding, errors='strict') - if name in d: - if not isinstance(d[name], list): - d[name] = [d[name]] - d[name].append(value) - else: - d[name] = value - return d - - -image_map_pattern = re.compile(r'[0-9]+,[0-9]+') - - -def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): - """Build a params dictionary from a query_string. - - Duplicate key/value pairs in the provided query_string will be - returned as {'key': [val1, val2, ...]}. Single key/values will - be returned as strings: {'key': 'value'}. - """ - if image_map_pattern.match(query_string): - # Server-side image map. Map the coords to 'x' and 'y' - # (like CGI::Request does). - pm = query_string.split(',') - pm = {'x': int(pm[0]), 'y': int(pm[1])} - else: - pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) - return pm - - -#### -# Inlined from jaraco.collections 1.5.2 -# Ref #1673 -class KeyTransformingDict(dict): - """ - A dict subclass that transforms the keys before they're used. - Subclasses may override the default transform_key to customize behavior. - """ - @staticmethod - def transform_key(key): - return key - - def __init__(self, *args, **kargs): - super(KeyTransformingDict, self).__init__() - # build a dictionary using the default constructs - d = dict(*args, **kargs) - # build this dictionary using transformed keys. - for item in d.items(): - self.__setitem__(*item) - - def __setitem__(self, key, val): - key = self.transform_key(key) - super(KeyTransformingDict, self).__setitem__(key, val) - - def __getitem__(self, key): - key = self.transform_key(key) - return super(KeyTransformingDict, self).__getitem__(key) - - def __contains__(self, key): - key = self.transform_key(key) - return super(KeyTransformingDict, self).__contains__(key) - - def __delitem__(self, key): - key = self.transform_key(key) - return super(KeyTransformingDict, self).__delitem__(key) - - def get(self, key, *args, **kwargs): - key = self.transform_key(key) - return super(KeyTransformingDict, self).get(key, *args, **kwargs) - - def setdefault(self, key, *args, **kwargs): - key = self.transform_key(key) - return super(KeyTransformingDict, self).setdefault( - key, *args, **kwargs) - - def pop(self, key, *args, **kwargs): - key = self.transform_key(key) - return super(KeyTransformingDict, self).pop(key, *args, **kwargs) - - def matching_key_for(self, key): - """ - Given a key, return the actual key stored in self that matches. - Raise KeyError if the key isn't found. - """ - try: - return next(e_key for e_key in self.keys() if e_key == key) - except StopIteration: - raise KeyError(key) -#### - - -class CaseInsensitiveDict(KeyTransformingDict): - - """A case-insensitive dict subclass. - - Each key is changed on entry to str(key).title(). - """ - - @staticmethod - def transform_key(key): - return str(key).title() - - -# TEXT = <any OCTET except CTLs, but including LWS> -# -# A CRLF is allowed in the definition of TEXT only as part of a header -# field continuation. It is expected that the folding LWS will be -# replaced with a single SP before interpretation of the TEXT value." -if str == bytes: - header_translate_table = ''.join([chr(i) for i in range(256)]) - header_translate_deletechars = ''.join( - [chr(i) for i in range(32)]) + chr(127) -else: - header_translate_table = None - header_translate_deletechars = bytes(range(32)) + bytes([127]) - - -class HeaderMap(CaseInsensitiveDict): - - """A dict subclass for HTTP request and response headers. - - Each key is changed on entry to str(key).title(). This allows headers - to be case-insensitive and avoid duplicates. - - Values are header values (decoded according to :rfc:`2047` if necessary). - """ - - protocol = (1, 1) - encodings = ['ISO-8859-1'] - - # Someday, when http-bis is done, this will probably get dropped - # since few servers, clients, or intermediaries do it. But until then, - # we're going to obey the spec as is. - # "Words of *TEXT MAY contain characters from character sets other than - # ISO-8859-1 only when encoded according to the rules of RFC 2047." - use_rfc_2047 = True - - def elements(self, key): - """Return a sorted list of HeaderElements for the given header.""" - key = str(key).title() - value = self.get(key) - return header_elements(key, value) - - def values(self, key): - """Return a sorted list of HeaderElement.value for the given header.""" - return [e.value for e in self.elements(key)] - - def output(self): - """Transform self into a list of (name, value) tuples.""" - return list(self.encode_header_items(self.items())) - - @classmethod - def encode_header_items(cls, header_items): - """ - Prepare the sequence of name, value tuples into a form suitable for - transmitting on the wire for HTTP. - """ - for k, v in header_items: - if not isinstance(v, six.string_types): - v = six.text_type(v) - - yield tuple(map(cls.encode_header_item, (k, v))) - - @classmethod - def encode_header_item(cls, item): - if isinstance(item, six.text_type): - item = cls.encode(item) - - # See header_translate_* constants above. - # Replace only if you really know what you're doing. - return item.translate( - header_translate_table, header_translate_deletechars) - - @classmethod - def encode(cls, v): - """Return the given header name or value, encoded for HTTP output.""" - for enc in cls.encodings: - try: - return v.encode(enc) - except UnicodeEncodeError: - continue - - if cls.protocol == (1, 1) and cls.use_rfc_2047: - # Encode RFC-2047 TEXT - # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). - # We do our own here instead of using the email module - # because we never want to fold lines--folding has - # been deprecated by the HTTP working group. - v = b2a_base64(v.encode('utf-8')) - return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=') - - raise ValueError('Could not encode header part %r using ' - 'any of the encodings %r.' % - (v, cls.encodings)) - - -class Host(object): - - """An internet address. - - name - Should be the client's host name. If not available (because no DNS - lookup is performed), the IP address should be used instead. - - """ - - ip = '0.0.0.0' - port = 80 - name = 'unknown.tld' - - def __init__(self, ip, port, name=None): - self.ip = ip - self.port = port - if name is None: - name = ip - self.name = name - - def __repr__(self): - return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) diff --git a/libraries/cherrypy/lib/jsontools.py b/libraries/cherrypy/lib/jsontools.py deleted file mode 100644 index 48683097..00000000 --- a/libraries/cherrypy/lib/jsontools.py +++ /dev/null @@ -1,88 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode - - -def json_processor(entity): - """Read application/json data into request.json.""" - if not entity.headers.get(ntou('Content-Length'), ntou('')): - raise cherrypy.HTTPError(411) - - body = entity.fp.read() - with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): - cherrypy.serving.request.json = json_decode(body.decode('utf-8')) - - -def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], - force=True, debug=False, processor=json_processor): - """Add a processor to parse JSON request entities: - The default processor places the parsed data into request.json. - - Incoming request entities which match the given content_type(s) will - be deserialized from JSON to the Python equivalent, and the result - stored at cherrypy.request.json. The 'content_type' argument may - be a Content-Type string or a list of allowable Content-Type strings. - - If the 'force' argument is True (the default), then entities of other - content types will not be allowed; "415 Unsupported Media Type" is - raised instead. - - Supply your own processor to use a custom decoder, or to handle the parsed - data differently. The processor can be configured via - tools.json_in.processor or via the decorator method. - - Note that the deserializer requires the client send a Content-Length - request header, or it will raise "411 Length Required". If for any - other reason the request entity cannot be deserialized from JSON, - it will raise "400 Bad Request: Invalid JSON document". - """ - request = cherrypy.serving.request - if isinstance(content_type, text_or_bytes): - content_type = [content_type] - - if force: - if debug: - cherrypy.log('Removing body processors %s' % - repr(request.body.processors.keys()), 'TOOLS.JSON_IN') - request.body.processors.clear() - request.body.default_proc = cherrypy.HTTPError( - 415, 'Expected an entity of content type %s' % - ', '.join(content_type)) - - for ct in content_type: - if debug: - cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') - request.body.processors[ct] = processor - - -def json_handler(*args, **kwargs): - value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) - return json_encode(value) - - -def json_out(content_type='application/json', debug=False, - handler=json_handler): - """Wrap request.handler to serialize its output to JSON. Sets Content-Type. - - If the given content_type is None, the Content-Type response header - is not set. - - Provide your own handler to use a custom encoder. For example - cherrypy.config['tools.json_out.handler'] = <function>, or - @json_out(handler=function). - """ - request = cherrypy.serving.request - # request.handler may be set to None by e.g. the caching tool - # to signal to all components that a response body has already - # been attached, in which case we don't need to wrap anything. - if request.handler is None: - return - if debug: - cherrypy.log('Replacing %s with JSON handler' % request.handler, - 'TOOLS.JSON_OUT') - request._json_inner_handler = request.handler - request.handler = handler - if content_type is not None: - if debug: - cherrypy.log('Setting Content-Type to %s' % - content_type, 'TOOLS.JSON_OUT') - cherrypy.serving.response.headers['Content-Type'] = content_type diff --git a/libraries/cherrypy/lib/locking.py b/libraries/cherrypy/lib/locking.py deleted file mode 100644 index 317fb58c..00000000 --- a/libraries/cherrypy/lib/locking.py +++ /dev/null @@ -1,47 +0,0 @@ -import datetime - - -class NeverExpires(object): - def expired(self): - return False - - -class Timer(object): - """ - A simple timer that will indicate when an expiration time has passed. - """ - def __init__(self, expiration): - 'Create a timer that expires at `expiration` (UTC datetime)' - self.expiration = expiration - - @classmethod - def after(cls, elapsed): - """ - Return a timer that will expire after `elapsed` passes. - """ - return cls(datetime.datetime.utcnow() + elapsed) - - def expired(self): - return datetime.datetime.utcnow() >= self.expiration - - -class LockTimeout(Exception): - 'An exception when a lock could not be acquired before a timeout period' - - -class LockChecker(object): - """ - Keep track of the time and detect if a timeout has expired - """ - def __init__(self, session_id, timeout): - self.session_id = session_id - if timeout: - self.timer = Timer.after(timeout) - else: - self.timer = NeverExpires() - - def expired(self): - if self.timer.expired(): - raise LockTimeout( - 'Timeout acquiring lock for %(session_id)s' % vars(self)) - return False diff --git a/libraries/cherrypy/lib/profiler.py b/libraries/cherrypy/lib/profiler.py deleted file mode 100644 index fccf2eb8..00000000 --- a/libraries/cherrypy/lib/profiler.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Profiler tools for CherryPy. - -CherryPy users -============== - -You can profile any of your pages as follows:: - - from cherrypy.lib import profiler - - class Root: - p = profiler.Profiler("/path/to/profile/dir") - - @cherrypy.expose - def index(self): - self.p.run(self._index) - - def _index(self): - return "Hello, world!" - - cherrypy.tree.mount(Root()) - -You can also turn on profiling for all requests -using the ``make_app`` function as WSGI middleware. - -CherryPy developers -=================== - -This module can be used whenever you make changes to CherryPy, -to get a quick sanity-check on overall CP performance. Use the -``--profile`` flag when running the test suite. Then, use the ``serve()`` -function to browse the results in a web browser. If you run this -module from the command line, it will call ``serve()`` for you. - -""" - -import io -import os -import os.path -import sys -import warnings - -import cherrypy - - -try: - import profile - import pstats - - def new_func_strip_path(func_name): - """Make profiler output more readable by adding `__init__` modules' parents - """ - filename, line, name = func_name - if filename.endswith('__init__.py'): - return ( - os.path.basename(filename[:-12]) + filename[-12:], - line, - name, - ) - return os.path.basename(filename), line, name - - pstats.func_strip_path = new_func_strip_path -except ImportError: - profile = None - pstats = None - - -_count = 0 - - -class Profiler(object): - - def __init__(self, path=None): - if not path: - path = os.path.join(os.path.dirname(__file__), 'profile') - self.path = path - if not os.path.exists(path): - os.makedirs(path) - - def run(self, func, *args, **params): - """Dump profile data into self.path.""" - global _count - c = _count = _count + 1 - path = os.path.join(self.path, 'cp_%04d.prof' % c) - prof = profile.Profile() - result = prof.runcall(func, *args, **params) - prof.dump_stats(path) - return result - - def statfiles(self): - """:rtype: list of available profiles. - """ - return [f for f in os.listdir(self.path) - if f.startswith('cp_') and f.endswith('.prof')] - - def stats(self, filename, sortby='cumulative'): - """:rtype stats(index): output of print_stats() for the given profile. - """ - sio = io.StringIO() - if sys.version_info >= (2, 5): - s = pstats.Stats(os.path.join(self.path, filename), stream=sio) - s.strip_dirs() - s.sort_stats(sortby) - s.print_stats() - else: - # pstats.Stats before Python 2.5 didn't take a 'stream' arg, - # but just printed to stdout. So re-route stdout. - s = pstats.Stats(os.path.join(self.path, filename)) - s.strip_dirs() - s.sort_stats(sortby) - oldout = sys.stdout - try: - sys.stdout = sio - s.print_stats() - finally: - sys.stdout = oldout - response = sio.getvalue() - sio.close() - return response - - @cherrypy.expose - def index(self): - return """<html> - <head><title>CherryPy profile data</title></head> - <frameset cols='200, 1*'> - <frame src='menu' /> - <frame name='main' src='' /> - </frameset> - </html> - """ - - @cherrypy.expose - def menu(self): - yield '<h2>Profiling runs</h2>' - yield '<p>Click on one of the runs below to see profiling data.</p>' - runs = self.statfiles() - runs.sort() - for i in runs: - yield "<a href='report?filename=%s' target='main'>%s</a><br />" % ( - i, i) - - @cherrypy.expose - def report(self, filename): - cherrypy.response.headers['Content-Type'] = 'text/plain' - return self.stats(filename) - - -class ProfileAggregator(Profiler): - - def __init__(self, path=None): - Profiler.__init__(self, path) - global _count - self.count = _count = _count + 1 - self.profiler = profile.Profile() - - def run(self, func, *args, **params): - path = os.path.join(self.path, 'cp_%04d.prof' % self.count) - result = self.profiler.runcall(func, *args, **params) - self.profiler.dump_stats(path) - return result - - -class make_app: - - def __init__(self, nextapp, path=None, aggregate=False): - """Make a WSGI middleware app which wraps 'nextapp' with profiling. - - nextapp - the WSGI application to wrap, usually an instance of - cherrypy.Application. - - path - where to dump the profiling output. - - aggregate - if True, profile data for all HTTP requests will go in - a single file. If False (the default), each HTTP request will - dump its profile data into a separate file. - - """ - if profile is None or pstats is None: - msg = ('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 ' - 'for details.') - warnings.warn(msg) - - self.nextapp = nextapp - self.aggregate = aggregate - if aggregate: - self.profiler = ProfileAggregator(path) - else: - self.profiler = Profiler(path) - - def __call__(self, environ, start_response): - def gather(): - result = [] - for line in self.nextapp(environ, start_response): - result.append(line) - return result - return self.profiler.run(gather) - - -def serve(path=None, port=8080): - if profile is None or pstats is None: - msg = ('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 ' - 'for details.') - warnings.warn(msg) - - cherrypy.config.update({'server.socket_port': int(port), - 'server.thread_pool': 10, - 'environment': 'production', - }) - cherrypy.quickstart(Profiler(path)) - - -if __name__ == '__main__': - serve(*tuple(sys.argv[1:])) diff --git a/libraries/cherrypy/lib/reprconf.py b/libraries/cherrypy/lib/reprconf.py deleted file mode 100644 index 291ab663..00000000 --- a/libraries/cherrypy/lib/reprconf.py +++ /dev/null @@ -1,514 +0,0 @@ -"""Generic configuration system using unrepr. - -Configuration data may be supplied as a Python dictionary, as a filename, -or as an open file object. When you supply a filename or file, Python's -builtin ConfigParser is used (with some extensions). - -Namespaces ----------- - -Configuration keys are separated into namespaces by the first "." in the key. - -The only key that cannot exist in a namespace is the "environment" entry. -This special entry 'imports' other config entries from a template stored in -the Config.environments dict. - -You can define your own namespaces to be called when new config is merged -by adding a named handler to Config.namespaces. The name can be any string, -and the handler must be either a callable or a context manager. -""" - -from cherrypy._cpcompat import text_or_bytes -from six.moves import configparser -from six.moves import builtins - -import operator -import sys - - -class NamespaceSet(dict): - - """A dict of config namespace names and handlers. - - Each config entry should begin with a namespace name; the corresponding - namespace handler will be called once for each config entry in that - namespace, and will be passed two arguments: the config key (with the - namespace removed) and the config value. - - Namespace handlers may be any Python callable; they may also be - Python 2.5-style 'context managers', in which case their __enter__ - method should return a callable to be used as the handler. - See cherrypy.tools (the Toolbox class) for an example. - """ - - def __call__(self, config): - """Iterate through config and pass it to each namespace handler. - - config - A flat dict, where keys use dots to separate - namespaces, and values are arbitrary. - - The first name in each config key is used to look up the corresponding - namespace handler. For example, a config entry of {'tools.gzip.on': v} - will call the 'tools' namespace handler with the args: ('gzip.on', v) - """ - # Separate the given config into namespaces - ns_confs = {} - for k in config: - if '.' in k: - ns, name = k.split('.', 1) - bucket = ns_confs.setdefault(ns, {}) - bucket[name] = config[k] - - # I chose __enter__ and __exit__ so someday this could be - # rewritten using Python 2.5's 'with' statement: - # for ns, handler in six.iteritems(self): - # with handler as callable: - # for k, v in six.iteritems(ns_confs.get(ns, {})): - # callable(k, v) - for ns, handler in self.items(): - exit = getattr(handler, '__exit__', None) - if exit: - callable = handler.__enter__() - no_exc = True - try: - try: - for k, v in ns_confs.get(ns, {}).items(): - callable(k, v) - except Exception: - # The exceptional case is handled here - no_exc = False - if exit is None: - raise - if not exit(*sys.exc_info()): - raise - # The exception is swallowed if exit() returns true - finally: - # The normal and non-local-goto cases are handled here - if no_exc and exit: - exit(None, None, None) - else: - for k, v in ns_confs.get(ns, {}).items(): - handler(k, v) - - def __repr__(self): - return '%s.%s(%s)' % (self.__module__, self.__class__.__name__, - dict.__repr__(self)) - - def __copy__(self): - newobj = self.__class__() - newobj.update(self) - return newobj - copy = __copy__ - - -class Config(dict): - - """A dict-like set of configuration data, with defaults and namespaces. - - May take a file, filename, or dict. - """ - - defaults = {} - environments = {} - namespaces = NamespaceSet() - - def __init__(self, file=None, **kwargs): - self.reset() - if file is not None: - self.update(file) - if kwargs: - self.update(kwargs) - - def reset(self): - """Reset self to default values.""" - self.clear() - dict.update(self, self.defaults) - - def update(self, config): - """Update self from a dict, file, or filename.""" - self._apply(Parser.load(config)) - - def _apply(self, config): - """Update self from a dict.""" - which_env = config.get('environment') - if which_env: - env = self.environments[which_env] - for k in env: - if k not in config: - config[k] = env[k] - - dict.update(self, config) - self.namespaces(config) - - def __setitem__(self, k, v): - dict.__setitem__(self, k, v) - self.namespaces({k: v}) - - -class Parser(configparser.ConfigParser): - - """Sub-class of ConfigParser that keeps the case of options and that - raises an exception if the file cannot be read. - """ - - def optionxform(self, optionstr): - return optionstr - - def read(self, filenames): - if isinstance(filenames, text_or_bytes): - filenames = [filenames] - for filename in filenames: - # try: - # fp = open(filename) - # except IOError: - # continue - fp = open(filename) - try: - self._read(fp, filename) - finally: - fp.close() - - def as_dict(self, raw=False, vars=None): - """Convert an INI file to a dictionary""" - # Load INI file into a dict - result = {} - for section in self.sections(): - if section not in result: - result[section] = {} - for option in self.options(section): - value = self.get(section, option, raw=raw, vars=vars) - try: - value = unrepr(value) - except Exception: - x = sys.exc_info()[1] - msg = ('Config error in section: %r, option: %r, ' - 'value: %r. Config values must be valid Python.' % - (section, option, value)) - raise ValueError(msg, x.__class__.__name__, x.args) - result[section][option] = value - return result - - def dict_from_file(self, file): - if hasattr(file, 'read'): - self.readfp(file) - else: - self.read(file) - return self.as_dict() - - @classmethod - def load(self, input): - """Resolve 'input' to dict from a dict, file, or filename.""" - is_file = ( - # Filename - isinstance(input, text_or_bytes) - # Open file object - or hasattr(input, 'read') - ) - return Parser().dict_from_file(input) if is_file else input.copy() - - -# public domain "unrepr" implementation, found on the web and then improved. - - -class _Builder2: - - def build(self, o): - m = getattr(self, 'build_' + o.__class__.__name__, None) - if m is None: - raise TypeError('unrepr does not recognize %s' % - repr(o.__class__.__name__)) - return m(o) - - def astnode(self, s): - """Return a Python2 ast Node compiled from a string.""" - try: - import compiler - except ImportError: - # Fallback to eval when compiler package is not available, - # e.g. IronPython 1.0. - return eval(s) - - p = compiler.parse('__tempvalue__ = ' + s) - return p.getChildren()[1].getChildren()[0].getChildren()[1] - - def build_Subscript(self, o): - expr, flags, subs = o.getChildren() - expr = self.build(expr) - subs = self.build(subs) - return expr[subs] - - def build_CallFunc(self, o): - children = o.getChildren() - # Build callee from first child - callee = self.build(children[0]) - # Build args and kwargs from remaining children - args = [] - kwargs = {} - for child in children[1:]: - class_name = child.__class__.__name__ - # None is ignored - if class_name == 'NoneType': - continue - # Keywords become kwargs - if class_name == 'Keyword': - kwargs.update(self.build(child)) - # Everything else becomes args - else: - args.append(self.build(child)) - - return callee(*args, **kwargs) - - def build_Keyword(self, o): - key, value_obj = o.getChildren() - value = self.build(value_obj) - kw_dict = {key: value} - return kw_dict - - def build_List(self, o): - return map(self.build, o.getChildren()) - - def build_Const(self, o): - return o.value - - def build_Dict(self, o): - d = {} - i = iter(map(self.build, o.getChildren())) - for el in i: - d[el] = i.next() - return d - - def build_Tuple(self, o): - return tuple(self.build_List(o)) - - def build_Name(self, o): - name = o.name - if name == 'None': - return None - if name == 'True': - return True - if name == 'False': - return False - - # See if the Name is a package or module. If it is, import it. - try: - return modules(name) - except ImportError: - pass - - # See if the Name is in builtins. - try: - return getattr(builtins, name) - except AttributeError: - pass - - raise TypeError('unrepr could not resolve the name %s' % repr(name)) - - def build_Add(self, o): - left, right = map(self.build, o.getChildren()) - return left + right - - def build_Mul(self, o): - left, right = map(self.build, o.getChildren()) - return left * right - - def build_Getattr(self, o): - parent = self.build(o.expr) - return getattr(parent, o.attrname) - - def build_NoneType(self, o): - return None - - def build_UnarySub(self, o): - return -self.build(o.getChildren()[0]) - - def build_UnaryAdd(self, o): - return self.build(o.getChildren()[0]) - - -class _Builder3: - - def build(self, o): - m = getattr(self, 'build_' + o.__class__.__name__, None) - if m is None: - raise TypeError('unrepr does not recognize %s' % - repr(o.__class__.__name__)) - return m(o) - - def astnode(self, s): - """Return a Python3 ast Node compiled from a string.""" - try: - import ast - except ImportError: - # Fallback to eval when ast package is not available, - # e.g. IronPython 1.0. - return eval(s) - - p = ast.parse('__tempvalue__ = ' + s) - return p.body[0].value - - def build_Subscript(self, o): - return self.build(o.value)[self.build(o.slice)] - - def build_Index(self, o): - return self.build(o.value) - - def _build_call35(self, o): - """ - Workaround for python 3.5 _ast.Call signature, docs found here - https://greentreesnakes.readthedocs.org/en/latest/nodes.html - """ - import ast - callee = self.build(o.func) - args = [] - if o.args is not None: - for a in o.args: - if isinstance(a, ast.Starred): - args.append(self.build(a.value)) - else: - args.append(self.build(a)) - kwargs = {} - for kw in o.keywords: - if kw.arg is None: # double asterix `**` - rst = self.build(kw.value) - if not isinstance(rst, dict): - raise TypeError('Invalid argument for call.' - 'Must be a mapping object.') - # give preference to the keys set directly from arg=value - for k, v in rst.items(): - if k not in kwargs: - kwargs[k] = v - else: # defined on the call as: arg=value - kwargs[kw.arg] = self.build(kw.value) - return callee(*args, **kwargs) - - def build_Call(self, o): - if sys.version_info >= (3, 5): - return self._build_call35(o) - - callee = self.build(o.func) - - if o.args is None: - args = () - else: - args = tuple([self.build(a) for a in o.args]) - - if o.starargs is None: - starargs = () - else: - starargs = tuple(self.build(o.starargs)) - - if o.kwargs is None: - kwargs = {} - else: - kwargs = self.build(o.kwargs) - if o.keywords is not None: # direct a=b keywords - for kw in o.keywords: - # preference because is a direct keyword against **kwargs - kwargs[kw.arg] = self.build(kw.value) - return callee(*(args + starargs), **kwargs) - - def build_List(self, o): - return list(map(self.build, o.elts)) - - def build_Str(self, o): - return o.s - - def build_Num(self, o): - return o.n - - def build_Dict(self, o): - return dict([(self.build(k), self.build(v)) - for k, v in zip(o.keys, o.values)]) - - def build_Tuple(self, o): - return tuple(self.build_List(o)) - - def build_Name(self, o): - name = o.id - if name == 'None': - return None - if name == 'True': - return True - if name == 'False': - return False - - # See if the Name is a package or module. If it is, import it. - try: - return modules(name) - except ImportError: - pass - - # See if the Name is in builtins. - try: - import builtins - return getattr(builtins, name) - except AttributeError: - pass - - raise TypeError('unrepr could not resolve the name %s' % repr(name)) - - def build_NameConstant(self, o): - return o.value - - def build_UnaryOp(self, o): - op, operand = map(self.build, [o.op, o.operand]) - return op(operand) - - def build_BinOp(self, o): - left, op, right = map(self.build, [o.left, o.op, o.right]) - return op(left, right) - - def build_Add(self, o): - return operator.add - - def build_Mult(self, o): - return operator.mul - - def build_USub(self, o): - return operator.neg - - def build_Attribute(self, o): - parent = self.build(o.value) - return getattr(parent, o.attr) - - def build_NoneType(self, o): - return None - - -def unrepr(s): - """Return a Python object compiled from a string.""" - if not s: - return s - if sys.version_info < (3, 0): - b = _Builder2() - else: - b = _Builder3() - obj = b.astnode(s) - return b.build(obj) - - -def modules(modulePath): - """Load a module and retrieve a reference to that module.""" - __import__(modulePath) - return sys.modules[modulePath] - - -def attributes(full_attribute_name): - """Load a module and retrieve an attribute of that module.""" - - # Parse out the path, module, and attribute - last_dot = full_attribute_name.rfind('.') - attr_name = full_attribute_name[last_dot + 1:] - mod_path = full_attribute_name[:last_dot] - - mod = modules(mod_path) - # Let an AttributeError propagate outward. - try: - attr = getattr(mod, attr_name) - except AttributeError: - raise AttributeError("'%s' object has no attribute '%s'" - % (mod_path, attr_name)) - - # Return a reference to the attribute. - return attr diff --git a/libraries/cherrypy/lib/sessions.py b/libraries/cherrypy/lib/sessions.py deleted file mode 100644 index 5b49ee13..00000000 --- a/libraries/cherrypy/lib/sessions.py +++ /dev/null @@ -1,919 +0,0 @@ -"""Session implementation for CherryPy. - -You need to edit your config file to use sessions. Here's an example:: - - [/] - tools.sessions.on = True - tools.sessions.storage_class = cherrypy.lib.sessions.FileSession - tools.sessions.storage_path = "/home/site/sessions" - tools.sessions.timeout = 60 - -This sets the session to be stored in files in the directory -/home/site/sessions, and the session timeout to 60 minutes. If you omit -``storage_class``, the sessions will be saved in RAM. -``tools.sessions.on`` is the only required line for working sessions, -the rest are optional. - -By default, the session ID is passed in a cookie, so the client's browser must -have cookies enabled for your site. - -To set data for the current session, use -``cherrypy.session['fieldname'] = 'fieldvalue'``; -to get data use ``cherrypy.session.get('fieldname')``. - -================ -Locking sessions -================ - -By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means -the session is locked early and unlocked late. Be mindful of this default mode -for any requests that take a long time to process (streaming responses, -expensive calculations, database lookups, API calls, etc), as other concurrent -requests that also utilize sessions will hang until the session is unlocked. - -If you want to control when the session data is locked and unlocked, -set ``tools.sessions.locking = 'explicit'``. Then call -``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. -Regardless of which mode you use, the session is guaranteed to be unlocked when -the request is complete. - -================= -Expiring Sessions -================= - -You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. -Simply call that function at the point you want the session to expire, and it -will cause the session cookie to expire client-side. - -=========================== -Session Fixation Protection -=========================== - -If CherryPy receives, via a request cookie, a session id that it does not -recognize, it will reject that id and create a new one to return in the -response cookie. This `helps prevent session fixation attacks -<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_. -However, CherryPy "recognizes" a session id by looking up the saved session -data for that id. Therefore, if you never save any session data, -**you will get a new session id for every request**. - -A side effect of CherryPy overwriting unrecognised session ids is that if you -have multiple, separate CherryPy applications running on a single domain (e.g. -on different ports), each app will overwrite the other's session id because by -default they use the same cookie name (``"session_id"``) but do not recognise -each others sessions. It is therefore a good idea to use a different name for -each, for example:: - - [/] - ... - tools.sessions.name = "my_app_session_id" - -================ -Sharing Sessions -================ - -If you run multiple instances of CherryPy (for example via mod_python behind -Apache prefork), you most likely cannot use the RAM session backend, since each -instance of CherryPy will have its own memory space. Use a different backend -instead, and verify that all instances are pointing at the same file or db -location. Alternately, you might try a load balancer which makes sessions -"sticky". Google is your friend, there. - -================ -Expiration Dates -================ - -The response cookie will possess an expiration date to inform the client at -which point to stop sending the cookie back in requests. If the server time -and client time differ, expect sessions to be unreliable. **Make sure the -system time of your server is accurate**. - -CherryPy defaults to a 60-minute session timeout, which also applies to the -cookie which is sent to the client. Unfortunately, some versions of Safari -("4 public beta" on Windows XP at least) appear to have a bug in their parsing -of the GMT expiration date--they appear to interpret the date as one hour in -the past. Sixty minutes minus one hour is pretty close to zero, so you may -experience this bug as a new session id for every request, unless the requests -are less than one second apart. To fix, try increasing the session.timeout. - -On the other extreme, some users report Firefox sending cookies after their -expiration date, although this was on a system with an inaccurate system time. -Maybe FF doesn't trust system time. -""" -import sys -import datetime -import os -import time -import threading -import binascii - -import six -from six.moves import cPickle as pickle -import contextlib2 - -import zc.lockfile - -import cherrypy -from cherrypy.lib import httputil -from cherrypy.lib import locking -from cherrypy.lib import is_iterator - - -if six.PY2: - FileNotFoundError = OSError - - -missing = object() - - -class Session(object): - - """A CherryPy dict-like Session object (one per request).""" - - _id = None - - id_observers = None - "A list of callbacks to which to pass new id's." - - @property - def id(self): - """Return the current session id.""" - return self._id - - @id.setter - def id(self, value): - self._id = value - for o in self.id_observers: - o(value) - - timeout = 60 - 'Number of minutes after which to delete session data.' - - locked = False - """ - If True, this session instance has exclusive read/write access - to session data.""" - - loaded = False - """ - If True, data has been retrieved from storage. This should happen - automatically on the first attempt to access session data.""" - - clean_thread = None - 'Class-level Monitor which calls self.clean_up.' - - clean_freq = 5 - 'The poll rate for expired session cleanup in minutes.' - - originalid = None - 'The session id passed by the client. May be missing or unsafe.' - - missing = False - 'True if the session requested by the client did not exist.' - - regenerated = False - """ - True if the application called session.regenerate(). This is not set by - internal calls to regenerate the session id.""" - - debug = False - 'If True, log debug information.' - - # --------------------- Session management methods --------------------- # - - def __init__(self, id=None, **kwargs): - self.id_observers = [] - self._data = {} - - for k, v in kwargs.items(): - setattr(self, k, v) - - self.originalid = id - self.missing = False - if id is None: - if self.debug: - cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') - self._regenerate() - else: - self.id = id - if self._exists(): - if self.debug: - cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS') - else: - if self.debug: - cherrypy.log('Expired or malicious session %r; ' - 'making a new one' % id, 'TOOLS.SESSIONS') - # Expired or malicious session. Make a new one. - # See https://github.com/cherrypy/cherrypy/issues/709. - self.id = None - self.missing = True - self._regenerate() - - def now(self): - """Generate the session specific concept of 'now'. - - Other session providers can override this to use alternative, - possibly timezone aware, versions of 'now'. - """ - return datetime.datetime.now() - - def regenerate(self): - """Replace the current session (with a new id).""" - self.regenerated = True - self._regenerate() - - def _regenerate(self): - if self.id is not None: - if self.debug: - cherrypy.log( - 'Deleting the existing session %r before ' - 'regeneration.' % self.id, - 'TOOLS.SESSIONS') - self.delete() - - old_session_was_locked = self.locked - if old_session_was_locked: - self.release_lock() - if self.debug: - cherrypy.log('Old lock released.', 'TOOLS.SESSIONS') - - self.id = None - while self.id is None: - self.id = self.generate_id() - # Assert that the generated id is not already stored. - if self._exists(): - self.id = None - if self.debug: - cherrypy.log('Set id to generated %s.' % self.id, - 'TOOLS.SESSIONS') - - if old_session_was_locked: - self.acquire_lock() - if self.debug: - cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS') - - def clean_up(self): - """Clean up expired sessions.""" - pass - - def generate_id(self): - """Return a new session id.""" - return binascii.hexlify(os.urandom(20)).decode('ascii') - - def save(self): - """Save session data.""" - try: - # If session data has never been loaded then it's never been - # accessed: no need to save it - if self.loaded: - t = datetime.timedelta(seconds=self.timeout * 60) - expiration_time = self.now() + t - if self.debug: - cherrypy.log('Saving session %r with expiry %s' % - (self.id, expiration_time), - 'TOOLS.SESSIONS') - self._save(expiration_time) - else: - if self.debug: - cherrypy.log( - 'Skipping save of session %r (no session loaded).' % - self.id, 'TOOLS.SESSIONS') - finally: - if self.locked: - # Always release the lock if the user didn't release it - self.release_lock() - if self.debug: - cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS') - - def load(self): - """Copy stored session data into this session instance.""" - data = self._load() - # data is either None or a tuple (session_data, expiration_time) - if data is None or data[1] < self.now(): - if self.debug: - cherrypy.log('Expired session %r, flushing data.' % self.id, - 'TOOLS.SESSIONS') - self._data = {} - else: - if self.debug: - cherrypy.log('Data loaded for session %r.' % self.id, - 'TOOLS.SESSIONS') - self._data = data[0] - self.loaded = True - - # Stick the clean_thread in the class, not the instance. - # The instances are created and destroyed per-request. - cls = self.__class__ - if self.clean_freq and not cls.clean_thread: - # clean_up is an instancemethod and not a classmethod, - # so that tool config can be accessed inside the method. - t = cherrypy.process.plugins.Monitor( - cherrypy.engine, self.clean_up, self.clean_freq * 60, - name='Session cleanup') - t.subscribe() - cls.clean_thread = t - t.start() - if self.debug: - cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS') - - def delete(self): - """Delete stored session data.""" - self._delete() - if self.debug: - cherrypy.log('Deleted session %s.' % self.id, - 'TOOLS.SESSIONS') - - # -------------------- Application accessor methods -------------------- # - - def __getitem__(self, key): - if not self.loaded: - self.load() - return self._data[key] - - def __setitem__(self, key, value): - if not self.loaded: - self.load() - self._data[key] = value - - def __delitem__(self, key): - if not self.loaded: - self.load() - del self._data[key] - - def pop(self, key, default=missing): - """Remove the specified key and return the corresponding value. - If key is not found, default is returned if given, - otherwise KeyError is raised. - """ - if not self.loaded: - self.load() - if default is missing: - return self._data.pop(key) - else: - return self._data.pop(key, default) - - def __contains__(self, key): - if not self.loaded: - self.load() - return key in self._data - - def get(self, key, default=None): - """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" - if not self.loaded: - self.load() - return self._data.get(key, default) - - def update(self, d): - """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" - if not self.loaded: - self.load() - self._data.update(d) - - def setdefault(self, key, default=None): - """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" - if not self.loaded: - self.load() - return self._data.setdefault(key, default) - - def clear(self): - """D.clear() -> None. Remove all items from D.""" - if not self.loaded: - self.load() - self._data.clear() - - def keys(self): - """D.keys() -> list of D's keys.""" - if not self.loaded: - self.load() - return self._data.keys() - - def items(self): - """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" - if not self.loaded: - self.load() - return self._data.items() - - def values(self): - """D.values() -> list of D's values.""" - if not self.loaded: - self.load() - return self._data.values() - - -class RamSession(Session): - - # Class-level objects. Don't rebind these! - cache = {} - locks = {} - - def clean_up(self): - """Clean up expired sessions.""" - - now = self.now() - for _id, (data, expiration_time) in list(six.iteritems(self.cache)): - if expiration_time <= now: - try: - del self.cache[_id] - except KeyError: - pass - try: - if self.locks[_id].acquire(blocking=False): - lock = self.locks.pop(_id) - lock.release() - except KeyError: - pass - - # added to remove obsolete lock objects - for _id in list(self.locks): - locked = ( - _id not in self.cache - and self.locks[_id].acquire(blocking=False) - ) - if locked: - lock = self.locks.pop(_id) - lock.release() - - def _exists(self): - return self.id in self.cache - - def _load(self): - return self.cache.get(self.id) - - def _save(self, expiration_time): - self.cache[self.id] = (self._data, expiration_time) - - def _delete(self): - self.cache.pop(self.id, None) - - def acquire_lock(self): - """Acquire an exclusive lock on the currently-loaded session data.""" - self.locked = True - self.locks.setdefault(self.id, threading.RLock()).acquire() - - def release_lock(self): - """Release the lock on the currently-loaded session data.""" - self.locks[self.id].release() - self.locked = False - - def __len__(self): - """Return the number of active sessions.""" - return len(self.cache) - - -class FileSession(Session): - - """Implementation of the File backend for sessions - - storage_path - The folder where session data will be saved. Each session - will be saved as pickle.dump(data, expiration_time) in its own file; - the filename will be self.SESSION_PREFIX + self.id. - - lock_timeout - A timedelta or numeric seconds indicating how long - to block acquiring a lock. If None (default), acquiring a lock - will block indefinitely. - """ - - SESSION_PREFIX = 'session-' - LOCK_SUFFIX = '.lock' - pickle_protocol = pickle.HIGHEST_PROTOCOL - - def __init__(self, id=None, **kwargs): - # The 'storage_path' arg is required for file-based sessions. - kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) - kwargs.setdefault('lock_timeout', None) - - Session.__init__(self, id=id, **kwargs) - - # validate self.lock_timeout - if isinstance(self.lock_timeout, (int, float)): - self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) - if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): - raise ValueError( - 'Lock timeout must be numeric seconds or a timedelta instance.' - ) - - @classmethod - def setup(cls, **kwargs): - """Set up the storage system for file-based sessions. - - This should only be called once per process; this will be done - automatically when using sessions.init (as the built-in Tool does). - """ - # The 'storage_path' arg is required for file-based sessions. - kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) - - for k, v in kwargs.items(): - setattr(cls, k, v) - - def _get_file_path(self): - f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) - if not os.path.abspath(f).startswith(self.storage_path): - raise cherrypy.HTTPError(400, 'Invalid session id in cookie.') - return f - - def _exists(self): - path = self._get_file_path() - return os.path.exists(path) - - def _load(self, path=None): - assert self.locked, ('The session load without being locked. ' - "Check your tools' priority levels.") - if path is None: - path = self._get_file_path() - try: - f = open(path, 'rb') - try: - return pickle.load(f) - finally: - f.close() - except (IOError, EOFError): - e = sys.exc_info()[1] - if self.debug: - cherrypy.log('Error loading the session pickle: %s' % - e, 'TOOLS.SESSIONS') - return 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: - 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. ' - "Check your tools' priority levels.") - try: - os.unlink(self._get_file_path()) - except OSError: - pass - - def acquire_lock(self, path=None): - """Acquire an exclusive lock on the currently-loaded session data.""" - if path is None: - path = self._get_file_path() - path += self.LOCK_SUFFIX - checker = locking.LockChecker(self.id, self.lock_timeout) - while not checker.expired(): - try: - self.lock = zc.lockfile.LockFile(path) - except zc.lockfile.LockError: - time.sleep(0.1) - else: - break - self.locked = True - if self.debug: - cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') - - def release_lock(self, path=None): - """Release the lock on the currently-loaded session data.""" - self.lock.close() - with contextlib2.suppress(FileNotFoundError): - os.remove(self.lock._path) - self.locked = False - - def clean_up(self): - """Clean up expired sessions.""" - now = self.now() - # Iterate over all session files in self.storage_path - for fname in os.listdir(self.storage_path): - have_session = ( - fname.startswith(self.SESSION_PREFIX) - and not fname.endswith(self.LOCK_SUFFIX) - ) - if have_session: - # We have a session file: lock and load it and check - # if it's expired. If it fails, nevermind. - path = os.path.join(self.storage_path, fname) - self.acquire_lock(path) - if self.debug: - # This is a bit of a hack, since we're calling clean_up - # on the first instance rather than the entire class, - # so depending on whether you have "debug" set on the - # path of the first session called, this may not run. - cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS') - - try: - contents = self._load(path) - # _load returns None on IOError - if contents is not None: - data, expiration_time = contents - if expiration_time < now: - # Session expired: deleting it - os.unlink(path) - finally: - self.release_lock(path) - - def __len__(self): - """Return the number of active sessions.""" - return len([fname for fname in os.listdir(self.storage_path) - if (fname.startswith(self.SESSION_PREFIX) and - not fname.endswith(self.LOCK_SUFFIX))]) - - -class MemcachedSession(Session): - - # The most popular memcached client for Python isn't thread-safe. - # Wrap all .get and .set operations in a single lock. - mc_lock = threading.RLock() - - # This is a separate set of locks per session id. - locks = {} - - servers = ['127.0.0.1:11211'] - - @classmethod - def setup(cls, **kwargs): - """Set up the storage system for memcached-based sessions. - - This should only be called once per process; this will be done - automatically when using sessions.init (as the built-in Tool does). - """ - for k, v in kwargs.items(): - setattr(cls, k, v) - - import memcache - cls.cache = memcache.Client(cls.servers) - - def _exists(self): - self.mc_lock.acquire() - try: - return bool(self.cache.get(self.id)) - finally: - self.mc_lock.release() - - def _load(self): - self.mc_lock.acquire() - try: - return self.cache.get(self.id) - finally: - self.mc_lock.release() - - def _save(self, expiration_time): - # Send the expiration time as "Unix time" (seconds since 1/1/1970) - td = int(time.mktime(expiration_time.timetuple())) - self.mc_lock.acquire() - try: - if not self.cache.set(self.id, (self._data, expiration_time), td): - raise AssertionError( - 'Session data for id %r not set.' % self.id) - finally: - self.mc_lock.release() - - def _delete(self): - self.cache.delete(self.id) - - def acquire_lock(self): - """Acquire an exclusive lock on the currently-loaded session data.""" - self.locked = True - self.locks.setdefault(self.id, threading.RLock()).acquire() - if self.debug: - cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') - - def release_lock(self): - """Release the lock on the currently-loaded session data.""" - self.locks[self.id].release() - self.locked = False - - def __len__(self): - """Return the number of active sessions.""" - raise NotImplementedError - - -# Hook functions (for CherryPy tools) - -def save(): - """Save any changed session data.""" - - if not hasattr(cherrypy.serving, 'session'): - return - request = cherrypy.serving.request - response = cherrypy.serving.response - - # Guard against running twice - if hasattr(request, '_sessionsaved'): - return - request._sessionsaved = True - - if response.stream: - # If the body is being streamed, we have to save the data - # *after* the response has been written out - request.hooks.attach('on_end_request', cherrypy.session.save) - else: - # If the body is not being streamed, we save the data now - # (so we can release the lock). - if is_iterator(response.body): - response.collapse_body() - cherrypy.session.save() - - -save.failsafe = True - - -def close(): - """Close the session object for this request.""" - sess = getattr(cherrypy.serving, 'session', None) - if getattr(sess, 'locked', False): - # If the session is still locked we release the lock - sess.release_lock() - if sess.debug: - cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') - - -close.failsafe = True -close.priority = 90 - - -def init(storage_type=None, path=None, path_header=None, name='session_id', - timeout=60, domain=None, secure=False, clean_freq=5, - persistent=True, httponly=False, debug=False, - # Py27 compat - # *, storage_class=RamSession, - **kwargs): - """Initialize session object (using cookies). - - storage_class - The Session subclass to use. Defaults to RamSession. - - storage_type - (deprecated) - One of 'ram', 'file', memcached'. This will be - used to look up the corresponding class in cherrypy.lib.sessions - globals. For example, 'file' will use the FileSession class. - - path - The 'path' value to stick in the response cookie metadata. - - path_header - If 'path' is None (the default), then the response - cookie 'path' will be pulled from request.headers[path_header]. - - name - The name of the cookie. - - timeout - The expiration timeout (in minutes) for the stored session data. - If 'persistent' is True (the default), this is also the timeout - for the cookie. - - domain - The cookie domain. - - secure - If False (the default) the cookie 'secure' value will not - be set. If True, the cookie 'secure' value will be set (to 1). - - clean_freq (minutes) - The poll rate for expired session cleanup. - - persistent - If True (the default), the 'timeout' argument will be used - to expire the cookie. If False, the cookie will not have an expiry, - and the cookie will be a "session cookie" which expires when the - browser is closed. - - httponly - If False (the default) the cookie 'httponly' value will not be set. - If True, the cookie 'httponly' value will be set (to 1). - - Any additional kwargs will be bound to the new Session instance, - and may be specific to the storage type. See the subclass of Session - you're using for more information. - """ - - # Py27 compat - storage_class = kwargs.pop('storage_class', RamSession) - - request = cherrypy.serving.request - - # Guard against running twice - if hasattr(request, '_session_init_flag'): - return - request._session_init_flag = True - - # Check if request came with a session ID - id = None - if name in request.cookie: - id = request.cookie[name].value - if debug: - cherrypy.log('ID obtained from request.cookie: %r' % id, - 'TOOLS.SESSIONS') - - first_time = not hasattr(cherrypy, 'session') - - if storage_type: - if first_time: - msg = 'storage_type is deprecated. Supply storage_class instead' - cherrypy.log(msg) - storage_class = storage_type.title() + 'Session' - storage_class = globals()[storage_class] - - # call setup first time only - if first_time: - if hasattr(storage_class, 'setup'): - storage_class.setup(**kwargs) - - # Create and attach a new Session instance to cherrypy.serving. - # It will possess a reference to (and lock, and lazily load) - # the requested session data. - kwargs['timeout'] = timeout - kwargs['clean_freq'] = clean_freq - cherrypy.serving.session = sess = storage_class(id, **kwargs) - sess.debug = debug - - def update_cookie(id): - """Update the cookie every time the session id changes.""" - cherrypy.serving.response.cookie[name] = id - sess.id_observers.append(update_cookie) - - # Create cherrypy.session which will proxy to cherrypy.serving.session - if not hasattr(cherrypy, 'session'): - cherrypy.session = cherrypy._ThreadLocalProxy('session') - - if persistent: - cookie_timeout = timeout - else: - # See http://support.microsoft.com/kb/223799/EN-US/ - # and http://support.mozilla.com/en-US/kb/Cookies - cookie_timeout = None - set_response_cookie(path=path, path_header=path_header, name=name, - timeout=cookie_timeout, domain=domain, secure=secure, - httponly=httponly) - - -def set_response_cookie(path=None, path_header=None, name='session_id', - timeout=60, domain=None, secure=False, httponly=False): - """Set a response cookie for the client. - - path - the 'path' value to stick in the response cookie metadata. - - path_header - if 'path' is None (the default), then the response - cookie 'path' will be pulled from request.headers[path_header]. - - name - the name of the cookie. - - timeout - the expiration timeout for the cookie. If 0 or other boolean - False, no 'expires' param will be set, and the cookie will be a - "session cookie" which expires when the browser is closed. - - domain - the cookie domain. - - secure - if False (the default) the cookie 'secure' value will not - be set. If True, the cookie 'secure' value will be set (to 1). - - httponly - If False (the default) the cookie 'httponly' value will not be set. - If True, the cookie 'httponly' value will be set (to 1). - - """ - # Set response cookie - cookie = cherrypy.serving.response.cookie - cookie[name] = cherrypy.serving.session.id - cookie[name]['path'] = ( - path or - cherrypy.serving.request.headers.get(path_header) or - '/' - ) - - if timeout: - cookie[name]['max-age'] = timeout * 60 - _add_MSIE_max_age_workaround(cookie[name], timeout) - if domain is not None: - cookie[name]['domain'] = domain - if secure: - cookie[name]['secure'] = 1 - if httponly: - if not cookie[name].isReservedKey('httponly'): - raise ValueError('The httponly cookie token is not supported.') - cookie[name]['httponly'] = 1 - - -def _add_MSIE_max_age_workaround(cookie, timeout): - """ - We'd like to use the "max-age" param as indicated in - http://www.faqs.org/rfcs/rfc2109.html but IE doesn't - save it to disk and the session is lost if people close - the browser. So we have to use the old "expires" ... sigh ... - """ - expires = time.time() + timeout * 60 - cookie['expires'] = httputil.HTTPDate(expires) - - -def expire(): - """Expire the current session cookie.""" - name = cherrypy.serving.request.config.get( - 'tools.sessions.name', 'session_id') - one_year = 60 * 60 * 24 * 365 - e = time.time() - one_year - cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) - cherrypy.serving.response.cookie[name].pop('max-age', None) diff --git a/libraries/cherrypy/lib/static.py b/libraries/cherrypy/lib/static.py deleted file mode 100644 index da9d9373..00000000 --- a/libraries/cherrypy/lib/static.py +++ /dev/null @@ -1,390 +0,0 @@ -"""Module with helpers for serving static files.""" - -import os -import platform -import re -import stat -import mimetypes - -from email.generator import _make_boundary as make_boundary -from io import UnsupportedOperation - -from six.moves import urllib - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.lib import cptools, httputil, file_generator_limited - - -def _setup_mimetypes(): - """Pre-initialize global mimetype map.""" - if not mimetypes.inited: - mimetypes.init() - mimetypes.types_map['.dwg'] = 'image/x-dwg' - mimetypes.types_map['.ico'] = 'image/x-icon' - mimetypes.types_map['.bz2'] = 'application/x-bzip2' - mimetypes.types_map['.gz'] = 'application/x-gzip' - - -_setup_mimetypes() - - -def serve_file(path, content_type=None, disposition=None, name=None, - debug=False): - """Set status, headers, and body in order to serve the given path. - - The Content-Type header will be set to the content_type arg, if provided. - If not provided, the Content-Type will be guessed by the file extension - of the 'path' argument. - - If disposition is not None, the Content-Disposition header will be set - to "<disposition>; filename=<name>". If name is None, it will be set - to the basename of path. If disposition is None, no Content-Disposition - header will be written. - """ - response = cherrypy.serving.response - - # If path is relative, users should fix it by making path absolute. - # That is, CherryPy should not guess where the application root is. - # It certainly should *not* use cwd (since CP may be invoked from a - # variety of paths). If using tools.staticdir, you can make your relative - # paths become absolute by supplying a value for "tools.staticdir.root". - if not os.path.isabs(path): - msg = "'%s' is not an absolute path." % path - if debug: - cherrypy.log(msg, 'TOOLS.STATICFILE') - raise ValueError(msg) - - try: - st = os.stat(path) - except (OSError, TypeError, ValueError): - # OSError when file fails to stat - # TypeError on Python 2 when there's a null byte - # ValueError on Python 3 when there's a null byte - if debug: - cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') - raise cherrypy.NotFound() - - # Check if path is a directory. - if stat.S_ISDIR(st.st_mode): - # Let the caller deal with it as they like. - if debug: - cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') - raise cherrypy.NotFound() - - # Set the Last-Modified response header, so that - # modified-since validation code can work. - response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) - cptools.validate_since() - - if content_type is None: - # Set content-type based on filename extension - ext = '' - i = path.rfind('.') - if i != -1: - ext = path[i:].lower() - content_type = mimetypes.types_map.get(ext, None) - if content_type is not None: - response.headers['Content-Type'] = content_type - if debug: - cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') - - cd = None - if disposition is not None: - if name is None: - name = os.path.basename(path) - cd = '%s; filename="%s"' % (disposition, name) - response.headers['Content-Disposition'] = cd - if debug: - cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') - - # Set Content-Length and use an iterable (file object) - # this way CP won't load the whole file in memory - content_length = st.st_size - fileobj = open(path, 'rb') - return _serve_fileobj(fileobj, content_type, content_length, debug=debug) - - -def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, - debug=False): - """Set status, headers, and body in order to serve the given file object. - - The Content-Type header will be set to the content_type arg, if provided. - - If disposition is not None, the Content-Disposition header will be set - to "<disposition>; filename=<name>". If name is None, 'filename' will - not be set. If disposition is None, no Content-Disposition header will - be written. - - CAUTION: If the request contains a 'Range' header, one or more seek()s will - be performed on the file object. This may cause undesired behavior if - the file object is not seekable. It could also produce undesired results - if the caller set the read position of the file object prior to calling - serve_fileobj(), expecting that the data would be served starting from that - position. - """ - response = cherrypy.serving.response - - try: - st = os.fstat(fileobj.fileno()) - except AttributeError: - if debug: - cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') - content_length = None - except UnsupportedOperation: - content_length = None - else: - # Set the Last-Modified response header, so that - # modified-since validation code can work. - response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) - cptools.validate_since() - content_length = st.st_size - - if content_type is not None: - response.headers['Content-Type'] = content_type - if debug: - cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') - - cd = None - if disposition is not None: - if name is None: - cd = disposition - else: - cd = '%s; filename="%s"' % (disposition, name) - response.headers['Content-Disposition'] = cd - if debug: - cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') - - return _serve_fileobj(fileobj, content_type, content_length, debug=debug) - - -def _serve_fileobj(fileobj, content_type, content_length, debug=False): - """Internal. Set response.body to the given file object, perhaps ranged.""" - response = cherrypy.serving.response - - # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code - request = cherrypy.serving.request - if request.protocol >= (1, 1): - response.headers['Accept-Ranges'] = 'bytes' - r = httputil.get_ranges(request.headers.get('Range'), content_length) - if r == []: - response.headers['Content-Range'] = 'bytes */%s' % content_length - message = ('Invalid Range (first-byte-pos greater than ' - 'Content-Length)') - if debug: - cherrypy.log(message, 'TOOLS.STATIC') - raise cherrypy.HTTPError(416, message) - - if r: - if len(r) == 1: - # Return a single-part response. - start, stop = r[0] - if stop > content_length: - stop = content_length - r_len = stop - start - if debug: - cherrypy.log( - 'Single part; start: %r, stop: %r' % (start, stop), - 'TOOLS.STATIC') - response.status = '206 Partial Content' - response.headers['Content-Range'] = ( - 'bytes %s-%s/%s' % (start, stop - 1, content_length)) - response.headers['Content-Length'] = r_len - fileobj.seek(start) - response.body = file_generator_limited(fileobj, r_len) - else: - # Return a multipart/byteranges response. - response.status = '206 Partial Content' - boundary = make_boundary() - ct = 'multipart/byteranges; boundary=%s' % boundary - response.headers['Content-Type'] = ct - if 'Content-Length' in response.headers: - # Delete Content-Length header so finalize() recalcs it. - del response.headers['Content-Length'] - - def file_ranges(): - # Apache compatibility: - yield b'\r\n' - - for start, stop in r: - if debug: - cherrypy.log( - 'Multipart; start: %r, stop: %r' % ( - start, stop), - 'TOOLS.STATIC') - yield ntob('--' + boundary, 'ascii') - yield ntob('\r\nContent-type: %s' % content_type, - 'ascii') - yield ntob( - '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % ( - start, stop - 1, content_length), - 'ascii') - fileobj.seek(start) - gen = file_generator_limited(fileobj, stop - start) - for chunk in gen: - yield chunk - yield b'\r\n' - # Final boundary - yield ntob('--' + boundary + '--', 'ascii') - - # Apache compatibility: - yield b'\r\n' - response.body = file_ranges() - return response.body - else: - if debug: - cherrypy.log('No byteranges requested', 'TOOLS.STATIC') - - # Set Content-Length and use an iterable (file object) - # this way CP won't load the whole file in memory - response.headers['Content-Length'] = content_length - response.body = fileobj - return response.body - - -def serve_download(path, name=None): - """Serve 'path' as an application/x-download attachment.""" - # This is such a common idiom I felt it deserved its own wrapper. - return serve_file(path, 'application/x-download', 'attachment', name) - - -def _attempt(filename, content_types, debug=False): - if debug: - cherrypy.log('Attempting %r (content_types %r)' % - (filename, content_types), 'TOOLS.STATICDIR') - try: - # you can set the content types for a - # complete directory per extension - content_type = None - if content_types: - r, ext = os.path.splitext(filename) - content_type = content_types.get(ext[1:], None) - serve_file(filename, content_type=content_type, debug=debug) - return True - except cherrypy.NotFound: - # If we didn't find the static file, continue handling the - # request. We might find a dynamic handler instead. - if debug: - cherrypy.log('NotFound', 'TOOLS.STATICFILE') - return False - - -def staticdir(section, dir, root='', match='', content_types=None, index='', - debug=False): - """Serve a static resource from the given (root +) dir. - - match - If given, request.path_info will be searched for the given - regular expression before attempting to serve static content. - - content_types - If given, it should be a Python dictionary of - {file-extension: content-type} pairs, where 'file-extension' is - a string (e.g. "gif") and 'content-type' is the value to write - out in the Content-Type response header (e.g. "image/gif"). - - index - If provided, it should be the (relative) name of a file to - serve for directory requests. For example, if the dir argument is - '/home/me', the Request-URI is 'myapp', and the index arg is - 'index.html', the file '/home/me/myapp/index.html' will be sought. - """ - request = cherrypy.serving.request - if request.method not in ('GET', 'HEAD'): - if debug: - cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') - return False - - if match and not re.search(match, request.path_info): - if debug: - cherrypy.log('request.path_info %r does not match pattern %r' % - (request.path_info, match), 'TOOLS.STATICDIR') - return False - - # Allow the use of '~' to refer to a user's home directory. - dir = os.path.expanduser(dir) - - # If dir is relative, make absolute using "root". - if not os.path.isabs(dir): - if not root: - msg = 'Static dir requires an absolute dir (or root).' - if debug: - cherrypy.log(msg, 'TOOLS.STATICDIR') - raise ValueError(msg) - dir = os.path.join(root, dir) - - # Determine where we are in the object tree relative to 'section' - # (where the static tool was defined). - if section == 'global': - section = '/' - section = section.rstrip(r'\/') - branch = request.path_info[len(section) + 1:] - branch = urllib.parse.unquote(branch.lstrip(r'\/')) - - # Requesting a file in sub-dir of the staticdir results - # in mixing of delimiter styles, e.g. C:\static\js/script.js. - # Windows accepts this form except not when the path is - # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js. - # http://bit.ly/1vdioCX - if platform.system() == 'Windows': - branch = branch.replace('/', '\\') - - # If branch is "", filename will end in a slash - filename = os.path.join(dir, branch) - if debug: - cherrypy.log('Checking file %r to fulfill %r' % - (filename, request.path_info), 'TOOLS.STATICDIR') - - # There's a chance that the branch pulled from the URL might - # have ".." or similar uplevel attacks in it. Check that the final - # filename is a child of dir. - if not os.path.normpath(filename).startswith(os.path.normpath(dir)): - raise cherrypy.HTTPError(403) # Forbidden - - handled = _attempt(filename, content_types) - if not handled: - # Check for an index file if a folder was requested. - if index: - handled = _attempt(os.path.join(filename, index), content_types) - if handled: - request.is_index = filename[-1] in (r'\/') - return handled - - -def staticfile(filename, root=None, match='', content_types=None, debug=False): - """Serve a static resource from the given (root +) filename. - - match - If given, request.path_info will be searched for the given - regular expression before attempting to serve static content. - - content_types - If given, it should be a Python dictionary of - {file-extension: content-type} pairs, where 'file-extension' is - a string (e.g. "gif") and 'content-type' is the value to write - out in the Content-Type response header (e.g. "image/gif"). - - """ - request = cherrypy.serving.request - if request.method not in ('GET', 'HEAD'): - if debug: - cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') - return False - - if match and not re.search(match, request.path_info): - if debug: - cherrypy.log('request.path_info %r does not match pattern %r' % - (request.path_info, match), 'TOOLS.STATICFILE') - return False - - # If filename is relative, make absolute using "root". - if not os.path.isabs(filename): - if not root: - msg = "Static tool requires an absolute filename (got '%s')." % ( - filename,) - if debug: - cherrypy.log(msg, 'TOOLS.STATICFILE') - raise ValueError(msg) - filename = os.path.join(root, filename) - - return _attempt(filename, content_types, debug=debug) diff --git a/libraries/cherrypy/lib/xmlrpcutil.py b/libraries/cherrypy/lib/xmlrpcutil.py deleted file mode 100644 index ddaac86a..00000000 --- a/libraries/cherrypy/lib/xmlrpcutil.py +++ /dev/null @@ -1,61 +0,0 @@ -"""XML-RPC tool helpers.""" -import sys - -from six.moves.xmlrpc_client import ( - loads as xmlrpc_loads, dumps as xmlrpc_dumps, - Fault as XMLRPCFault -) - -import cherrypy -from cherrypy._cpcompat import ntob - - -def process_body(): - """Return (params, method) from request body.""" - try: - return xmlrpc_loads(cherrypy.request.body.read()) - except Exception: - return ('ERROR PARAMS', ), 'ERRORMETHOD' - - -def patched_path(path): - """Return 'path', doctored for RPC.""" - if not path.endswith('/'): - path += '/' - if path.startswith('/RPC2/'): - # strip the first /rpc2 - path = path[5:] - return path - - -def _set_response(body): - """Set up HTTP status, headers and body within CherryPy.""" - # The XML-RPC spec (http://www.xmlrpc.com/spec) says: - # "Unless there's a lower-level error, always return 200 OK." - # Since Python's xmlrpc_client interprets a non-200 response - # as a "Protocol Error", we'll just return 200 every time. - response = cherrypy.response - response.status = '200 OK' - response.body = ntob(body, 'utf-8') - response.headers['Content-Type'] = 'text/xml' - response.headers['Content-Length'] = len(body) - - -def respond(body, encoding='utf-8', allow_none=0): - """Construct HTTP response body.""" - if not isinstance(body, XMLRPCFault): - body = (body,) - - _set_response( - xmlrpc_dumps( - body, methodresponse=1, - encoding=encoding, - allow_none=allow_none - ) - ) - - -def on_error(*args, **kwargs): - """Construct HTTP response body for an error response.""" - body = str(sys.exc_info()[1]) - _set_response(xmlrpc_dumps(XMLRPCFault(1, body))) diff --git a/libraries/cherrypy/process/__init__.py b/libraries/cherrypy/process/__init__.py deleted file mode 100644 index f242d226..00000000 --- a/libraries/cherrypy/process/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Site container for an HTTP server. - -A Web Site Process Bus object is used to connect applications, servers, -and frameworks with site-wide services such as daemonization, process -reload, signal handling, drop privileges, PID file management, logging -for all of these, and many more. - -The 'plugins' module defines a few abstract and concrete services for -use with the bus. Some use tool-specific channels; see the documentation -for each class. -""" - -from .wspbus import bus -from . import plugins, servers - - -__all__ = ('bus', 'plugins', 'servers') diff --git a/libraries/cherrypy/process/plugins.py b/libraries/cherrypy/process/plugins.py deleted file mode 100644 index 8c246c81..00000000 --- a/libraries/cherrypy/process/plugins.py +++ /dev/null @@ -1,752 +0,0 @@ -"""Site services for use with a Web Site Process Bus.""" - -import os -import re -import signal as _signal -import sys -import time -import threading - -from six.moves import _thread - -from cherrypy._cpcompat import text_or_bytes -from cherrypy._cpcompat import ntob, Timer - -# _module__file__base is used by Autoreload to make -# absolute any filenames retrieved from sys.modules which are not -# already absolute paths. This is to work around Python's quirk -# of importing the startup script and using a relative filename -# for it in sys.modules. -# -# Autoreload examines sys.modules afresh every time it runs. If an application -# changes the current directory by executing os.chdir(), then the next time -# Autoreload runs, it will not be able to find any filenames which are -# not absolute paths, because the current directory is not the same as when the -# module was first imported. Autoreload will then wrongly conclude the file -# has "changed", and initiate the shutdown/re-exec sequence. -# See ticket #917. -# For this workaround to have a decent probability of success, this module -# needs to be imported as early as possible, before the app has much chance -# to change the working directory. -_module__file__base = os.getcwd() - - -class SimplePlugin(object): - - """Plugin base class which auto-subscribes methods for known channels.""" - - bus = None - """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine. - """ - - def __init__(self, bus): - self.bus = bus - - def subscribe(self): - """Register this object as a (multi-channel) listener on the bus.""" - for channel in self.bus.listeners: - # Subscribe self.start, self.exit, etc. if present. - method = getattr(self, channel, None) - if method is not None: - self.bus.subscribe(channel, method) - - def unsubscribe(self): - """Unregister this object as a listener on the bus.""" - for channel in self.bus.listeners: - # Unsubscribe self.start, self.exit, etc. if present. - method = getattr(self, channel, None) - if method is not None: - self.bus.unsubscribe(channel, method) - - -class SignalHandler(object): - - """Register bus channels (and listeners) for system signals. - - You can modify what signals your application listens for, and what it does - when it receives signals, by modifying :attr:`SignalHandler.handlers`, - a dict of {signal name: callback} pairs. The default set is:: - - handlers = {'SIGTERM': self.bus.exit, - 'SIGHUP': self.handle_SIGHUP, - 'SIGUSR1': self.bus.graceful, - } - - The :func:`SignalHandler.handle_SIGHUP`` method calls - :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>` - if the process is daemonized, but - :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>` - if the process is attached to a TTY. This is because Unix window - managers tend to send SIGHUP to terminal windows when the user closes them. - - Feel free to add signals which are not available on every platform. - The :class:`SignalHandler` will ignore errors raised from attempting - to register handlers for unknown signals. - """ - - handlers = {} - """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" - - signals = {} - """A map from signal numbers to names.""" - - for k, v in vars(_signal).items(): - if k.startswith('SIG') and not k.startswith('SIG_'): - signals[v] = k - del k, v - - def __init__(self, bus): - self.bus = bus - # Set default handlers - self.handlers = {'SIGTERM': self.bus.exit, - 'SIGHUP': self.handle_SIGHUP, - 'SIGUSR1': self.bus.graceful, - } - - if sys.platform[:4] == 'java': - del self.handlers['SIGUSR1'] - self.handlers['SIGUSR2'] = self.bus.graceful - self.bus.log('SIGUSR1 cannot be set on the JVM platform. ' - 'Using SIGUSR2 instead.') - self.handlers['SIGINT'] = self._jython_SIGINT_handler - - self._previous_handlers = {} - # used to determine is the process is a daemon in `self._is_daemonized` - self._original_pid = os.getpid() - - def _jython_SIGINT_handler(self, signum=None, frame=None): - # See http://bugs.jython.org/issue1313 - self.bus.log('Keyboard Interrupt: shutting down bus') - self.bus.exit() - - def _is_daemonized(self): - """Return boolean indicating if the current process is - running as a daemon. - - The criteria to determine the `daemon` condition is to verify - if the current pid is not the same as the one that got used on - the initial construction of the plugin *and* the stdin is not - connected to a terminal. - - The sole validation of the tty is not enough when the plugin - is executing inside other process like in a CI tool - (Buildbot, Jenkins). - """ - return ( - self._original_pid != os.getpid() and - not os.isatty(sys.stdin.fileno()) - ) - - def subscribe(self): - """Subscribe self.handlers to signals.""" - for sig, func in self.handlers.items(): - try: - self.set_handler(sig, func) - except ValueError: - pass - - def unsubscribe(self): - """Unsubscribe self.handlers from signals.""" - for signum, handler in self._previous_handlers.items(): - signame = self.signals[signum] - - if handler is None: - self.bus.log('Restoring %s handler to SIG_DFL.' % signame) - handler = _signal.SIG_DFL - else: - self.bus.log('Restoring %s handler %r.' % (signame, handler)) - - try: - our_handler = _signal.signal(signum, handler) - if our_handler is None: - self.bus.log('Restored old %s handler %r, but our ' - 'handler was not registered.' % - (signame, handler), level=30) - except ValueError: - self.bus.log('Unable to restore %s handler %r.' % - (signame, handler), level=40, traceback=True) - - def set_handler(self, signal, listener=None): - """Subscribe a handler for the given signal (number or name). - - If the optional 'listener' argument is provided, it will be - subscribed as a listener for the given signal's channel. - - If the given signal name or number is not available on the current - platform, ValueError is raised. - """ - if isinstance(signal, text_or_bytes): - signum = getattr(_signal, signal, None) - if signum is None: - raise ValueError('No such signal: %r' % signal) - signame = signal - else: - try: - signame = self.signals[signal] - except KeyError: - raise ValueError('No such signal: %r' % signal) - signum = signal - - prev = _signal.signal(signum, self._handle_signal) - self._previous_handlers[signum] = prev - - if listener is not None: - self.bus.log('Listening for %s.' % signame) - self.bus.subscribe(signame, listener) - - def _handle_signal(self, signum=None, frame=None): - """Python signal handler (self.set_handler subscribes it for you).""" - signame = self.signals[signum] - self.bus.log('Caught signal %s.' % signame) - self.bus.publish(signame) - - def handle_SIGHUP(self): - """Restart if daemonized, else exit.""" - if self._is_daemonized(): - self.bus.log('SIGHUP caught while daemonized. Restarting.') - self.bus.restart() - else: - # not daemonized (may be foreground or background) - self.bus.log('SIGHUP caught but not daemonized. Exiting.') - self.bus.exit() - - -try: - import pwd - import grp -except ImportError: - pwd, grp = None, None - - -class DropPrivileges(SimplePlugin): - - """Drop privileges. uid/gid arguments not available on Windows. - - Special thanks to `Gavin Baker - <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_ - """ - - def __init__(self, bus, umask=None, uid=None, gid=None): - SimplePlugin.__init__(self, bus) - self.finalized = False - self.uid = uid - self.gid = gid - self.umask = umask - - @property - def uid(self): - """The uid under which to run. Availability: Unix.""" - return self._uid - - @uid.setter - def uid(self, val): - if val is not None: - if pwd is None: - self.bus.log('pwd module not available; ignoring uid.', - level=30) - val = None - elif isinstance(val, text_or_bytes): - val = pwd.getpwnam(val)[2] - self._uid = val - - @property - def gid(self): - """The gid under which to run. Availability: Unix.""" - return self._gid - - @gid.setter - def gid(self, val): - if val is not None: - if grp is None: - self.bus.log('grp module not available; ignoring gid.', - level=30) - val = None - elif isinstance(val, text_or_bytes): - val = grp.getgrnam(val)[2] - self._gid = val - - @property - def umask(self): - """The default permission mode for newly created files and directories. - - Usually expressed in octal format, for example, ``0644``. - Availability: Unix, Windows. - """ - return self._umask - - @umask.setter - def umask(self, val): - if val is not None: - try: - os.umask - except AttributeError: - self.bus.log('umask function not available; ignoring umask.', - level=30) - val = None - self._umask = val - - def start(self): - # uid/gid - def current_ids(): - """Return the current (uid, gid) if available.""" - name, group = None, None - if pwd: - name = pwd.getpwuid(os.getuid())[0] - if grp: - group = grp.getgrgid(os.getgid())[0] - return name, group - - if self.finalized: - if not (self.uid is None and self.gid is None): - self.bus.log('Already running as uid: %r gid: %r' % - current_ids()) - else: - if self.uid is None and self.gid is None: - if pwd or grp: - self.bus.log('uid/gid not set', level=30) - else: - self.bus.log('Started as uid: %r gid: %r' % current_ids()) - if self.gid is not None: - os.setgid(self.gid) - os.setgroups([]) - if self.uid is not None: - os.setuid(self.uid) - self.bus.log('Running as uid: %r gid: %r' % current_ids()) - - # umask - if self.finalized: - if self.umask is not None: - self.bus.log('umask already set to: %03o' % self.umask) - else: - if self.umask is None: - self.bus.log('umask not set', level=30) - else: - old_umask = os.umask(self.umask) - self.bus.log('umask old: %03o, new: %03o' % - (old_umask, self.umask)) - - self.finalized = True - # This is slightly higher than the priority for server.start - # in order to facilitate the most common use: starting on a low - # port (which requires root) and then dropping to another user. - start.priority = 77 - - -class Daemonizer(SimplePlugin): - - """Daemonize the running script. - - Use this with a Web Site Process Bus via:: - - Daemonizer(bus).subscribe() - - When this component finishes, the process is completely decoupled from - the parent environment. Please note that when this component is used, - the return code from the parent process will still be 0 if a startup - error occurs in the forked children. Errors in the initial daemonizing - process still return proper exit codes. Therefore, if you use this - plugin to daemonize, don't use the return code as an accurate indicator - of whether the process fully started. In fact, that return code only - indicates if the process successfully finished the first fork. - """ - - def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', - stderr='/dev/null'): - SimplePlugin.__init__(self, bus) - self.stdin = stdin - self.stdout = stdout - self.stderr = stderr - self.finalized = False - - def start(self): - if self.finalized: - self.bus.log('Already deamonized.') - - # forking has issues with threads: - # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html - # "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: - self.bus.log('There are %r active threads. ' - 'Daemonizing now may cause strange failures.' % - threading.enumerate(), level=30) - - self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log) - - self.finalized = True - start.priority = 65 - - @staticmethod - def daemonize( - stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', - logger=lambda msg: None): - # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 - # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) - # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 - - # Finish up with the current stdout/stderr - sys.stdout.flush() - sys.stderr.flush() - - error_tmpl = ( - '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n' - ) - - for fork in range(2): - msg = ['Forking once.', 'Forking twice.'][fork] - try: - pid = os.fork() - if pid > 0: - # This is the parent; exit. - logger(msg) - os._exit(0) - except OSError as exc: - # Python raises OSError rather than returning negative numbers. - sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1)) - if fork == 0: - os.setsid() - - os.umask(0) - - si = open(stdin, 'r') - so = open(stdout, 'a+') - se = open(stderr, 'a+') - - # os.dup2(fd, fd2) will close fd2 if necessary, - # so we don't explicitly close stdin/out/err. - # See http://docs.python.org/lib/os-fd-ops.html - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - - logger('Daemonized to PID: %s' % os.getpid()) - - -class PIDFile(SimplePlugin): - - """Maintain a PID file via a WSPBus.""" - - def __init__(self, bus, pidfile): - SimplePlugin.__init__(self, bus) - self.pidfile = pidfile - self.finalized = False - - def start(self): - pid = os.getpid() - 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')) - self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) - self.finalized = True - start.priority = 70 - - def exit(self): - try: - os.remove(self.pidfile) - self.bus.log('PID file removed: %r.' % self.pidfile) - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - pass - - -class PerpetualTimer(Timer): - - """A responsive subclass of threading.Timer whose run() method repeats. - - Use this timer only when you really need a very interruptible timer; - this checks its 'finished' condition up to 20 times a second, which can - results in pretty high CPU usage - """ - - def __init__(self, *args, **kwargs): - "Override parent constructor to allow 'bus' to be provided." - self.bus = kwargs.pop('bus', None) - super(PerpetualTimer, self).__init__(*args, **kwargs) - - def run(self): - while True: - self.finished.wait(self.interval) - if self.finished.isSet(): - return - try: - self.function(*self.args, **self.kwargs) - except Exception: - if self.bus: - self.bus.log( - 'Error in perpetual timer thread function %r.' % - self.function, level=40, traceback=True) - # Quit on first error to avoid massive logs. - raise - - -class BackgroundTask(threading.Thread): - - """A subclass of threading.Thread whose run() method repeats. - - Use this class for most repeating tasks. It uses time.sleep() to wait - for each interval, which isn't very responsive; that is, even if you call - self.cancel(), you'll have to wait until the sleep() call finishes before - the thread stops. To compensate, it defaults to being daemonic, which means - it won't delay stopping the whole process. - """ - - def __init__(self, interval, function, args=[], kwargs={}, bus=None): - super(BackgroundTask, self).__init__() - self.interval = interval - self.function = function - self.args = args - self.kwargs = kwargs - self.running = False - self.bus = bus - - # default to daemonic - self.daemon = True - - def cancel(self): - self.running = False - - def run(self): - self.running = True - while self.running: - time.sleep(self.interval) - if not self.running: - return - try: - self.function(*self.args, **self.kwargs) - except Exception: - if self.bus: - self.bus.log('Error in background task thread function %r.' - % self.function, level=40, traceback=True) - # Quit on first error to avoid massive logs. - raise - - -class Monitor(SimplePlugin): - - """WSPBus listener to periodically run a callback in its own thread.""" - - callback = None - """The function to call at intervals.""" - - frequency = 60 - """The time in seconds between callback runs.""" - - thread = None - """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` - thread. - """ - - def __init__(self, bus, callback, frequency=60, name=None): - SimplePlugin.__init__(self, bus) - self.callback = callback - self.frequency = frequency - self.thread = None - self.name = name - - def start(self): - """Start our callback in its own background thread.""" - if self.frequency > 0: - threadname = self.name or self.__class__.__name__ - if self.thread is None: - self.thread = BackgroundTask(self.frequency, self.callback, - bus=self.bus) - self.thread.setName(threadname) - self.thread.start() - self.bus.log('Started monitor thread %r.' % threadname) - else: - self.bus.log('Monitor thread %r already started.' % threadname) - start.priority = 70 - - def stop(self): - """Stop our callback's background task thread.""" - if self.thread is None: - 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() - self.thread.cancel() - if not self.thread.daemon: - self.bus.log('Joining %r' % name) - self.thread.join() - self.bus.log('Stopped thread %r.' % name) - self.thread = None - - def graceful(self): - """Stop the callback's background task thread and restart it.""" - self.stop() - self.start() - - -class Autoreloader(Monitor): - - """Monitor which re-executes the process when files change. - - This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`) - if any of the files it monitors change (or is deleted). By default, the - autoreloader monitors all imported modules; you can add to the - set by adding to ``autoreload.files``:: - - cherrypy.engine.autoreload.files.add(myFile) - - If there are imported files you do *not* wish to monitor, you can - adjust the ``match`` attribute, a regular expression. For example, - to stop monitoring cherrypy itself:: - - cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' - - Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins, - the autoreload plugin takes a ``frequency`` argument. The default is - 1 second; that is, the autoreloader will examine files once each second. - """ - - files = None - """The set of files to poll for modifications.""" - - frequency = 1 - """The interval in seconds at which to poll for modified files.""" - - match = '.*' - """A regular expression by which to match filenames.""" - - def __init__(self, bus, frequency=1, match='.*'): - self.mtimes = {} - self.files = set() - self.match = match - Monitor.__init__(self, bus, self.run, frequency) - - def start(self): - """Start our own background task thread for self.run.""" - if self.thread is None: - self.mtimes = {} - Monitor.start(self) - start.priority = 70 - - def sysfiles(self): - """Return a Set of sys.modules filenames to monitor.""" - search_mod_names = filter(re.compile(self.match).match, sys.modules) - mods = map(sys.modules.get, search_mod_names) - return set(filter(None, map(self._file_for_module, mods))) - - @classmethod - def _file_for_module(cls, module): - """Return the relevant file for the module.""" - return ( - cls._archive_for_zip_module(module) - or cls._file_for_file_module(module) - ) - - @staticmethod - def _archive_for_zip_module(module): - """Return the archive filename for the module if relevant.""" - try: - return module.__loader__.archive - except AttributeError: - pass - - @classmethod - def _file_for_file_module(cls, module): - """Return the file for the module.""" - try: - return module.__file__ and cls._make_absolute(module.__file__) - except AttributeError: - pass - - @staticmethod - def _make_absolute(filename): - """Ensure filename is absolute to avoid effect of os.chdir.""" - return filename if os.path.isabs(filename) else ( - os.path.normpath(os.path.join(_module__file__base, filename)) - ) - - def run(self): - """Reload the process if registered files have been modified.""" - for filename in self.sysfiles() | self.files: - if filename: - if filename.endswith('.pyc'): - filename = filename[:-1] - - oldtime = self.mtimes.get(filename, 0) - if oldtime is None: - # Module with no .py file. Skip it. - continue - - try: - mtime = os.stat(filename).st_mtime - except OSError: - # Either a module with no .py file, or it's been deleted. - mtime = None - - if filename not in self.mtimes: - # If a module has no .py file, this will be None. - self.mtimes[filename] = mtime - else: - if mtime is None or mtime > oldtime: - # The file has been deleted or modified. - self.bus.log('Restarting because %s changed.' % - filename) - self.thread.cancel() - self.bus.log('Stopped thread %r.' % - self.thread.getName()) - self.bus.restart() - return - - -class ThreadManager(SimplePlugin): - - """Manager for HTTP request threads. - - If you have control over thread creation and destruction, publish to - the 'acquire_thread' and 'release_thread' channels (for each thread). - This will register/unregister the current thread and publish to - 'start_thread' and 'stop_thread' listeners in the bus as needed. - - If threads are created and destroyed by code you do not control - (e.g., Apache), then, at the beginning of every HTTP request, - publish to 'acquire_thread' only. You should not publish to - 'release_thread' in this case, since you do not know whether - the thread will be re-used or not. The bus will call - 'stop_thread' listeners for you when it stops. - """ - - threads = None - """A map of {thread ident: index number} pairs.""" - - def __init__(self, bus): - self.threads = {} - SimplePlugin.__init__(self, bus) - self.bus.listeners.setdefault('acquire_thread', set()) - self.bus.listeners.setdefault('start_thread', set()) - self.bus.listeners.setdefault('release_thread', set()) - self.bus.listeners.setdefault('stop_thread', set()) - - def acquire_thread(self): - """Run 'start_thread' listeners for the current thread. - - If the current thread has already been seen, any 'start_thread' - listeners will not be run again. - """ - thread_ident = _thread.get_ident() - if thread_ident not in self.threads: - # We can't just use get_ident as the thread ID - # because some platforms reuse thread ID's. - i = len(self.threads) + 1 - self.threads[thread_ident] = i - self.bus.publish('start_thread', i) - - def release_thread(self): - """Release the current thread and run 'stop_thread' listeners.""" - thread_ident = _thread.get_ident() - i = self.threads.pop(thread_ident, None) - if i is not None: - self.bus.publish('stop_thread', i) - - def stop(self): - """Release all threads and run all 'stop_thread' listeners.""" - for thread_ident, i in self.threads.items(): - self.bus.publish('stop_thread', i) - self.threads.clear() - graceful = stop diff --git a/libraries/cherrypy/process/servers.py b/libraries/cherrypy/process/servers.py deleted file mode 100644 index dcb34de6..00000000 --- a/libraries/cherrypy/process/servers.py +++ /dev/null @@ -1,416 +0,0 @@ -r""" -Starting in CherryPy 3.1, cherrypy.server is implemented as an -:ref:`Engine Plugin<plugins>`. It's an instance of -:class:`cherrypy._cpserver.Server`, which is a subclass of -:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class -is designed to control other servers, as well. - -Multiple servers/ports -====================== - -If you need to start more than one HTTP server (to serve on multiple ports, or -protocols, etc.), you can manually register each one and then start them all -with engine.start:: - - s1 = ServerAdapter( - cherrypy.engine, - MyWSGIServer(host='0.0.0.0', port=80) - ) - s2 = ServerAdapter( - cherrypy.engine, - another.HTTPServer(host='127.0.0.1', SSL=True) - ) - s1.subscribe() - s2.subscribe() - cherrypy.engine.start() - -.. index:: SCGI - -FastCGI/SCGI -============ - -There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in -:mod:`cherrypy.process.servers`. To start an fcgi server, for example, -wrap an instance of it in a ServerAdapter:: - - addr = ('0.0.0.0', 4000) - f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) - s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) - s.subscribe() - -The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for -you via its `-f` flag. -Note that you need to download and install `flup <http://trac.saddi.com/flup>`_ -yourself, whether you use ``cherryd`` or not. - -.. _fastcgi: -.. index:: FastCGI - -FastCGI -------- - -A very simple setup lets your cherry run with FastCGI. -You just need the flup library, -plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. - -CherryPy code -^^^^^^^^^^^^^ - -hello.py:: - - #!/usr/bin/python - import cherrypy - - class HelloWorld: - '''Sample request handler class.''' - @cherrypy.expose - def index(self): - return "Hello world!" - - cherrypy.tree.mount(HelloWorld()) - # CherryPy autoreload must be disabled for the flup server to work - cherrypy.config.update({'engine.autoreload.on':False}) - -Then run :doc:`/deployguide/cherryd` with the '-f' arg:: - - cherryd -c <myconfig> -d -f -i hello.py - -Apache -^^^^^^ - -At the top level in httpd.conf:: - - FastCgiIpcDir /tmp - FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 - -And inside the relevant VirtualHost section:: - - # FastCGI config - AddHandler fastcgi-script .fcgi - ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 - -Lighttpd -^^^^^^^^ - -For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these -instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is -active within ``server.modules``. Then, within your ``$HTTP["host"]`` -directive, configure your fastcgi script like the following:: - - $HTTP["url"] =~ "" { - fastcgi.server = ( - "/" => ( - "script.fcgi" => ( - "bin-path" => "/path/to/your/script.fcgi", - "socket" => "/tmp/script.sock", - "check-local" => "disable", - "disable-time" => 1, - "min-procs" => 1, - "max-procs" => 1, # adjust as needed - ), - ), - ) - } # end of $HTTP["url"] =~ "^/" - -Please see `Lighttpd FastCGI Docs -<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for -an explanation of the possible configuration options. -""" - -import os -import sys -import time -import warnings -import contextlib - -import portend - - -class Timeouts: - occupied = 5 - free = 1 - - -class ServerAdapter(object): - - """Adapter for an HTTP server. - - If you need to start more than one HTTP server (to serve on multiple - ports, or protocols, etc.), you can manually register each one and then - start them all with bus.start:: - - s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) - s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) - s1.subscribe() - s2.subscribe() - bus.start() - """ - - def __init__(self, bus, httpserver=None, bind_addr=None): - self.bus = bus - self.httpserver = httpserver - self.bind_addr = bind_addr - self.interrupt = None - self.running = False - - def subscribe(self): - self.bus.subscribe('start', self.start) - self.bus.subscribe('stop', self.stop) - - def unsubscribe(self): - self.bus.unsubscribe('start', self.start) - self.bus.unsubscribe('stop', self.stop) - - def start(self): - """Start the HTTP server.""" - if self.running: - self.bus.log('Already serving on %s' % self.description) - return - - self.interrupt = None - if not self.httpserver: - raise ValueError('No HTTP server has been created.') - - if not os.environ.get('LISTEN_PID', None): - # Start the httpserver in a new thread. - if isinstance(self.bind_addr, tuple): - portend.free(*self.bind_addr, timeout=Timeouts.free) - - import threading - t = threading.Thread(target=self._start_http_thread) - t.setName('HTTPServer ' + t.getName()) - t.start() - - self.wait() - self.running = True - self.bus.log('Serving on %s' % self.description) - start.priority = 75 - - @property - def description(self): - """ - A description about where this server is bound. - """ - if self.bind_addr is None: - on_what = 'unknown interface (dynamic?)' - elif isinstance(self.bind_addr, tuple): - on_what = self._get_base() - else: - on_what = 'socket file: %s' % self.bind_addr - return on_what - - def _get_base(self): - if not self.httpserver: - return '' - host, port = self.bound_addr - if getattr(self.httpserver, 'ssl_adapter', None): - scheme = 'https' - if port != 443: - host += ':%s' % port - else: - scheme = 'http' - if port != 80: - host += ':%s' % port - - return '%s://%s' % (scheme, host) - - def _start_http_thread(self): - """HTTP servers MUST be running in new threads, so that the - main thread persists to receive KeyboardInterrupt's. If an - exception is raised in the httpserver's thread then it's - trapped here, and the bus (and therefore our httpserver) - are shut down. - """ - try: - self.httpserver.start() - except KeyboardInterrupt: - self.bus.log('<Ctrl-C> hit: shutting down HTTP server') - self.interrupt = sys.exc_info()[1] - self.bus.exit() - except SystemExit: - self.bus.log('SystemExit raised: shutting down HTTP server') - self.interrupt = sys.exc_info()[1] - self.bus.exit() - raise - except Exception: - self.interrupt = sys.exc_info()[1] - self.bus.log('Error in HTTP server: shutting down', - traceback=True, level=40) - self.bus.exit() - raise - - def wait(self): - """Wait until the HTTP server is ready to receive requests.""" - while not getattr(self.httpserver, 'ready', False): - if self.interrupt: - raise self.interrupt - time.sleep(.1) - - # bypass check when LISTEN_PID is set - if os.environ.get('LISTEN_PID', None): - return - - # bypass check when running via socket-activation - # (for socket-activation the port will be managed by systemd) - if not isinstance(self.bind_addr, tuple): - return - - # wait for port to be occupied - with _safe_wait(*self.bound_addr): - portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) - - @property - def bound_addr(self): - """ - The bind address, or if it's an ephemeral port and the - socket has been bound, return the actual port bound. - """ - host, port = self.bind_addr - if port == 0 and self.httpserver.socket: - # Bound to ephemeral port. Get the actual port allocated. - port = self.httpserver.socket.getsockname()[1] - return host, port - - def stop(self): - """Stop the HTTP server.""" - if self.running: - # stop() MUST block until the server is *truly* stopped. - self.httpserver.stop() - # Wait for the socket to be truly freed. - if isinstance(self.bind_addr, tuple): - portend.free(*self.bound_addr, timeout=Timeouts.free) - self.running = False - self.bus.log('HTTP Server %s shut down' % self.httpserver) - else: - self.bus.log('HTTP Server %s already shut down' % self.httpserver) - stop.priority = 25 - - def restart(self): - """Restart the HTTP server.""" - self.stop() - self.start() - - -class FlupCGIServer(object): - - """Adapter for a flup.server.cgi.WSGIServer.""" - - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - self.ready = False - - def start(self): - """Start the CGI server.""" - # We have to instantiate the server class here because its __init__ - # starts a threadpool. If we do it too early, daemonize won't work. - from flup.server.cgi import WSGIServer - - self.cgiserver = WSGIServer(*self.args, **self.kwargs) - self.ready = True - self.cgiserver.run() - - def stop(self): - """Stop the HTTP server.""" - self.ready = False - - -class FlupFCGIServer(object): - - """Adapter for a flup.server.fcgi.WSGIServer.""" - - def __init__(self, *args, **kwargs): - if kwargs.get('bindAddress', None) is None: - import socket - if not hasattr(socket, 'fromfd'): - raise ValueError( - 'Dynamic FCGI server not available on this platform. ' - 'You must use a static or external one by providing a ' - 'legal bindAddress.') - self.args = args - self.kwargs = kwargs - self.ready = False - - def start(self): - """Start the FCGI server.""" - # We have to instantiate the server class here because its __init__ - # starts a threadpool. If we do it too early, daemonize won't work. - from flup.server.fcgi import WSGIServer - self.fcgiserver = WSGIServer(*self.args, **self.kwargs) - # TODO: report this bug upstream to flup. - # If we don't set _oldSIGs on Windows, we get: - # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", - # line 108, in run - # self._restoreSignalHandlers() - # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", - # line 156, in _restoreSignalHandlers - # for signum,handler in self._oldSIGs: - # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' - self.fcgiserver._installSignalHandlers = lambda: None - self.fcgiserver._oldSIGs = [] - self.ready = True - self.fcgiserver.run() - - def stop(self): - """Stop the HTTP server.""" - # Forcibly stop the fcgi server main event loop. - self.fcgiserver._keepGoing = False - # Force all worker threads to die off. - self.fcgiserver._threadPool.maxSpare = ( - self.fcgiserver._threadPool._idleCount) - self.ready = False - - -class FlupSCGIServer(object): - - """Adapter for a flup.server.scgi.WSGIServer.""" - - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - self.ready = False - - def start(self): - """Start the SCGI server.""" - # We have to instantiate the server class here because its __init__ - # starts a threadpool. If we do it too early, daemonize won't work. - from flup.server.scgi import WSGIServer - self.scgiserver = WSGIServer(*self.args, **self.kwargs) - # TODO: report this bug upstream to flup. - # If we don't set _oldSIGs on Windows, we get: - # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", - # line 108, in run - # self._restoreSignalHandlers() - # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", - # line 156, in _restoreSignalHandlers - # for signum,handler in self._oldSIGs: - # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' - self.scgiserver._installSignalHandlers = lambda: None - self.scgiserver._oldSIGs = [] - self.ready = True - self.scgiserver.run() - - def stop(self): - """Stop the HTTP server.""" - self.ready = False - # Forcibly stop the scgi server main event loop. - self.scgiserver._keepGoing = False - # Force all worker threads to die off. - self.scgiserver._threadPool.maxSpare = 0 - - -@contextlib.contextmanager -def _safe_wait(host, port): - """ - On systems where a loopback interface is not available and the - server is bound to all interfaces, it's difficult to determine - whether the server is in fact occupying the port. In this case, - just issue a warning and move on. See issue #1100. - """ - try: - yield - except portend.Timeout: - if host == portend.client_host(host): - raise - msg = 'Unable to verify that the server is bound on %r' % port - warnings.warn(msg) diff --git a/libraries/cherrypy/process/win32.py b/libraries/cherrypy/process/win32.py deleted file mode 100644 index 096b0278..00000000 --- a/libraries/cherrypy/process/win32.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Windows service. Requires pywin32.""" - -import os -import win32api -import win32con -import win32event -import win32service -import win32serviceutil - -from cherrypy.process import wspbus, plugins - - -class ConsoleCtrlHandler(plugins.SimplePlugin): - - """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" - - def __init__(self, bus): - self.is_set = False - plugins.SimplePlugin.__init__(self, bus) - - def start(self): - if self.is_set: - self.bus.log('Handler for console events already set.', level=40) - return - - result = win32api.SetConsoleCtrlHandler(self.handle, 1) - if result == 0: - self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % - win32api.GetLastError(), level=40) - else: - self.bus.log('Set handler for console events.', level=40) - self.is_set = True - - def stop(self): - if not self.is_set: - self.bus.log('Handler for console events already off.', level=40) - return - - try: - result = win32api.SetConsoleCtrlHandler(self.handle, 0) - except ValueError: - # "ValueError: The object has not been registered" - result = 1 - - if result == 0: - self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % - win32api.GetLastError(), level=40) - else: - self.bus.log('Removed handler for console events.', level=40) - self.is_set = False - - def handle(self, event): - """Handle console control events (like Ctrl-C).""" - if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, - win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, - win32con.CTRL_CLOSE_EVENT): - self.bus.log('Console event %s: shutting down bus' % event) - - # Remove self immediately so repeated Ctrl-C doesn't re-call it. - try: - self.stop() - except ValueError: - pass - - self.bus.exit() - # 'First to return True stops the calls' - return 1 - return 0 - - -class Win32Bus(wspbus.Bus): - - """A Web Site Process Bus implementation for Win32. - - Instead of time.sleep, this bus blocks using native win32event objects. - """ - - def __init__(self): - self.events = {} - wspbus.Bus.__init__(self) - - def _get_state_event(self, state): - """Return a win32event for the given state (creating it if needed).""" - try: - return self.events[state] - except KeyError: - event = win32event.CreateEvent(None, 0, 0, - 'WSPBus %s Event (pid=%r)' % - (state.name, os.getpid())) - self.events[state] = event - return event - - @property - def state(self): - return self._state - - @state.setter - def state(self, value): - self._state = value - event = self._get_state_event(value) - win32event.PulseEvent(event) - - def wait(self, state, interval=0.1, channel=None): - """Wait for the given state(s), KeyboardInterrupt or SystemExit. - - Since this class uses native win32event objects, the interval - argument is ignored. - """ - if isinstance(state, (tuple, list)): - # Don't wait for an event that beat us to the punch ;) - if self.state not in state: - events = tuple([self._get_state_event(s) for s in state]) - win32event.WaitForMultipleObjects( - events, 0, win32event.INFINITE) - else: - # Don't wait for an event that beat us to the punch ;) - if self.state != state: - event = self._get_state_event(state) - win32event.WaitForSingleObject(event, win32event.INFINITE) - - -class _ControlCodes(dict): - - """Control codes used to "signal" a service via ControlService. - - User-defined control codes are in the range 128-255. We generally use - the standard Python value for the Linux signal and add 128. Example: - - >>> signal.SIGUSR1 - 10 - control_codes['graceful'] = 128 + 10 - """ - - def key_for(self, obj): - """For the given value, return its corresponding key.""" - for key, val in self.items(): - if val is obj: - return key - raise ValueError('The given object could not be found: %r' % obj) - - -control_codes = _ControlCodes({'graceful': 138}) - - -def signal_child(service, command): - if command == 'stop': - win32serviceutil.StopService(service) - elif command == 'restart': - win32serviceutil.RestartService(service) - else: - win32serviceutil.ControlService(service, control_codes[command]) - - -class PyWebService(win32serviceutil.ServiceFramework): - - """Python Web Service.""" - - _svc_name_ = 'Python Web Service' - _svc_display_name_ = 'Python Web Service' - _svc_deps_ = None # sequence of service names on which this depends - _exe_name_ = 'pywebsvc' - _exe_args_ = None # Default to no arguments - - # Only exists on Windows 2000 or later, ignored on windows NT - _svc_description_ = 'Python Web Service' - - def SvcDoRun(self): - from cherrypy import process - process.bus.start() - process.bus.block() - - def SvcStop(self): - from cherrypy import process - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - process.bus.exit() - - def SvcOther(self, control): - from cherrypy import process - process.bus.publish(control_codes.key_for(control)) - - -if __name__ == '__main__': - win32serviceutil.HandleCommandLine(PyWebService) diff --git a/libraries/cherrypy/process/wspbus.py b/libraries/cherrypy/process/wspbus.py deleted file mode 100644 index 39ac45bf..00000000 --- a/libraries/cherrypy/process/wspbus.py +++ /dev/null @@ -1,590 +0,0 @@ -r"""An implementation of the Web Site Process Bus. - -This module is completely standalone, depending only on the stdlib. - -Web Site Process Bus --------------------- - -A Bus object is used to contain and manage site-wide behavior: -daemonization, HTTP server start/stop, process reload, signal handling, -drop privileges, PID file management, logging for all of these, -and many more. - -In addition, a Bus object provides a place for each web framework -to register code that runs in response to site-wide events (like -process start and stop), or which controls or otherwise interacts with -the site-wide components mentioned above. For example, a framework which -uses file-based templates would add known template filenames to an -autoreload component. - -Ideally, a Bus object will be flexible enough to be useful in a variety -of invocation scenarios: - - 1. The deployer starts a site from the command line via a - framework-neutral deployment script; applications from multiple frameworks - are mixed in a single site. Command-line arguments and configuration - files are used to define site-wide components such as the HTTP server, - WSGI component graph, autoreload behavior, signal handling, etc. - 2. The deployer starts a site via some other process, such as Apache; - applications from multiple frameworks are mixed in a single site. - Autoreload and signal handling (from Python at least) are disabled. - 3. The deployer starts a site via a framework-specific mechanism; - for example, when running tests, exploring tutorials, or deploying - single applications from a single framework. The framework controls - which site-wide components are enabled as it sees fit. - -The Bus object in this package uses topic-based publish-subscribe -messaging to accomplish all this. A few topic channels are built in -('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and -site containers are free to define their own. If a message is sent to a -channel that has not been defined or has no listeners, there is no effect. - -In general, there should only ever be a single Bus object per process. -Frameworks and site containers share a single Bus object by publishing -messages and subscribing listeners. - -The Bus object works as a finite state machine which models the current -state of the process. Bus methods move it from one state to another; -those methods then publish to subscribed listeners on the channel for -the new state.:: - - O - | - V - STOPPING --> STOPPED --> EXITING -> X - A A | - | \___ | - | \ | - | V V - STARTED <-- STARTING - -""" - -import atexit - -try: - import ctypes -except (ImportError, MemoryError): - """Google AppEngine is shipped without ctypes - - :seealso: http://stackoverflow.com/a/6523777/70170 - """ - ctypes = None - -import operator -import os -import sys -import threading -import time -import traceback as _traceback -import warnings -import subprocess -import functools - -import six - - -# Here I save the value of os.getcwd(), which, if I am imported early enough, -# will be the directory from which the startup script was run. This is needed -# by _do_execv(), to change back to the original directory before execv()ing a -# new process. This is a defense against the application having changed the -# current working directory (which could make sys.executable "not found" if -# sys.executable is a relative-path, and/or cause other problems). -_startup_cwd = os.getcwd() - - -class ChannelFailures(Exception): - """Exception raised during errors on Bus.publish().""" - - delimiter = '\n' - - def __init__(self, *args, **kwargs): - """Initialize ChannelFailures errors wrapper.""" - super(ChannelFailures, self).__init__(*args, **kwargs) - self._exceptions = list() - - def handle_exception(self): - """Append the current exception to self.""" - self._exceptions.append(sys.exc_info()[1]) - - def get_instances(self): - """Return a list of seen exception instances.""" - return self._exceptions[:] - - def __str__(self): - """Render the list of errors, which happened in channel.""" - exception_strings = map(repr, self.get_instances()) - return self.delimiter.join(exception_strings) - - __repr__ = __str__ - - def __bool__(self): - """Determine whether any error happened in channel.""" - return bool(self._exceptions) - __nonzero__ = __bool__ - -# Use a flag to indicate the state of the bus. - - -class _StateEnum(object): - - class State(object): - name = None - - def __repr__(self): - return 'states.%s' % self.name - - def __setattr__(self, key, value): - if isinstance(value, self.State): - value.name = key - object.__setattr__(self, key, value) - - -states = _StateEnum() -states.STOPPED = states.State() -states.STARTING = states.State() -states.STARTED = states.State() -states.STOPPING = states.State() -states.EXITING = states.State() - - -try: - import fcntl -except ImportError: - max_files = 0 -else: - try: - max_files = os.sysconf('SC_OPEN_MAX') - except AttributeError: - max_files = 1024 - - -class Bus(object): - """Process state-machine and messenger for HTTP site deployment. - - All listeners for a given channel are guaranteed to be called even - if others at the same channel fail. Each failure is logged, but - execution proceeds on to the next listener. The only way to stop all - processing from inside a listener is to raise SystemExit and stop the - whole server. - """ - - states = states - state = states.STOPPED - execv = False - max_cloexec_files = max_files - - def __init__(self): - """Initialize pub/sub bus.""" - self.execv = False - self.state = states.STOPPED - channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main' - self.listeners = dict( - (channel, set()) - for channel in channels - ) - self._priorities = {} - - def subscribe(self, channel, callback=None, priority=None): - """Add the given callback at the given channel (if not present). - - If callback is None, return a partial suitable for decorating - the callback. - """ - if callback is None: - return functools.partial( - self.subscribe, - channel, - priority=priority, - ) - - ch_listeners = self.listeners.setdefault(channel, set()) - ch_listeners.add(callback) - - if priority is None: - priority = getattr(callback, 'priority', 50) - self._priorities[(channel, callback)] = priority - - def unsubscribe(self, channel, callback): - """Discard the given callback (if present).""" - listeners = self.listeners.get(channel) - if listeners and callback in listeners: - listeners.discard(callback) - del self._priorities[(channel, callback)] - - def publish(self, channel, *args, **kwargs): - """Return output of all subscribers for the given channel.""" - if channel not in self.listeners: - return [] - - exc = ChannelFailures() - output = [] - - raw_items = ( - (self._priorities[(channel, listener)], listener) - for listener in self.listeners[channel] - ) - items = sorted(raw_items, key=operator.itemgetter(0)) - for priority, listener in items: - try: - output.append(listener(*args, **kwargs)) - except KeyboardInterrupt: - raise - except SystemExit: - e = sys.exc_info()[1] - # If we have previous errors ensure the exit code is non-zero - if exc and e.code == 0: - e.code = 1 - raise - except Exception: - exc.handle_exception() - if channel == 'log': - # Assume any further messages to 'log' will fail. - pass - else: - self.log('Error in %r listener %r' % (channel, listener), - level=40, traceback=True) - if exc: - raise exc - return output - - def _clean_exit(self): - """Assert that the Bus is not running in atexit handler callback.""" - if self.state != states.EXITING: - warnings.warn( - 'The main thread is exiting, but the Bus is in the %r state; ' - 'shutting it down automatically now. You must either call ' - 'bus.block() after start(), or call bus.exit() before the ' - 'main thread exits.' % self.state, RuntimeWarning) - self.exit() - - def start(self): - """Start all services.""" - atexit.register(self._clean_exit) - - self.state = states.STARTING - self.log('Bus STARTING') - try: - self.publish('start') - self.state = states.STARTED - self.log('Bus STARTED') - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - self.log('Shutting down due to error in start listener:', - level=40, traceback=True) - e_info = sys.exc_info()[1] - try: - self.exit() - except Exception: - # Any stop/exit errors will be logged inside publish(). - pass - # Re-raise the original error - raise e_info - - def exit(self): - """Stop all services and prepare to exit the process.""" - exitstate = self.state - EX_SOFTWARE = 70 - try: - self.stop() - - self.state = states.EXITING - self.log('Bus EXITING') - self.publish('exit') - # This isn't strictly necessary, but it's better than seeing - # "Waiting for child threads to terminate..." and then nothing. - self.log('Bus EXITED') - except Exception: - # This method is often called asynchronously (whether thread, - # signal handler, console handler, or atexit handler), so we - # can't just let exceptions propagate out unhandled. - # Assume it's been logged and just die. - os._exit(EX_SOFTWARE) - - if exitstate == states.STARTING: - # exit() was called before start() finished, possibly due to - # Ctrl-C because a start listener got stuck. In this case, - # we could get stuck in a loop where Ctrl-C never exits the - # process, so we just call os.exit here. - os._exit(EX_SOFTWARE) - - def restart(self): - """Restart the process (may close connections). - - This method does not restart the process from the calling thread; - instead, it stops the bus and asks the main thread to call execv. - """ - self.execv = True - self.exit() - - def graceful(self): - """Advise all services to reload.""" - self.log('Bus graceful') - self.publish('graceful') - - def block(self, interval=0.1): - """Wait for the EXITING state, KeyboardInterrupt or SystemExit. - - This function is intended to be called only by the main thread. - After waiting for the EXITING state, it also waits for all threads - to terminate, and then calls os.execv if self.execv is True. This - design allows another thread to call bus.restart, yet have the main - thread perform the actual execv call (required on some platforms). - """ - try: - self.wait(states.EXITING, interval=interval, channel='main') - except (KeyboardInterrupt, IOError): - # The time.sleep call might raise - # "IOError: [Errno 4] Interrupted function call" on KBInt. - self.log('Keyboard Interrupt: shutting down bus') - self.exit() - except SystemExit: - self.log('SystemExit raised: shutting down bus') - self.exit() - raise - - # Waiting for ALL child threads to finish is necessary on OS X. - # See https://github.com/cherrypy/cherrypy/issues/581. - # It's also good to let them all shut down before allowing - # the main thread to call atexit handlers. - # See https://github.com/cherrypy/cherrypy/issues/751. - self.log('Waiting for child threads to terminate...') - for t in threading.enumerate(): - # Validate the we're not trying to join the MainThread - # that will cause a deadlock and the case exist when - # implemented as a windows service and in any other case - # that another thread executes cherrypy.engine.exit() - if ( - t != threading.currentThread() 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()) - t.join() - - if self.execv: - self._do_execv() - - def wait(self, state, interval=0.1, channel=None): - """Poll for the given state(s) at intervals; publish to channel.""" - if isinstance(state, (tuple, list)): - states = state - else: - states = [state] - - while self.state not in states: - time.sleep(interval) - self.publish(channel) - - def _do_execv(self): - """Re-execute the current process. - - This must be called from the main thread, because certain platforms - (OS X) don't allow execv to be called in a child thread very well. - """ - try: - args = self._get_true_argv() - except NotImplementedError: - """It's probably win32 or GAE""" - args = [sys.executable] + self._get_interpreter_argv() + sys.argv - - self.log('Re-spawning %s' % ' '.join(args)) - - self._extend_pythonpath(os.environ) - - if sys.platform[:4] == 'java': - from _systemrestart import SystemRestart - raise SystemRestart - else: - if sys.platform == 'win32': - args = ['"%s"' % arg for arg in args] - - os.chdir(_startup_cwd) - if self.max_cloexec_files: - self._set_cloexec() - os.execv(sys.executable, args) - - @staticmethod - def _get_interpreter_argv(): - """Retrieve current Python interpreter's arguments. - - Returns empty tuple in case of frozen mode, uses built-in arguments - reproduction function otherwise. - - Frozen mode is possible for the app has been packaged into a binary - executable using py2exe. In this case the interpreter's arguments are - already built-in into that executable. - - :seealso: https://github.com/cherrypy/cherrypy/issues/1526 - Ref: https://pythonhosted.org/PyInstaller/runtime-information.html - """ - return ([] - if getattr(sys, 'frozen', False) - else subprocess._args_from_interpreter_flags()) - - @staticmethod - def _get_true_argv(): - """Retrieve all real arguments of the python interpreter. - - ...even those not listed in ``sys.argv`` - - :seealso: http://stackoverflow.com/a/28338254/595220 - :seealso: http://stackoverflow.com/a/6683222/595220 - :seealso: http://stackoverflow.com/a/28414807/595220 - """ - try: - char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p - - argv = ctypes.POINTER(char_p)() - argc = ctypes.c_int() - - ctypes.pythonapi.Py_GetArgcArgv( - ctypes.byref(argc), - ctypes.byref(argv), - ) - - _argv = argv[:argc.value] - - # The code below is trying to correctly handle special cases. - # `-c`'s argument interpreted by Python itself becomes `-c` as - # well. Same applies to `-m`. This snippet is trying to survive - # at least the case with `-m` - # Ref: https://github.com/cherrypy/cherrypy/issues/1545 - # Ref: python/cpython@418baf9 - argv_len, is_command, is_module = len(_argv), False, False - - try: - m_ind = _argv.index('-m') - if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'): - """ - In some older Python versions `-m`'s argument may be - substituted with `-c`, not `-m` - """ - is_module = True - except (IndexError, ValueError): - m_ind = None - - try: - c_ind = _argv.index('-c') - if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c': - is_command = True - except (IndexError, ValueError): - c_ind = None - - if is_module: - """It's containing `-m -m` sequence of arguments""" - if is_command and c_ind < m_ind: - """There's `-c -c` before `-m`""" - raise RuntimeError( - "Cannot reconstruct command from '-c'. Ref: " - 'https://github.com/cherrypy/cherrypy/issues/1545') - # Survive module argument here - original_module = sys.argv[0] - if not os.access(original_module, os.R_OK): - """There's no such module exist""" - raise AttributeError( - "{} doesn't seem to be a module " - 'accessible by current user'.format(original_module)) - del _argv[m_ind:m_ind + 2] # remove `-m -m` - # ... and substitute it with the original module path: - _argv.insert(m_ind, original_module) - elif is_command: - """It's containing just `-c -c` sequence of arguments""" - raise RuntimeError( - "Cannot reconstruct command from '-c'. " - 'Ref: https://github.com/cherrypy/cherrypy/issues/1545') - except AttributeError: - """It looks Py_GetArgcArgv is completely absent in some environments - - It is known, that there's no Py_GetArgcArgv in MS Windows and - ``ctypes`` module is completely absent in Google AppEngine - - :seealso: https://github.com/cherrypy/cherrypy/issues/1506 - :seealso: https://github.com/cherrypy/cherrypy/issues/1512 - :ref: http://bit.ly/2gK6bXK - """ - raise NotImplementedError - else: - return _argv - - @staticmethod - def _extend_pythonpath(env): - """Prepend current working dir to PATH environment variable if needed. - - If sys.path[0] is an empty string, the interpreter was likely - invoked with -m and the effective path is about to change on - re-exec. Add the current directory to $PYTHONPATH to ensure - that the new process sees the same path. - - This issue cannot be addressed in the general case because - Python cannot reliably reconstruct the - original command line (http://bugs.python.org/issue14208). - - (This idea filched from tornado.autoreload) - """ - path_prefix = '.' + os.pathsep - existing_path = env.get('PYTHONPATH', '') - needs_patch = ( - sys.path[0] == '' and - not existing_path.startswith(path_prefix) - ) - - if needs_patch: - env['PYTHONPATH'] = path_prefix + existing_path - - def _set_cloexec(self): - """Set the CLOEXEC flag on all open files (except stdin/out/err). - - If self.max_cloexec_files is an integer (the default), then on - platforms which support it, it represents the max open files setting - for the operating system. This function will be called just before - the process is restarted via os.execv() to prevent open files - from persisting into the new process. - - Set self.max_cloexec_files to 0 to disable this behavior. - """ - for fd in range(3, self.max_cloexec_files): # skip stdin/out/err - try: - flags = fcntl.fcntl(fd, fcntl.F_GETFD) - except IOError: - continue - fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) - - def stop(self): - """Stop all services.""" - self.state = states.STOPPING - self.log('Bus STOPPING') - self.publish('stop') - self.state = states.STOPPED - self.log('Bus STOPPED') - - def start_with_callback(self, func, args=None, kwargs=None): - """Start 'func' in a new thread T, then start self (and return T).""" - if args is None: - args = () - if kwargs is None: - kwargs = {} - args = (func,) + args - - 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.start() - - self.start() - - return t - - def log(self, msg='', level=20, traceback=False): - """Log the given message. Append the last traceback if requested.""" - if traceback: - msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) - self.publish('log', msg, level) - - -bus = Bus() diff --git a/libraries/cherrypy/scaffold/__init__.py b/libraries/cherrypy/scaffold/__init__.py deleted file mode 100644 index bcddba2d..00000000 --- a/libraries/cherrypy/scaffold/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -"""<MyProject>, a CherryPy application. - -Use this as a base for creating new CherryPy applications. When you want -to make a new app, copy and paste this folder to some other location -(maybe site-packages) and rename it to the name of your project, -then tweak as desired. - -Even before any tweaking, this should serve a few demonstration pages. -Change to this directory and run: - - cherryd -c site.conf - -""" - -import cherrypy -from cherrypy import tools, url - -import os -local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -@cherrypy.config(**{'tools.log_tracebacks.on': True}) -class Root: - """Declaration of the CherryPy app URI structure.""" - - @cherrypy.expose - def index(self): - """Render HTML-template at the root path of the web-app.""" - return """<html> -<body>Try some <a href='%s?a=7'>other</a> path, -or a <a href='%s?n=14'>default</a> path.<br /> -Or, just look at the pretty picture:<br /> -<img src='%s' /> -</body></html>""" % (url('other'), url('else'), - url('files/made_with_cherrypy_small.png')) - - @cherrypy.expose - def default(self, *args, **kwargs): - """Render catch-all args and kwargs.""" - return 'args: %s kwargs: %s' % (args, kwargs) - - @cherrypy.expose - def other(self, a=2, b='bananas', c=None): - """Render number of fruits based on third argument.""" - cherrypy.response.headers['Content-Type'] = 'text/plain' - if c is None: - return 'Have %d %s.' % (int(a), b) - else: - return 'Have %d %s, %s.' % (int(a), b, c) - - files = tools.staticdir.handler( - section='/files', - dir=os.path.join(local_dir, 'static'), - # Ignore .php files, etc. - match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', - ) - - -root = Root() - -# Uncomment the following to use your own favicon instead of CP's default. -# favicon_path = os.path.join(local_dir, "favicon.ico") -# root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/libraries/cherrypy/scaffold/apache-fcgi.conf b/libraries/cherrypy/scaffold/apache-fcgi.conf deleted file mode 100644 index 6e4f144c..00000000 --- a/libraries/cherrypy/scaffold/apache-fcgi.conf +++ /dev/null @@ -1,22 +0,0 @@ -# Apache2 server conf file for using CherryPy with mod_fcgid. - -# This doesn't have to be "C:/", but it has to be a directory somewhere, and -# MUST match the directory used in the FastCgiExternalServer directive, below. -DocumentRoot "C:/" - -ServerName 127.0.0.1 -Listen 80 -LoadModule fastcgi_module modules/mod_fastcgi.dll -LoadModule rewrite_module modules/mod_rewrite.so - -Options ExecCGI -SetHandler fastcgi-script -RewriteEngine On -# Send requests for any URI to our fastcgi handler. -RewriteRule ^(.*)$ /fastcgi.pyc [L] - -# The FastCgiExternalServer directive defines filename as an external FastCGI application. -# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. -# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this -# filename will be handled by this external FastCGI application. -FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 diff --git a/libraries/cherrypy/scaffold/example.conf b/libraries/cherrypy/scaffold/example.conf deleted file mode 100644 index 63250fe3..00000000 --- a/libraries/cherrypy/scaffold/example.conf +++ /dev/null @@ -1,3 +0,0 @@ -[/] -log.error_file: "error.log" -log.access_file: "access.log" diff --git a/libraries/cherrypy/scaffold/site.conf b/libraries/cherrypy/scaffold/site.conf deleted file mode 100644 index 6ed38983..00000000 --- a/libraries/cherrypy/scaffold/site.conf +++ /dev/null @@ -1,14 +0,0 @@ -[global] -# Uncomment this when you're done developing -#environment: "production" - -server.socket_host: "0.0.0.0" -server.socket_port: 8088 - -# Uncomment the following lines to run on HTTPS at the same time -#server.2.socket_host: "0.0.0.0" -#server.2.socket_port: 8433 -#server.2.ssl_certificate: '../test/test.pem' -#server.2.ssl_private_key: '../test/test.pem' - -tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png deleted file mode 100644 index 724f9d72d9ca5aede0b788fc1216286aa967e212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6347 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 N002ovPDHLkV1nY?RmA`R diff --git a/libraries/cherrypy/test/__init__.py b/libraries/cherrypy/test/__init__.py deleted file mode 100644 index 068382be..00000000 --- a/libraries/cherrypy/test/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Regression test suite for CherryPy. -""" - -import os -import sys - - -def newexit(): - os._exit(1) - - -def setup(): - # We want to monkey patch sys.exit so that we can get some - # information about where exit is being called. - newexit._old = sys.exit - sys.exit = newexit - - -def teardown(): - try: - sys.exit = sys.exit._old - except AttributeError: - sys.exit = sys._exit diff --git a/libraries/cherrypy/test/_test_decorators.py b/libraries/cherrypy/test/_test_decorators.py deleted file mode 100644 index 74832e40..00000000 --- a/libraries/cherrypy/test/_test_decorators.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Test module for the @-decorator syntax, which is version-specific""" - -import cherrypy -from cherrypy import expose, tools - - -class ExposeExamples(object): - - @expose - def no_call(self): - return 'Mr E. R. Bradshaw' - - @expose() - def call_empty(self): - return 'Mrs. B.J. Smegma' - - @expose('call_alias') - def nesbitt(self): - return 'Mr Nesbitt' - - @expose(['alias1', 'alias2']) - def andrews(self): - return 'Mr Ken Andrews' - - @expose(alias='alias3') - def watson(self): - return 'Mr. and Mrs. Watson' - - -class ToolExamples(object): - - @expose - # This is here to demonstrate that using the config decorator - # does not overwrite other config attributes added by the Tool - # decorator (in this case response_headers). - @cherrypy.config(**{'response.stream': True}) - @tools.response_headers(headers=[('Content-Type', 'application/data')]) - def blah(self): - yield b'blah' diff --git a/libraries/cherrypy/test/_test_states_demo.py b/libraries/cherrypy/test/_test_states_demo.py deleted file mode 100644 index a49407ba..00000000 --- a/libraries/cherrypy/test/_test_states_demo.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import sys -import time - -import cherrypy - -starttime = time.time() - - -class Root: - - @cherrypy.expose - def index(self): - return 'Hello World' - - @cherrypy.expose - def mtimes(self): - return repr(cherrypy.engine.publish('Autoreloader', 'mtimes')) - - @cherrypy.expose - def pid(self): - return str(os.getpid()) - - @cherrypy.expose - def start(self): - return repr(starttime) - - @cherrypy.expose - def exit(self): - # This handler might be called before the engine is STARTED if an - # HTTP worker thread handles it before the HTTP server returns - # control to engine.start. We avoid that race condition here - # by waiting for the Bus to be STARTED. - cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) - cherrypy.engine.exit() - - -@cherrypy.engine.subscribe('start', priority=100) -def unsub_sig(): - cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False)) - if cherrypy.config.get('unsubsig', False): - cherrypy.log('Unsubscribing the default cherrypy signal handler') - cherrypy.engine.signal_handler.unsubscribe() - try: - from signal import signal, SIGTERM - except ImportError: - pass - else: - def old_term_handler(signum=None, frame=None): - cherrypy.log('I am an old SIGTERM handler.') - sys.exit(0) - cherrypy.log('Subscribing the new one.') - signal(SIGTERM, old_term_handler) - - -@cherrypy.engine.subscribe('start', priority=6) -def starterror(): - if cherrypy.config.get('starterror', False): - 1 / 0 - - -@cherrypy.engine.subscribe('start', priority=6) -def log_test_case_name(): - if cherrypy.config.get('test_case_name', False): - cherrypy.log('STARTED FROM: %s' % - cherrypy.config.get('test_case_name')) - - -cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/libraries/cherrypy/test/benchmark.py b/libraries/cherrypy/test/benchmark.py deleted file mode 100644 index 44dfeff1..00000000 --- a/libraries/cherrypy/test/benchmark.py +++ /dev/null @@ -1,425 +0,0 @@ -"""CherryPy Benchmark Tool - - Usage: - benchmark.py [options] - - --null: use a null Request object (to bench the HTTP server only) - --notests: start the server but do not run the tests; this allows - you to check the tested pages with a browser - --help: show this help message - --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) - --modpython: run tests via apache on 54583 (with modpython_gateway) - --ab=path: Use the ab script/executable at 'path' (see below) - --apache=path: Use the apache script/exe at 'path' (see below) - - To run the benchmarks, the Apache Benchmark tool "ab" must either be on - your system path, or specified via the --ab=path option. - - To run the modpython tests, the "apache" executable or script must be - on your system path, or provided via the --apache=path option. On some - platforms, "apache" may be called "apachectl" or "apache2ctl"--create - a symlink to them if needed. -""" - -import getopt -import os -import re -import sys -import time - -import cherrypy -from cherrypy import _cperror, _cpmodpy -from cherrypy.lib import httputil - - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -AB_PATH = '' -APACHE_PATH = 'apache' -SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' - -__all__ = ['ABSession', 'Root', 'print_report', - 'run_standard_benchmarks', 'safe_threads', - 'size_report', 'thread_report', - ] - -size_cache = {} - - -class Root: - - @cherrypy.expose - def index(self): - return """<html> -<head> - <title>CherryPy Benchmark</title> -</head> -<body> - <ul> - <li><a href="hello">Hello, world! (14 byte dynamic)</a></li> - <li><a href="static/index.html">Static file (14 bytes static)</a></li> - <li><form action="sizer">Response of length: - <input type='text' name='size' value='10' /></form> - </li> - </ul> -</body> -</html>""" - - @cherrypy.expose - def hello(self): - return 'Hello, world\r\n' - - @cherrypy.expose - def sizer(self, size): - resp = size_cache.get(size, None) - if resp is None: - size_cache[size] = resp = 'X' * int(size) - return resp - - -def init(): - - cherrypy.config.update({ - 'log.error.file': '', - 'environment': 'production', - 'server.socket_host': '127.0.0.1', - 'server.socket_port': 54583, - 'server.max_request_header_size': 0, - 'server.max_request_body_size': 0, - }) - - # Cheat mode on ;) - del cherrypy.config['tools.log_tracebacks.on'] - del cherrypy.config['tools.log_headers.on'] - del cherrypy.config['tools.trailing_slash.on'] - - appconf = { - '/static': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }, - } - globals().update( - app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), - ) - - -class NullRequest: - - """A null HTTP request class, returning 200 and an empty body.""" - - def __init__(self, local, remote, scheme='http'): - pass - - def close(self): - pass - - def run(self, method, path, query_string, protocol, headers, rfile): - cherrypy.response.status = '200 OK' - cherrypy.response.header_list = [('Content-Type', 'text/html'), - ('Server', 'Null CherryPy'), - ('Date', httputil.HTTPDate()), - ('Content-Length', '0'), - ] - cherrypy.response.body = [''] - return cherrypy.response - - -class NullResponse: - pass - - -class ABSession: - - """A session of 'ab', the Apache HTTP server benchmarking tool. - -Example output from ab: - -This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 -Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ -Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ - -Benchmarking 127.0.0.1 (be patient) -Completed 100 requests -Completed 200 requests -Completed 300 requests -Completed 400 requests -Completed 500 requests -Completed 600 requests -Completed 700 requests -Completed 800 requests -Completed 900 requests - - -Server Software: CherryPy/3.1beta -Server Hostname: 127.0.0.1 -Server Port: 54583 - -Document Path: /static/index.html -Document Length: 14 bytes - -Concurrency Level: 10 -Time taken for tests: 9.643867 seconds -Complete requests: 1000 -Failed requests: 0 -Write errors: 0 -Total transferred: 189000 bytes -HTML transferred: 14000 bytes -Requests per second: 103.69 [#/sec] (mean) -Time per request: 96.439 [ms] (mean) -Time per request: 9.644 [ms] (mean, across all concurrent requests) -Transfer rate: 19.08 [Kbytes/sec] received - -Connection Times (ms) - min mean[+/-sd] median max -Connect: 0 0 2.9 0 10 -Processing: 20 94 7.3 90 130 -Waiting: 0 43 28.1 40 100 -Total: 20 95 7.3 100 130 - -Percentage of the requests served within a certain time (ms) - 50% 100 - 66% 100 - 75% 100 - 80% 100 - 90% 100 - 95% 100 - 98% 100 - 99% 110 - 100% 130 (longest request) -Finished 1000 requests -""" - - parse_patterns = [ - ('complete_requests', 'Completed', - br'^Complete requests:\s*(\d+)'), - ('failed_requests', 'Failed', - br'^Failed requests:\s*(\d+)'), - ('requests_per_second', 'req/sec', - br'^Requests per second:\s*([0-9.]+)'), - ('time_per_request_concurrent', 'msec/req', - br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), - ('transfer_rate', 'KB/sec', - br'^Transfer rate:\s*([0-9.]+)') - ] - - def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, - concurrency=10): - self.path = path - self.requests = requests - self.concurrency = concurrency - - def args(self): - port = cherrypy.server.socket_port - assert self.concurrency > 0 - assert self.requests > 0 - # Don't use "localhost". - # Cf - # http://mail.python.org/pipermail/python-win32/2008-March/007050.html - return ('-k -n %s -c %s http://127.0.0.1:%s%s' % - (self.requests, self.concurrency, port, self.path)) - - def run(self): - # Parse output of ab, setting attributes on self - try: - self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) - except Exception: - print(_cperror.format_exc()) - raise - - for attr, name, pattern in self.parse_patterns: - val = re.search(pattern, self.output, re.MULTILINE) - if val: - val = val.group(1) - setattr(self, attr, val) - else: - setattr(self, attr, None) - - -safe_threads = (25, 50, 100, 200, 400) -if sys.platform in ('win32',): - # For some reason, ab crashes with > 50 threads on my Win2k laptop. - safe_threads = (10, 20, 30, 40, 50) - - -def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): - sess = ABSession(path) - attrs, names, patterns = list(zip(*sess.parse_patterns)) - avg = dict.fromkeys(attrs, 0.0) - - yield ('threads',) + names - for c in concurrency: - sess.concurrency = c - sess.run() - row = [c] - for attr in attrs: - val = getattr(sess, attr) - if val is None: - print(sess.output) - row = None - break - val = float(val) - avg[attr] += float(val) - row.append(val) - if row: - yield row - - # Add a row of averages. - yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] - - -def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), - concurrency=50): - sess = ABSession(concurrency=concurrency) - attrs, names, patterns = list(zip(*sess.parse_patterns)) - yield ('bytes',) + names - for sz in sizes: - sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) - sess.run() - yield [sz] + [getattr(sess, attr) for attr in attrs] - - -def print_report(rows): - for row in rows: - print('') - for val in row: - sys.stdout.write(str(val).rjust(10) + ' | ') - print('') - - -def run_standard_benchmarks(): - print('') - print('Client Thread Report (1000 requests, 14 byte response body, ' - '%s server threads):' % cherrypy.server.thread_pool) - print_report(thread_report()) - - print('') - print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' - '%s server threads):' % cherrypy.server.thread_pool) - print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) - - print('') - print('Size Report (1000 requests, 50 client threads, ' - '%s server threads):' % cherrypy.server.thread_pool) - print_report(size_report()) - - -# modpython and other WSGI # - -def startup_modpython(req=None): - """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). - """ - if cherrypy.engine.state == cherrypy._cpengine.STOPPED: - if req: - if 'nullreq' in req.get_options(): - cherrypy.engine.request_class = NullRequest - cherrypy.engine.response_class = NullResponse - ab_opt = req.get_options().get('ab', '') - if ab_opt: - global AB_PATH - AB_PATH = ab_opt - cherrypy.engine.start() - if cherrypy.engine.state == cherrypy._cpengine.STARTING: - cherrypy.engine.wait() - return 0 # apache.OK - - -def run_modpython(use_wsgi=False): - print('Starting mod_python...') - pyopts = [] - - # Pass the null and ab=path options through Apache - if '--null' in opts: - pyopts.append(('nullreq', '')) - - if '--ab' in opts: - pyopts.append(('ab', opts['--ab'])) - - s = _cpmodpy.ModPythonServer - if use_wsgi: - pyopts.append(('wsgi.application', 'cherrypy::tree')) - pyopts.append( - ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) - handler = 'modpython_gateway::handler' - s = s(port=54583, opts=pyopts, - apache_path=APACHE_PATH, handler=handler) - else: - pyopts.append( - ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) - s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) - - try: - s.start() - run() - finally: - s.stop() - - -if __name__ == '__main__': - init() - - longopts = ['cpmodpy', 'modpython', 'null', 'notests', - 'help', 'ab=', 'apache='] - try: - switches, args = getopt.getopt(sys.argv[1:], '', longopts) - opts = dict(switches) - except getopt.GetoptError: - print(__doc__) - sys.exit(2) - - if '--help' in opts: - print(__doc__) - sys.exit(0) - - if '--ab' in opts: - AB_PATH = opts['--ab'] - - if '--notests' in opts: - # Return without stopping the server, so that the pages - # can be tested from a standard web browser. - def run(): - port = cherrypy.server.socket_port - print('You may now open http://127.0.0.1:%s%s/' % - (port, SCRIPT_NAME)) - - if '--null' in opts: - print('Using null Request object') - else: - def run(): - end = time.time() - start - print('Started in %s seconds' % end) - if '--null' in opts: - print('\nUsing null Request object') - try: - try: - run_standard_benchmarks() - except Exception: - print(_cperror.format_exc()) - raise - finally: - cherrypy.engine.exit() - - print('Starting CherryPy app server...') - - class NullWriter(object): - - """Suppresses the printing of socket errors.""" - - def write(self, data): - pass - sys.stderr = NullWriter() - - start = time.time() - - if '--cpmodpy' in opts: - run_modpython() - elif '--modpython' in opts: - run_modpython(use_wsgi=True) - else: - if '--null' in opts: - cherrypy.server.request_class = NullRequest - cherrypy.server.response_class = NullResponse - - cherrypy.engine.start_with_callback(run) - cherrypy.engine.block() diff --git a/libraries/cherrypy/test/checkerdemo.py b/libraries/cherrypy/test/checkerdemo.py deleted file mode 100644 index 3438bd0c..00000000 --- a/libraries/cherrypy/test/checkerdemo.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Demonstration app for cherrypy.checker. - -This application is intentionally broken and badly designed. -To demonstrate the output of the CherryPy Checker, simply execute -this module. -""" - -import os -import cherrypy -thisdir = os.path.dirname(os.path.abspath(__file__)) - - -class Root: - pass - - -if __name__ == '__main__': - conf = {'/base': {'tools.staticdir.root': thisdir, - # Obsolete key. - 'throw_errors': True, - }, - # This entry should be OK. - '/base/static': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static'}, - # Warn on missing folder. - '/base/js': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'js'}, - # Warn on dir with an abs path even though we provide root. - '/base/static2': {'tools.staticdir.on': True, - 'tools.staticdir.dir': '/static'}, - # Warn on dir with a relative path with no root. - '/static3': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static'}, - # Warn on unknown namespace - '/unknown': {'toobles.gzip.on': True}, - # Warn special on cherrypy.<known ns>.* - '/cpknown': {'cherrypy.tools.encode.on': True}, - # Warn on mismatched types - '/conftype': {'request.show_tracebacks': 14}, - # Warn on unknown tool. - '/web': {'tools.unknown.on': True}, - # Warn on server.* in app config. - '/app1': {'server.socket_host': '0.0.0.0'}, - # Warn on 'localhost' - 'global': {'server.socket_host': 'localhost'}, - # Warn on '[name]' - '[/extra_brackets]': {}, - } - cherrypy.quickstart(Root(), config=conf) diff --git a/libraries/cherrypy/test/fastcgi.conf b/libraries/cherrypy/test/fastcgi.conf deleted file mode 100644 index e5c5163c..00000000 --- a/libraries/cherrypy/test/fastcgi.conf +++ /dev/null @@ -1,18 +0,0 @@ - -# Apache2 server conf file for testing CherryPy with mod_fastcgi. -# fumanchu: I had to hard-code paths due to crazy Debian layouts :( -ServerRoot /usr/lib/apache2 -User #1000 -ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log - -DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test" -ServerName 127.0.0.1 -Listen 8080 -LoadModule fastcgi_module modules/mod_fastcgi.so -LoadModule rewrite_module modules/mod_rewrite.so - -Options +ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/libraries/cherrypy/test/fcgi.conf b/libraries/cherrypy/test/fcgi.conf deleted file mode 100644 index 3062eb35..00000000 --- a/libraries/cherrypy/test/fcgi.conf +++ /dev/null @@ -1,14 +0,0 @@ - -# Apache2 server conf file for testing CherryPy with mod_fcgid. - -DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test" -ServerName 127.0.0.1 -Listen 8080 -LoadModule fastcgi_module modules/mod_fastcgi.dll -LoadModule rewrite_module modules/mod_rewrite.so - -Options ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/libraries/cherrypy/test/helper.py b/libraries/cherrypy/test/helper.py deleted file mode 100644 index 01c5a0c0..00000000 --- a/libraries/cherrypy/test/helper.py +++ /dev/null @@ -1,542 +0,0 @@ -"""A library of helper functions for the CherryPy test suite.""" - -import datetime -import io -import logging -import os -import re -import subprocess -import sys -import time -import unittest -import warnings - -import portend -import pytest -import six - -from cheroot.test import webtest - -import cherrypy -from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob -from cherrypy.lib import httputil -from cherrypy.lib import gctools - -log = logging.getLogger(__name__) -thisdir = os.path.abspath(os.path.dirname(__file__)) -serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') - - -class Supervisor(object): - - """Base class for modeling and controlling servers during testing.""" - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - if k == 'port': - setattr(self, k, int(v)) - setattr(self, k, v) - - -def log_to_stderr(msg, level): - return sys.stderr.write(msg + os.linesep) - - -class LocalSupervisor(Supervisor): - - """Base class for modeling/controlling servers which run in the same - process. - - When the server side runs in a different process, start/stop can dump all - state between each test module easily. When the server side runs in the - same process as the client, however, we have to do a bit more work to - ensure config and mounted apps are reset between tests. - """ - - using_apache = False - using_wsgi = False - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - - cherrypy.server.httpserver = self.httpserver_class - - # This is perhaps the wrong place for this call but this is the only - # place that i've found so far that I KNOW is early enough to set this. - cherrypy.config.update({'log.screen': False}) - engine = cherrypy.engine - if hasattr(engine, 'signal_handler'): - engine.signal_handler.subscribe() - if hasattr(engine, 'console_control_handler'): - engine.console_control_handler.subscribe() - - def start(self, modulename=None): - """Load and start the HTTP server.""" - if modulename: - # Unhook httpserver so cherrypy.server.start() creates a new - # one (with config from setup_server, if declared). - cherrypy.server.httpserver = None - - cherrypy.engine.start() - - self.sync_apps() - - def sync_apps(self): - """Tell the server about any apps which the setup functions mounted.""" - pass - - def stop(self): - td = getattr(self, 'teardown', None) - if td: - td() - - cherrypy.engine.exit() - - servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {}))) - for name, server in servers_copy: - server.unsubscribe() - del cherrypy.servers[name] - - -class NativeServerSupervisor(LocalSupervisor): - - """Server supervisor for the builtin HTTP server.""" - - httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' - using_apache = False - using_wsgi = False - - def __str__(self): - return 'Builtin HTTP Server on %s:%s' % (self.host, self.port) - - -class LocalWSGISupervisor(LocalSupervisor): - - """Server supervisor for the builtin WSGI server.""" - - httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' - using_apache = False - using_wsgi = True - - def __str__(self): - return 'Builtin WSGI Server on %s:%s' % (self.host, self.port) - - def sync_apps(self): - """Hook a new WSGI app into the origin server.""" - cherrypy.server.httpserver.wsgi_app = self.get_app() - - def get_app(self, app=None): - """Obtain a new (decorated) WSGI app to hook into the origin server.""" - if app is None: - app = cherrypy.tree - - if self.validate: - try: - from wsgiref import validate - except ImportError: - warnings.warn( - 'Error importing wsgiref. The validator will not run.') - else: - # wraps the app in the validator - app = validate.validator(app) - - return app - - -def get_cpmodpy_supervisor(**options): - from cherrypy.test import modpy - sup = modpy.ModPythonSupervisor(**options) - sup.template = modpy.conf_cpmodpy - return sup - - -def get_modpygw_supervisor(**options): - from cherrypy.test import modpy - sup = modpy.ModPythonSupervisor(**options) - sup.template = modpy.conf_modpython_gateway - sup.using_wsgi = True - return sup - - -def get_modwsgi_supervisor(**options): - from cherrypy.test import modwsgi - return modwsgi.ModWSGISupervisor(**options) - - -def get_modfcgid_supervisor(**options): - from cherrypy.test import modfcgid - return modfcgid.ModFCGISupervisor(**options) - - -def get_modfastcgi_supervisor(**options): - from cherrypy.test import modfastcgi - return modfastcgi.ModFCGISupervisor(**options) - - -def get_wsgi_u_supervisor(**options): - cherrypy.server.wsgi_version = ('u', 0) - return LocalWSGISupervisor(**options) - - -class CPWebCase(webtest.WebCase): - - script_name = '' - scheme = 'http' - - available_servers = {'wsgi': LocalWSGISupervisor, - 'wsgi_u': get_wsgi_u_supervisor, - 'native': NativeServerSupervisor, - 'cpmodpy': get_cpmodpy_supervisor, - 'modpygw': get_modpygw_supervisor, - 'modwsgi': get_modwsgi_supervisor, - 'modfcgid': get_modfcgid_supervisor, - 'modfastcgi': get_modfastcgi_supervisor, - } - default_server = 'wsgi' - - @classmethod - def _setup_server(cls, supervisor, conf): - v = sys.version.split()[0] - log.info('Python version used to run this test script: %s' % v) - log.info('CherryPy version: %s' % cherrypy.__version__) - if supervisor.scheme == 'https': - ssl = ' (ssl)' - else: - ssl = '' - log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl)) - log.info('PID: %s' % os.getpid()) - - cherrypy.server.using_apache = supervisor.using_apache - cherrypy.server.using_wsgi = supervisor.using_wsgi - - if sys.platform[:4] == 'java': - cherrypy.config.update({'server.nodelay': False}) - - if isinstance(conf, text_or_bytes): - parser = cherrypy.lib.reprconf.Parser() - conf = parser.dict_from_file(conf).get('global', {}) - else: - conf = conf or {} - baseconf = conf.copy() - baseconf.update({'server.socket_host': supervisor.host, - 'server.socket_port': supervisor.port, - 'server.protocol_version': supervisor.protocol, - 'environment': 'test_suite', - }) - if supervisor.scheme == 'https': - # baseconf['server.ssl_module'] = 'builtin' - baseconf['server.ssl_certificate'] = serverpem - baseconf['server.ssl_private_key'] = serverpem - - # helper must be imported lazily so the coverage tool - # can run against module-level statements within cherrypy. - # Also, we have to do "from cherrypy.test import helper", - # exactly like each test module does, because a relative import - # would stick a second instance of webtest in sys.modules, - # and we wouldn't be able to globally override the port anymore. - if supervisor.scheme == 'https': - webtest.WebCase.HTTP_CONN = HTTPSConnection - return baseconf - - @classmethod - def setup_class(cls): - '' - # Creates a server - conf = { - 'scheme': 'http', - 'protocol': 'HTTP/1.1', - 'port': 54583, - 'host': '127.0.0.1', - 'validate': False, - 'server': 'wsgi', - } - supervisor_factory = cls.available_servers.get( - conf.get('server', 'wsgi')) - if supervisor_factory is None: - raise RuntimeError('Unknown server in config: %s' % conf['server']) - supervisor = supervisor_factory(**conf) - - # Copied from "run_test_suite" - cherrypy.config.reset() - baseconf = cls._setup_server(supervisor, conf) - cherrypy.config.update(baseconf) - setup_client() - - if hasattr(cls, 'setup_server'): - # Clear the cherrypy tree and clear the wsgi server so that - # it can be updated with the new root - cherrypy.tree = cherrypy._cptree.Tree() - cherrypy.server.httpserver = None - cls.setup_server() - # Add a resource for verifying there are no refleaks - # to *every* test class. - cherrypy.tree.mount(gctools.GCRoot(), '/gc') - cls.do_gc_test = True - supervisor.start(cls.__module__) - - cls.supervisor = supervisor - - @classmethod - def teardown_class(cls): - '' - if hasattr(cls, 'setup_server'): - cls.supervisor.stop() - - do_gc_test = False - - def test_gc(self): - if not self.do_gc_test: - return - - self.getPage('/gc/stats') - try: - self.assertBody('Statistics:') - except Exception: - 'Failures occur intermittently. See #1420' - - def prefix(self): - return self.script_name.rstrip('/') - - def base(self): - if ((self.scheme == 'http' and self.PORT == 80) or - (self.scheme == 'https' and self.PORT == 443)): - port = '' - else: - port = ':%s' % self.PORT - - return '%s://%s%s%s' % (self.scheme, self.HOST, port, - self.script_name.rstrip('/')) - - def exit(self): - sys.exit() - - def getPage(self, url, headers=None, method='GET', body=None, - protocol=None, raise_subcls=None): - """Open the url. Return status, headers, body. - - `raise_subcls` must be a tuple with the exceptions classes - or a single exception class that are not going to be considered - a socket.error regardless that they were are subclass of a - socket.error and therefore not considered for a connection retry. - """ - if self.script_name: - url = httputil.urljoin(self.script_name, url) - return webtest.WebCase.getPage(self, url, headers, method, body, - protocol, raise_subcls) - - def skip(self, msg='skipped '): - pytest.skip(msg) - - def assertErrorPage(self, status, message=None, pattern=''): - """Compare the response body with a built in error page. - - The function will optionally look for the regexp pattern, - within the exception embedded in the error page.""" - - # This will never contain a traceback - page = cherrypy._cperror.get_error_page(status, message=message) - - # First, test the response body without checking the traceback. - # Stick a match-all group (.*) in to grab the traceback. - def esc(text): - return re.escape(ntob(text)) - epage = re.escape(page) - epage = epage.replace( - esc('<pre id="traceback"></pre>'), - esc('<pre id="traceback">') + b'(.*)' + esc('</pre>')) - m = re.match(epage, self.body, re.DOTALL) - if not m: - self._handlewebError( - 'Error page does not match; expected:\n' + page) - return - - # Now test the pattern against the traceback - if pattern is None: - # Special-case None to mean that there should be *no* traceback. - if m and m.group(1): - self._handlewebError('Error page contains traceback') - else: - if (m is None) or ( - not re.search(ntob(re.escape(pattern), self.encoding), - m.group(1))): - msg = 'Error page does not contain %s in traceback' - self._handlewebError(msg % repr(pattern)) - - date_tolerance = 2 - - def assertEqualDates(self, dt1, dt2, seconds=None): - """Assert abs(dt1 - dt2) is within Y seconds.""" - if seconds is None: - seconds = self.date_tolerance - - if dt1 > dt2: - diff = dt1 - dt2 - else: - diff = dt2 - dt1 - if not diff < datetime.timedelta(seconds=seconds): - raise AssertionError('%r and %r are not within %r seconds.' % - (dt1, dt2, seconds)) - - -def _test_method_sorter(_, x, y): - """Monkeypatch the test sorter to always run test_gc last in each suite.""" - if x == 'test_gc': - return 1 - if y == 'test_gc': - return -1 - if x > y: - return 1 - if x < y: - return -1 - return 0 - - -unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter - - -def setup_client(): - """Set up the WebCase classes to match the server's socket settings.""" - webtest.WebCase.PORT = cherrypy.server.socket_port - webtest.WebCase.HOST = cherrypy.server.socket_host - if cherrypy.server.ssl_certificate: - CPWebCase.scheme = 'https' - -# --------------------------- Spawning helpers --------------------------- # - - -class CPProcess(object): - - pid_file = os.path.join(thisdir, 'test.pid') - config_file = os.path.join(thisdir, 'test.conf') - config_template = """[global] -server.socket_host: '%(host)s' -server.socket_port: %(port)s -checker.on: False -log.screen: False -log.error_file: r'%(error_log)s' -log.access_file: r'%(access_log)s' -%(ssl)s -%(extra)s -""" - error_log = os.path.join(thisdir, 'test.error.log') - access_log = os.path.join(thisdir, 'test.access.log') - - def __init__(self, wait=False, daemonize=False, ssl=False, - socket_host=None, socket_port=None): - self.wait = wait - self.daemonize = daemonize - self.ssl = ssl - self.host = socket_host or cherrypy.server.socket_host - self.port = socket_port or cherrypy.server.socket_port - - def write_conf(self, extra=''): - if self.ssl: - serverpem = os.path.join(thisdir, 'test.pem') - ssl = """ -server.ssl_certificate: r'%s' -server.ssl_private_key: r'%s' -""" % (serverpem, serverpem) - else: - ssl = '' - - conf = self.config_template % { - 'host': self.host, - 'port': self.port, - 'error_log': self.error_log, - 'access_log': self.access_log, - 'ssl': ssl, - 'extra': extra, - } - with io.open(self.config_file, 'w', encoding='utf-8') as f: - f.write(six.text_type(conf)) - - def start(self, imports=None): - """Start cherryd in a subprocess.""" - portend.free(self.host, self.port, timeout=1) - - args = [ - '-m', - 'cherrypy', - '-c', self.config_file, - '-p', self.pid_file, - ] - r""" - Command for running cherryd server with autoreload enabled - - Using - - ``` - ['-c', - "__requires__ = 'CherryPy'; \ - import pkg_resources, re, sys; \ - sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ - sys.exit(\ - pkg_resources.load_entry_point(\ - 'CherryPy', 'console_scripts', 'cherryd')())"] - ``` - - doesn't work as it's impossible to reconstruct the `-c`'s contents. - Ref: https://github.com/cherrypy/cherrypy/issues/1545 - """ - - if not isinstance(imports, (list, tuple)): - imports = [imports] - for i in imports: - if i: - args.append('-i') - args.append(i) - - if self.daemonize: - args.append('-d') - - env = os.environ.copy() - # Make sure we import the cherrypy package in which this module is - # defined. - grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) - if env.get('PYTHONPATH', ''): - env['PYTHONPATH'] = os.pathsep.join( - (grandparentdir, env['PYTHONPATH'])) - else: - env['PYTHONPATH'] = grandparentdir - self._proc = subprocess.Popen([sys.executable] + args, env=env) - if self.wait: - self.exit_code = self._proc.wait() - else: - portend.occupied(self.host, self.port, timeout=5) - - # Give the engine a wee bit more time to finish STARTING - if self.daemonize: - time.sleep(2) - else: - time.sleep(1) - - def get_pid(self): - if self.daemonize: - return int(open(self.pid_file, 'rb').read()) - return self._proc.pid - - def join(self): - """Wait for the process to exit.""" - if self.daemonize: - return self._join_daemon() - self._proc.wait() - - def _join_daemon(self): - try: - try: - # Mac, UNIX - os.wait() - except AttributeError: - # Windows - try: - pid = self.get_pid() - except IOError: - # Assume the subprocess deleted the pidfile on shutdown. - pass - else: - os.waitpid(pid, 0) - except OSError: - x = sys.exc_info()[1] - if x.args != (10, 'No child processes'): - raise diff --git a/libraries/cherrypy/test/logtest.py b/libraries/cherrypy/test/logtest.py deleted file mode 100644 index ed8f1540..00000000 --- a/libraries/cherrypy/test/logtest.py +++ /dev/null @@ -1,228 +0,0 @@ -"""logtest, a unittest.TestCase helper for testing log output.""" - -import sys -import time -from uuid import UUID - -import six - -from cherrypy._cpcompat import text_or_bytes, ntob - - -try: - # On Windows, msvcrt.getch reads a single char without output. - import msvcrt - - def getchar(): - return msvcrt.getch() -except ImportError: - # Unix getchr - import tty - import termios - - def getchar(): - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - - -class LogCase(object): - - """unittest.TestCase mixin for testing log messages. - - logfile: a filename for the desired log. Yes, I know modes are evil, - but it makes the test functions so much cleaner to set this once. - - lastmarker: the last marker in the log. This can be used to search for - messages since the last marker. - - markerPrefix: a string with which to prefix log markers. This should be - unique enough from normal log output to use for marker identification. - """ - - logfile = None - lastmarker = None - markerPrefix = b'test suite marker: ' - - def _handleLogError(self, msg, data, marker, pattern): - print('') - print(' ERROR: %s' % msg) - - if not self.interactive: - raise self.failureException(msg) - - p = (' Show: ' - '[L]og [M]arker [P]attern; ' - '[I]gnore, [R]aise, or sys.e[X]it >> ') - sys.stdout.write(p + ' ') - # ARGH - sys.stdout.flush() - while True: - i = getchar().upper() - if i not in 'MPLIRX': - continue - print(i.upper()) # Also prints new line - if i == 'L': - for x, line in enumerate(data): - if (x + 1) % self.console_height == 0: - # The \r and comma should make the next line overwrite - sys.stdout.write('<-- More -->\r ') - m = getchar().lower() - # Erase our "More" prompt - sys.stdout.write(' \r ') - if m == 'q': - break - print(line.rstrip()) - elif i == 'M': - print(repr(marker or self.lastmarker)) - elif i == 'P': - print(repr(pattern)) - elif i == 'I': - # return without raising the normal exception - return - elif i == 'R': - raise self.failureException(msg) - elif i == 'X': - self.exit() - sys.stdout.write(p + ' ') - - def exit(self): - sys.exit() - - def emptyLog(self): - """Overwrite self.logfile with 0 bytes.""" - open(self.logfile, 'wb').write('') - - def markLog(self, key=None): - """Insert a marker line into the log and set self.lastmarker.""" - if key is None: - key = str(time.time()) - self.lastmarker = key - - open(self.logfile, 'ab+').write( - ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8')) - - def _read_marked_region(self, marker=None): - """Return lines from self.logfile in the marked region. - - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be returned. - """ -# Give the logger time to finish writing? -# time.sleep(0.5) - - logfile = self.logfile - marker = marker or self.lastmarker - if marker is None: - return open(logfile, 'rb').readlines() - - if isinstance(marker, six.text_type): - 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 - return data - - def assertInLog(self, line, marker=None): - """Fail if the given (partial) line is not in the log. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - for logline in data: - if line in logline: - return - msg = '%r not found in log' % line - self._handleLogError(msg, data, marker, line) - - def assertNotInLog(self, line, marker=None): - """Fail if the given (partial) line is in the log. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - for logline in data: - if line in logline: - msg = '%r found in log' % line - self._handleLogError(msg, data, marker, line) - - def assertValidUUIDv4(self, marker=None): - """Fail if the given UUIDv4 is not valid. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - data = [ - chunk.decode('utf-8').rstrip('\n').rstrip('\r') - for chunk in data - ] - for log_chunk in data: - try: - uuid_log = data[-1] - uuid_obj = UUID(uuid_log, version=4) - except (TypeError, ValueError): - pass # it might be in other chunk - else: - if str(uuid_obj) == uuid_log: - return - msg = '%r is not a valid UUIDv4' % uuid_log - self._handleLogError(msg, data, marker, log_chunk) - - msg = 'UUIDv4 not found in log' - self._handleLogError(msg, data, marker, log_chunk) - - def assertLog(self, sliceargs, lines, marker=None): - """Fail if log.readlines()[sliceargs] is not contained in 'lines'. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - if isinstance(sliceargs, int): - # Single arg. Use __getitem__ and allow lines to be str or list. - if isinstance(lines, (tuple, list)): - lines = lines[0] - if isinstance(lines, six.text_type): - lines = lines.encode('utf-8') - if lines not in data[sliceargs]: - msg = '%r not found on log line %r' % (lines, sliceargs) - self._handleLogError( - msg, - [data[sliceargs], '--EXTRA CONTEXT--'] + data[ - sliceargs + 1:sliceargs + 6], - marker, - 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.") - - start, stop = sliceargs - for line, logline in zip(lines, data[start:stop]): - if isinstance(line, six.text_type): - line = line.encode('utf-8') - if line not in logline: - msg = '%r not found in log' % line - self._handleLogError(msg, data[start:stop], marker, line) diff --git a/libraries/cherrypy/test/modfastcgi.py b/libraries/cherrypy/test/modfastcgi.py deleted file mode 100644 index 79ec3d18..00000000 --- a/libraries/cherrypy/test/modfastcgi.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. - -To autostart fastcgi, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl", "apache2ctl", -or "httpd"--create a symlink to them if needed. - -You'll also need the WSGIServer from flup.servers. -See http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -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. -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. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -import re - -import cherrypy -from cherrypy.process import servers -from cherrypy.test import helper - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -def read_process(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) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = 'apache2ctl' -CONF_PATH = 'fastcgi.conf' - -conf_fastcgi = """ -# Apache2 server conf file for testing CherryPy with mod_fastcgi. -# fumanchu: I had to hard-code paths due to crazy Debian layouts :( -ServerRoot /usr/lib/apache2 -User #1000 -ErrorLog %(root)s/mod_fastcgi.error.log - -DocumentRoot "%(root)s" -ServerName 127.0.0.1 -Listen %(port)s -LoadModule fastcgi_module modules/mod_fastcgi.so -LoadModule rewrite_module modules/mod_rewrite.so - -Options +ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 -""" - - -def erase_script_name(environ, start_response): - environ['SCRIPT_NAME'] = '' - return cherrypy.tree(environ, start_response) - - -class ModFCGISupervisor(helper.LocalWSGISupervisor): - - httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' - using_apache = True - using_wsgi = True - template = conf_fastcgi - - def __str__(self): - return 'FCGI Server on %s:%s' % (self.host, self.port) - - def start(self, modulename): - 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) - cherrypy.server.socket_port = 4000 - # For FCGI, we both start apache... - self.start_apache() - # ...and our local server - cherrypy.engine.start() - self.sync_apps() - - def start_apache(self): - fcgiconf = CONF_PATH - if not os.path.isabs(fcgiconf): - fcgiconf = os.path.join(curdir, fcgiconf) - - # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: - 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: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') - helper.LocalWSGISupervisor.stop(self) - - def sync_apps(self): - cherrypy.server.httpserver.fcgiserver.application = self.get_app( - erase_script_name) diff --git a/libraries/cherrypy/test/modfcgid.py b/libraries/cherrypy/test/modfcgid.py deleted file mode 100644 index d101bd67..00000000 --- a/libraries/cherrypy/test/modfcgid.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. - -To autostart fcgid, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl", "apache2ctl", -or "httpd"--create a symlink to them if needed. - -You'll also need the WSGIServer from flup.servers. -See http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -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. -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. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -import re - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.process import servers -from cherrypy.test import helper - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -def read_process(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) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = 'httpd' -CONF_PATH = 'fcgi.conf' - -conf_fcgid = """ -# Apache2 server conf file for testing CherryPy with mod_fcgid. - -DocumentRoot "%(root)s" -ServerName 127.0.0.1 -Listen %(port)s -LoadModule fastcgi_module modules/mod_fastcgi.dll -LoadModule rewrite_module modules/mod_rewrite.so - -Options ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 -""" - - -class ModFCGISupervisor(helper.LocalSupervisor): - - using_apache = True - using_wsgi = True - template = conf_fcgid - - def __str__(self): - return 'FCGI Server on %s:%s' % (self.host, self.port) - - def start(self, modulename): - cherrypy.server.httpserver = servers.FlupFCGIServer( - 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 - helper.LocalServer.start(self, modulename) - - def start_apache(self): - fcgiconf = CONF_PATH - if not os.path.isabs(fcgiconf): - fcgiconf = os.path.join(curdir, fcgiconf) - - # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: - 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: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') - helper.LocalServer.stop(self) - - def sync_apps(self): - cherrypy.server.httpserver.fcgiserver.application = self.get_app() diff --git a/libraries/cherrypy/test/modpy.py b/libraries/cherrypy/test/modpy.py deleted file mode 100644 index 7c288d2c..00000000 --- a/libraries/cherrypy/test/modpy.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. - -To autostart modpython, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- -create a symlink to them if needed. - -If you wish to test the WSGI interface instead of our _cpmodpy interface, -you also need the 'modpython_gateway' module at: -http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -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. -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. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -import re - -import cherrypy -from cherrypy.test import helper - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -def read_process(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) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = 'httpd' -CONF_PATH = 'test_mp.conf' - -conf_modpython_gateway = """ -# Apache2 server conf file for testing CherryPy with modpython_gateway. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s -LoadModule python_module modules/mod_python.so - -SetHandler python-program -PythonFixupHandler cherrypy.test.modpy::wsgisetup -PythonOption testmod %(modulename)s -PythonHandler modpython_gateway::handler -PythonOption wsgi.application cherrypy::tree -PythonOption socket_host %(host)s -PythonDebug On -""" - -conf_cpmodpy = """ -# Apache2 server conf file for testing CherryPy with _cpmodpy. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s -LoadModule python_module modules/mod_python.so - -SetHandler python-program -PythonFixupHandler cherrypy.test.modpy::cpmodpysetup -PythonHandler cherrypy._cpmodpy::handler -PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server -PythonOption socket_host %(host)s -PythonDebug On -""" - - -class ModPythonSupervisor(helper.Supervisor): - - using_apache = True - using_wsgi = False - template = None - - def __str__(self): - return 'ModPython Server on %s:%s' % (self.host, self.port) - - def start(self, modulename): - mpconf = CONF_PATH - if not os.path.isabs(mpconf): - mpconf = os.path.join(curdir, mpconf) - - f = open(mpconf, 'wb') - try: - 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: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') - - -loaded = False - - -def wsgisetup(req): - global loaded - if not loaded: - 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'], - }) - - 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 - - -def cpmodpysetup(req): - global loaded - if not loaded: - 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'], - }) - from mod_python import apache - return apache.OK diff --git a/libraries/cherrypy/test/modwsgi.py b/libraries/cherrypy/test/modwsgi.py deleted file mode 100644 index f558e223..00000000 --- a/libraries/cherrypy/test/modwsgi.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. - -To autostart modwsgi, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- -create a symlink to them if needed. - - -KNOWN BUGS -========== - -##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. -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. -##4. Apache replaces status "reason phrases" automatically. For example, -## CherryPy may set "304 Not modified" but Apache will write out -## "304 Not Modified" (capital "M"). -##5. Apache does not allow custom error codes as per the spec. -##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the -## Request-URI too early. -7. mod_wsgi will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. When responding with 204 No Content, mod_wsgi adds a Content-Length - header for you. -9. When an error is raised, mod_wsgi has no facility for printing a - traceback as the response content (it's sent to the Apache log instead). -10. Startup and shutdown of Apache when running mod_wsgi seems slow. -""" - -import os -import re -import sys -import time - -import portend - -from cheroot.test import webtest - -import cherrypy -from cherrypy.test import helper - -curdir = os.path.abspath(os.path.dirname(__file__)) - - -def read_process(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) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -if sys.platform == 'win32': - APACHE_PATH = 'httpd' -else: - APACHE_PATH = 'apache' - -CONF_PATH = 'test_mw.conf' - -conf_modwsgi = r""" -# Apache2 server conf file for testing CherryPy with modpython_gateway. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s - -AllowEncodedSlashes On -LoadModule rewrite_module modules/mod_rewrite.so -RewriteEngine on -RewriteMap escaping int:escape - -LoadModule log_config_module modules/mod_log_config.so -LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined -CustomLog "%(curdir)s/apache.access.log" combined -ErrorLog "%(curdir)s/apache.error.log" -LogLevel debug - -LoadModule wsgi_module modules/mod_wsgi.so -LoadModule env_module modules/mod_env.so - -WSGIScriptAlias / "%(curdir)s/modwsgi.py" -SetEnv testmod %(testmod)s -""" # noqa E501 - - -class ModWSGISupervisor(helper.Supervisor): - - """Server Controller for ModWSGI and CherryPy.""" - - using_apache = True - using_wsgi = True - template = conf_modwsgi - - def __str__(self): - return 'ModWSGI Server on %s:%s' % (self.host, self.port) - - def start(self, modulename): - mpconf = CONF_PATH - if not os.path.isabs(mpconf): - mpconf = os.path.join(curdir, mpconf) - - f = open(mpconf, 'wb') - try: - 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: - 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) - time.sleep(1) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') - - -loaded = False - - -def application(environ, start_response): - global loaded - if not loaded: - loaded = True - 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, - }) - return cherrypy.tree(environ, start_response) diff --git a/libraries/cherrypy/test/sessiondemo.py b/libraries/cherrypy/test/sessiondemo.py deleted file mode 100644 index 8226c1b9..00000000 --- a/libraries/cherrypy/test/sessiondemo.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/python -"""A session demonstration app.""" - -import calendar -from datetime import datetime -import sys - -import six - -import cherrypy -from cherrypy.lib import sessions - - -page = """ -<html> -<head> -<style type='text/css'> -table { border-collapse: collapse; border: 1px solid #663333; } -th { text-align: right; background-color: #663333; color: white; padding: 0.5em; } -td { white-space: pre-wrap; font-family: monospace; padding: 0.5em; - border: 1px solid #663333; } -.warn { font-family: serif; color: #990000; } -</style> -<script type="text/javascript"> -<!-- -function twodigit(d) { return d < 10 ? "0" + d : d; } -function formattime(t) { - var month = t.getUTCMonth() + 1; - var day = t.getUTCDate(); - var year = t.getUTCFullYear(); - var hours = t.getUTCHours(); - var minutes = t.getUTCMinutes(); - return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " + - hours + ":" + twodigit(minutes) + " UTC"); -} - -function interval(s) { - // Return the given interval (in seconds) as an English phrase - var seconds = s %% 60; - s = Math.floor(s / 60); - var minutes = s %% 60; - s = Math.floor(s / 60); - var hours = s %% 24; - var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds); - var days = Math.floor(s / 24); - if (days != 0) v = days + ' days, ' + v; - return v; -} - -var fudge_seconds = 5; - -function init() { - // Set the content of the 'btime' cell. - var currentTime = new Date(); - var bunixtime = Math.floor(currentTime.getTime() / 1000); - - var v = formattime(currentTime); - v += " (Unix time: " + bunixtime + ")"; - - var diff = Math.abs(%(serverunixtime)s - bunixtime); - if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>"; - - document.getElementById('btime').innerHTML = v; - - // Warn if response cookie expires is not close to one hour in the future. - // Yes, we want this to happen when wit hit the 'Expire' link, too. - var expires = Date.parse("%(expires)s") / 1000; - var onehour = (60 * 60); - if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) { - diff = Math.floor(expires - bunixtime); - if (expires > (bunixtime + onehour)) { - var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future."; - } else { - var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past."; - } - document.getElementById('respcookiewarn').innerHTML = msg; - } -} -//--> -</script> -</head> - -<body onload='init()'> -<h2>Session Demo</h2> -<p>Reload this page. The session ID should not change from one reload to the next</p> -<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p> -<table> - <tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr> - <tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr> - <tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr> - <tr><th>Session Data</th><td>%(sessiondata)s</td></tr> - <tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr> - <tr><th>Browser Time</th><td id='btime'> </td></tr> - <tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr> - <tr><th>Python Version:</th><td>%(pyversion)s</td></tr> -</table> -</body></html> -""" # noqa E501 - - -class Root(object): - - 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.') - if cherrypy.session.missing: - changemsg.append( - 'Created new session due to missing ' - '(expired or malicious) session.') - if cherrypy.session.regenerated: - changemsg.append('Application generated a new session.') - - try: - expires = cherrypy.response.cookie['session_id']['expires'] - except KeyError: - expires = '' - - return page % { - 'sessionid': cherrypy.session.id, - 'changemsg': '<br>'.join(changemsg), - 'respcookie': cherrypy.response.cookie.output(), - 'reqcookie': cherrypy.request.cookie.output(), - 'sessiondata': list(six.iteritems(cherrypy.session)), - 'servertime': ( - datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' - ), - 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), - 'cpversion': cherrypy.__version__, - 'pyversion': sys.version, - 'expires': expires, - } - - @cherrypy.expose - def index(self): - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'green' - return self.page() - - @cherrypy.expose - def expire(self): - sessions.expire() - return self.page() - - @cherrypy.expose - def regen(self): - cherrypy.session.regenerate() - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'yellow' - return self.page() - - -if __name__ == '__main__': - cherrypy.config.update({ - # 'environment': 'production', - 'log.screen': True, - 'tools.sessions.on': True, - }) - cherrypy.quickstart(Root()) diff --git a/libraries/cherrypy/test/static/404.html b/libraries/cherrypy/test/static/404.html deleted file mode 100644 index 01b17b09..00000000 --- a/libraries/cherrypy/test/static/404.html +++ /dev/null @@ -1,5 +0,0 @@ -<html> - <body> - <h1>I couldn't find that thing you were looking for!</h1> - </body> -</html> diff --git a/libraries/cherrypy/test/static/dirback.jpg b/libraries/cherrypy/test/static/dirback.jpg deleted file mode 100644 index 80403dc227c19b9192158420f5288121e7f2669a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16585 zcmbt*XHZjJ)b2?k2_^L2jDScNBy`k7T0p=M0wRb+K@7cviX{oX3JM58C3F%%kX|fw z5dlLN5D*kZq^Jnkxp}{D=Ki>U@6JpnIg{CEPS#%Kd7icQ-|W8w1Z^y>EddY+06?4< zus;i!0bE@0|F5;*2?!xTykIW~AOwJf0I(3S-viJ900R2&M*nw#zz_h6;9UD}{oktq z00u#zT!H`y0)}wG!7$D@Fz1&b&OQfqgcY!mqbD6Q2^TJggi;Ide4!Q9b&W1TMHF?- z9(Rs-eshfJ9$OUpe|F?Noe+3`2H=DI&nF?^1n~Vu^T3ZI_>QeL%Ggu5I;IpTZh4}j zErN>A74Ka=3t6Y@@U`te4t|Oc`i2Y_T}~n>kkZEjrY%(L_%GbVO&pJvv{SDqa4YBK z5&2%mC`qp}RUW)|Ubt6?15(cfiRZQa;fbHlOlr-)`MS7++VnzaFzA)aK&`Fen9kDQ z+kcsb)T3c1K7A#*YP<|j4a$Eh>3NhV-oxY(Ee%wkoA=S_<lTiYNnt|y;al5Zb?h9Z zk7P`Dg<M?3{MOciBqVPy5sx&&MQ|cddG09MFdZuA6Hhp?11mv$#{{*N*6K1}H|jmR zU&nOtx+rYEJjQ-p3V4d%pW<)wlJiZ_7Tt?lBN!p}f#3}8!I>|q6=&SAAFi>&SkYW| zrdLsciEwk?#yPza$wULmwArF;OOty)zC5<ceskCLbzMo>``q!HsK=ImsZETR_7=vK zVTOAc<6>7qpKTW_6uj=Ui3bn8qQ_FrB%!CQVb<uOx*8%V>dPERX=rF^iU0(c-cn;P zcU`#>PCOtGMofiNqAW9<aNMHkF(-}m;BLHIiMvf<uw&GDa&IIl$q={fbW-b%9YRel zee$9A3?mr%IJ)aGWA~XL`K!Z43|I?`iOiJNaLgNF%8AlO?2I+J<qu?2sB_;aFWxei zfzZIujb9i;GShB>Q%R(<9&d5R#zmrxKX<UOfp@WRiC9&{r7rv0<b<xZPPuVQS@;!W z6lqK`st8WCErjhNjE27s&aKXU)xqI?eGOY~_{LA)U$1Bn|CgxV=mQIpKWrZ9OtOqo zG-(O9<tp!c|HC<@v0*+GhWskBQsG!*VHUAksulXSTXo^%&#a0xndvLh=S2nTrDn4{ z8b5z!iR}XlOR*!9DtAEs#W%{Mw^a$b1=Bx?akFS;Z?!heUuV$ykacRpoH*f7gI;U% zAw6qhopZr&z2CWv#;?@Qe_X;&zr?S2Z?!)sWcEkBj5#=zWE6$3G>~;$cBEJFm)5*= zw1-rLfYxcWDUlB`;TZh{)o*P&QW>O-&`(Q*3TUcAS=rY`3(Y#z@YAnx1jm^m>D$4@ zrP@$Idzo|44~BH2CT`_};t)v0L(f*obO7mYeHOO@3|>%3pRgldM3UdPl{Nf6df814 zG#pYUED(yP6i8U0E*{%muM+Ly>!g{gQ3b$eshMwC4tQ_Ibyn78`K@e?#C@RkZK=gR z5LyRf=~zlg^+2Q41g{vT@%eC_q~P*kFG0{W*;?RqN)fq9Wafu+4$_pZKI3UYJM?J9 zSQV<hr$KP|P#bh%0=!B#W--H3f&<%kRq+&c%izyTF6y;Xaa^rSf7jw?C4&boj4ozd z+ZwVxYcy0!U%9&kw>SJEmU<`i+7emFb-^G~Of*#X<F!BT1tNlW<!3htrbjTp)sw%5 zV%^&llUr~RnBF(1%x8Y*Mc1ZSC2@U~!Ip=T53V#sSzx=9*tah+PqeO5Vm^-GJ|GSE z`1gSmgE6p&FL-lQ?egt)?w$R6^Mp-FK_DB~91&c8@=|rF%ml9f+-dc&FDzQ&@CRcS zg|@~%pY6sJKJB%GwU%4$nP05X&sRv&?DhS5O_cIh7?r}P8yR>N-M*um9BSD8Hdrd} znfru>)1xT;WJ3>$1)f6hyC-q=;ce#cV#-YDmxz)wvX(nV!>*`IsoSSmg-!9&wlD5D z7(KO{Ew2va@z<4UKg}~UvFkwKR%?Zj5la2(<`-=QrQc#@vnI6j7F{Lvy2#Tb!jX`( zz!?i$Q&a~(**v(Jo(+SIU)0+=o2DxhU*|;1zrWeJOAs2n5?o5lw4_BGUyB2_<Ub{e ztT$ZrDdJL^JQzTbeBv5<uCIqQt_k;k?;0bU9jdT&mA~>~1>ewZCxt5iA>8QNXejy) z3k)TkrguJNTd%~IPhY&F)X5|O#}$~lc#{Mtq@*d0O98|Xop3sp%}A;=AXapzgdTd% zB$ahX$1il^k4)!Dy=9`LL3iFuM|oRti{Q7vGi_2~Q9X8+VrE4T`t^yh2mJ@ra%&e& z-e!<?r9frWz9C<@Uw$%bejo7oifMOt_M(?@FFro(12o<L_oXD}Li3-T6x6f93(3cy z^CZ}&|4Si^&3%cnCLBzA(`@nc7aQ1{Ol|f{Ih=O!c751m%BdT-s|jX-y84tYzc^zN z0Q@F!5WC@~OYC?jDH`PQoTqRkcGN6-t%3wF$cjVSru0*|8gwUB*-Nl7c1HTa=bQ3q z2W~^k-N?7sB(&3nS7_KuGEBaUztJa5H)?KyX>;EMdw==QH1V->21=5=Ecaj;@02dv z^9xTjdGgm(jd3GS>#kDzm*yNb&?XT=)%CbSkN|DSi$Wm9S3L#lp|*M1=jiF|6{BZ5 zs!%~&IQV+jgsgQ1>bE+djwHi8N6s4ZtYQRMQ-6y$Aj)=d&uusgXJg2Puhwn)@;$9= z1rbh+4UBTn*h#8eh}BtK@CNC-6YWltd<w8i6{wpjH#)X7JQ60|F@CjcK*kG{iTbJb zL3yN+0nc_61(oMK8GjdWe~3Al%{=#$1s12CC+o%`oe?=dk~4z$iX(=h7?jA+DRT=( z1m=LGHNfN7mK7!!rTIrRRU_)iAGRAw_ng}gHMoD6+@jqHLP_n7!`##abG}H5FaoV5 zj`i7+6C|D1ro6s_P1@qsNhgJEnOqe`DmUKz+%WWOvi7&geeWATud($fVk9Jl{(jb0 zA;(zF2!=Ndh4HM3@kFdLHK&S@6CjDl*kK?axj9wYS85JX9e5S_R9=skj|xefZ*7?f zJrJl10n)!cutLuuRXQ#;sf9S;OsC!QoMg^`8Zwlpi1h24%ikJg6&GR1L?9Q@&+oMs zu=gvAC0aZDwT;V9F41N0FG9^B-J6Hw&<gj{{X*^pk5AfUwdn`+nv^w`3zt+((!J%x zRJ{{rx+2tSPdxAHIMxMfc+Bck(C}S!9zT<0Q*7NGc59~Lc>jFK&tEy;=yrt((w{3V z^Qt2diQh1tkJ5}$D3i8TeqVeP;~1W#@GUkBFcPSoEX4zYp*<-!Xih2NvQwvY{=#<b zUD@%~&f)d^viP1&3%`)Z(jG-VX(^i`E^iw4fiCh1n~~QBkJ3kMQc$#0)Lln2|CL7Z zQn65>jqoJY&+4iU1yuLtAv+yQ>PcJyI_<dl_D`ZNMi``yjRkeZmy+_x8zP%!6)jfe zsu4hs>O?JWL7f3hlex4Ai55t=!1IStU{vP?;rRor8EzzO2rB8r1Rh!S-efXpjd(ZS zIPdix-l<jycEuRg7u|-MZf`?%mhA&ST*vzchrd4G{)fAhmM4-sbK>NrN*__yYHJ^; z12qR82GNoIE({s{-G|gK`jnTk{L+V0QY@@boKMs<5{17hI>+Fi3zCqiwxKdQ2d2Ac zzF(Piu*;C_v1nxpH0g~hCJ}H*XpSm@l1y;}C$uX1bmV0$th1CW6mr%Y1hEajMfp<` zk9)Cae=><+d-Z3^Z%q*r-rI^sPl(q9!&@bnFMq>TEGZfl8#X6rJwh7fj}VA{)QS+H z5N@?>ADt)H)J7Z(?z%yW*z>Cr<WTx|UdcRdq)j&Fe*5tA4L7s({-a8FPTZMyv~Ezg zyOxR@qrAtvozpRg8-ybBGce|(nA>RBVLEaGt}QmV;rE$PmMrb_N2qL?h8VCAQl3E@ zO^(q?k}?ZkK<zmRt`?sZY-$fd1Sx1T09(z4{?wFx_bG9DMimd{dll<rF&sbrcHBJF zso$#2?nQ)nGq2LO@=*E(iZN(HveIp+C_0-D5a+dAqnP67F3I-b$Cho<-=xU1;D%v7 zmx^Mh!B2p(3wBP2ge`pM<xJ1VsR5y1h~J)hoHVMeRTRo)u+?OI*Ea8<k0t}wsP5Rw zL;V_ZZlcK4+~Uf&6l8I`7N&Ev6!{ZXdi9T*Y_Yid@y)+$7_xR$^&i3GJti`?CU2K9 zxB8{x&zvRL$k<KM!-k%e+H__F%U2Q(mISx_QxYeGNz!wW2#lxej-`>SdfU9#5sx&{ zAyKr~3%{P%(tR=)4EvUuGfw>hRTuTVdUd#}NK(SUmMRy*`>~^194P4EaWbOYhMx%m zxD+1xGUj^~72L3nR?-mzJFiPmX5J49(pXjJ4{_`+FEo;~w=~N0lGVAt54?yX7Hrl= z=sgm&<-r1KVt%K!o4J6d&`JnsX#^6@a4$_eUFRUB?bWtXLEsy^^7EglZ@US-;EA7k zK^VNECsjL`c8x)@5Ij*=_%ylm>1`tPca`UT$n<|kWgrzod(mep>zzF+H`pdGVKanE z%S_HG8OyUYXhX`aPVW7{9D89Ab6;eTV@wC{1f7CI=;QrUGb3?l6b*%!WJIOoVqiiz z|DPz7p*cXML^4p|hy!ZW^d)K}1zV^_{s{gkFz}Js!SA7dGWUZZ$qGf4$3Q#eJ{~P8 zu|rx32qgXEQq$C)YhM3x1F`bxRr>6cr@picLoER_10~u)ag)zy1%a{3$cKIXr7>3N zEfICF+wK!^ABBu<<qv1M%Ab5E8d<FG15bQUq7g>!?SD)W>W?9k1Dh4Fx873ce1-@( zHWsS$w<lZAekkJkAs@ZyU#Mfmv?&(Y^fu8mGodvnN+Rwb+7XO7Ds#L>c%WBbH#P8Y zBi<z7bKTJVylY3`#`H&o3TWl!t0Kl%a_5Ogn4m)Hb2P2!s$U^d;Cw|-6YaaH@sBgd zvk3gh)wj>GEoC>DpH{KsA3PtlKim9k-M=XFG3@G0l^km9ipQNojBSwLb@g!F(~0w6 zD$O8EQRZTpb~7`_b(*E^_*~<!tdXP=Veg<<YR*2p=Hu4tSJCo|D(oHyVkMUip~ z1<GA*3o$rrq`Vb)xYU@5^3cY_snKMAvF)iOjj%FC6v~d_zp{MQl^fZ3jmcx?y$`76 zYO7$6_sdBi-v<!Tn3}EuBYGe=D%Hl9hJ#se$D#EHr>@XB{(1QN%uo^gML%l3pBsLM z&njvFB-aaEDJ-HJHK66r=mByI>L<}cQRv^Oa&zICO*(Z!GW!r<E<4kFDv0R_$?}$1 zU!MD}X48I9(FZE*MtXa!0pS`FnBE$dAt)P6LqMMOOSM#Hj|PAClDj~+M#xobGL(K+ zBjI#VEF)t&ePn5B>Q4r&_KeQ&&OgrZ>BrUKOS%J2+yU_cqdqfT6Vymj@*=ADB~FdE zRh|{I6y^07hdA=cV`%BDFyjHXKo1}I)FIvSHm}C!5rJx(xO#ZlPKbu@EN?RJEv|DV z`jmUQ-QJ(_9O1mRX(Q)xDt+{OfPF?CFD$*^35Jx*;DB}*wSUL8|87S$GM}|laZ;CV zyei@43p+ez1&v`&y1p=MZqEoo{zHBg5dTIUpEO1Zs=8ju=AlGm@mG$Ozy!$E0-J)0 z>wY89PlI#Mve6o29!=Ha)04iZ_W>`-&Tz=vEWX1hjg{cYYWrnMgV2CUHXOrk43#2N zt}18VT4|-pzSj}|hhTf&KL<)A^0VzoBJ+-n3<N;ACA^{ou{=jUgV`WMB^5NNFJ*6= zlLkNs)d@Ym+S$W#0%f08(@bQIgjOo7SIXK9EHs7r5C>6!q3;B?Bm5IWpm_i}#exRs z(0ztfR+2>^tdGPlBmPPle3UZQ3XP1#ZU?0#5ghmj!jLOpn2r}2gIb%i$KtHR_kr}w zVlDpK3`c&gUWpazVgm~q_h1i;nf%elAic?7xWnFAxrUn9!uC%tsMeQ=_*dv+AGyp; z{>;Ii{@VGXJ^o&A5`NZuWls4)l{(;govY5o{lPetCopBVmT6O+JNM^R3BQh`Y5}2N zh3$vh!#V+w_GQTxA(`sA$Y=7hYeW<R9Wc8#?=qn;^CEJglVJPDrEamVOj?#gSsZZ2 zeAxh;An9K{&@!{ju-#+Mk0oLbYuwfwW{IZ0P|a(x(SLoN$XJcYc|1IeT3kBub!50k zm^#{JyCOdF?9j<HWrM~K75D?<_cIK|?}_l5eaGCM(_785_X&$+0(WKlF3ot)xvJN< zGF60D%5_8=4nfbj`<YWIrjx0V*7Rg7u%`x}hV818KLSH$L4T%<wI?w^`pq~^M{k%a zg};#q8}&j!ElXAKa6w3ai5Qp`t7d<j2w|ZG@~}@|oBQ2QQcjo)D<pMFt>->*^hSge ze{Qn6c^iz<?DET#PJx3wVkuxRZ`hM;UNBz=!oVDOaU=^gi5{anyav~TKN2A?E1~qo zxe>zY&2jFAL?*a;1j@|;;A35-%F38Qt+n1s9{;4SU$pDBtYjtH`-3shIUIp8yMy~J z+-^&t8g6Dra<t=D;)i&f_hYtK8Uice=M(Q~i#GE%4jyIV(UK8*fzJ+Ci@t#*kJ#Ug zmGOT&TRQvXl%=05Dkmo+8mA&X3y}7zth-&&ii0dZ6h>Gv)0HZ}mZh_&F1a!~G#A;@ z;!^5RLLq~4-u!sj#Ps;Om&VD#cxvj*LdsQwz&iD5PJH@S7W_X8{=~NJVBr+&@}X}` z9=H7PxrqYTocLj{pc{>66iYddcs$t(VRZXX^zaC7?Lsb)oU5(P5(jo^j4#nRSs%P} z%%-6F%NUr+Xp+da{6<_)qEy=4FXv?c!Pg5SY{fKOZuy}Y2qQS}1C<JJC#_VV(?4*4 z91n6mH8JIlM*-80r~*Smtxi~_kxh)-PgnkCm^k>I9aC2o(Nso|kALE+fW;>7iB%aq z#<nrwgN(7Y)mTjOn>m{lQIi4J8xMCYM0~@7gzR0i!w(;B{CP$PGd3e9-ibTLuDB>S z814c!JeP=(RSEEt?V%yF;sdHC)K0%uKNTIz-EbID5$MOrx^FV`Z$eermS?x&tA@lI zGy1LXOl$Fs_ek=?%L$)d<*El&j?DgBRnhk!+d(m=YlZy!Ss?2D$<kVbbw;h+%0R5y z*+IAbsNlyov3l8qM+{um>-eCHijS0p!pt6i5=SPlxZZ{7XQ)u#`5fg0Pg-1YCJ_<P z4?BIWhfm%^E$fBr^(xO?w}gm=(uaC#si3Z?z_}ZK<faa9lcs+N*)2M+!8kKv7c>9L zEE=UV_buke9PIN1+@#me#P|o1;^wHS{HW^JSXtAc;NltnC1NMr--+XYT#E6zX_3*x zW5SuEZ`jGOI~VX3$B)FFq@;WQ$vA#+SDc3Ujerax@wUIxW`B=U(aaFIM_0ocuOI@I z8`Ni;*r3Ue8U)#(blwU|%F>YbI55&3WYeGVlu~nokPV2U#o7Bn1Y><K?)ld2SB8n5 z=4B0DI#>G|<vy`|U{c25NEWZw`ojv45*cr(R4DsF>aiZ7qC*lLk;NevUsPuVFzz0; z&=3nhbbh4y&@jOLVEcQugpU~X<wqWdFaIfw;!8k>D0KKTgU9k!zbZjzdBoS;zy%@H z(ZVl1c@GnpzTp?n@4Dtawx=pK4Q5;EVP*K0=ouH|xU`%6uQ3i*mw99t#<)!%!}}xG z|1d~!MT8C-eatzXq!L;m)wp;$4aWwI&OUi#`1B@P{0$^=q!tPuWpRZV&RKr(%ipOY z!HqFgXj_=uQRTLqk66@S&t_t7*id(m;U!$g?8z@s62%5re-SIJb-dlnqbIXj`@n7C zC~rfD!$~2G`Wxr`NWmuAm=1Z|f&cL}%#COGAkPVvk5>kLrYCSv;<Bmh!i`IQ{(Tp$ zP^;`p&jL(I2>8;(^p;5+Zk3domGX^fEG?j2p?^RaCQlYHw?0okt%W;`)-wp&vd}?u z_%U@Zeb)&fgn3uTB^GCUM@e$i0)aZO)og~_#9-ygI^R?b&R3w2WUimT#@RArj7Hzb zWnK~xWWfoKG|<c-1yE2|zlIp~v3wEImMeWa$|*o9>PVKIw&_O}UrdCHQU6^Gr{r+r z;adHy;pZP`A<)K{b);`LB??NrPko@4<k9egcCVNpsId<<ja2zSqDrDX7Ra3Ri$}DC zWV1#nTXLdIU}@y6UK``bi(BU0jmofqtJg-9w6t|iuFjzN^@l&-Tvn_kT`Jd!a6-_$ zCh~{BFd|zgDsPsE`rVG2_&Z4^eIzUp$c)1-&(eu9HAK$#8$;w}fD+npY2%uyFGdiX zQ^)P2z2%XHTHOa^iJNY5_=jWHN7{#$IHZA&<zvR1Wi(5<dMAR&cXbEb#*&0AOn`2r zp(BQgo>^ErI#vJh$lec8mX6dFgZvwzPu0(Gw&7vBohF!%If^i+|9-YOmJL~_zHeU! zaRMf%yq7k)O_be|N_UU8vD9p(+4~)E9bixM?S}HF@iZBICm7OwZR`FPOk{=?{WiGN z?oIaObqZB`;ac`LOFSX+nFNNtIn@PY9ZJ*@;R0VU9kF%Y^fPNP&ub&y9kR2sP*qVH z!dChV*<8?LtTw|?qkyd>7%lXcLh)1N#B5b_K;!~ydxy!Vhi=wO^G+RV<d9i%)xBH~ zBA!D1e~5OR&RDB1f+yIMb7H2qSul}`U=;x;nh?G3&-jEWUJ~|7dKMMp@^9kgqxs4H zOzhJxq}=-=(D$O4BESMk8!##Z4Qjr`X(rAGS*V323}W<3Gu3EU<4(Gn`M*dx&BJ{c zn>Q5-*wr6@`{eXIcB0nE&bl%qtUxobl3_AVOZy{-cVH0GFrHn*6{@j>IA3!R2_aAB zjdzUp4XyrU@BH~-`_me$G+a-ve%{31z&;>NtgZzNJHT`kVeEa~c9r?SX9ry2lArG_ z<EMY0$RpL#@jK=ec;;FgdSVMB=V>SR$SF0QWKh=}Rgh#8+Y`J{z`++qv)p%Z{+We6 zl^NEcK8#4b$=+mb=TI(GIGXJ^O}^F6IKH4_&^!Q{VmX_iQ}d}gaWY#pdE#m10avzu zz@W;e<_+?ZLf0NcVmihxu~U#ww*I$hXJik;z@R%0U+-#uDsHl4A&Hav#UbZE@ceei z=Nd2<2@Iv)*Akxy>h*bghg*q=cV3-h4Y5qXr1z(iqjf39C<lD1(!m#1aa*q#HFvFA zIPLy?{p%zgRQIIj5LuH?$m5`jgine~Qc2aPOdYh8-)WxQir_J8^0OpGQU6|Zc?_%L zW+@}W(Q+*olR8@-O(<h2bA8LS_jC0Q3M@d2&PeJ)$1s(Q!k!_-HV^5|?KhZH3AfB4 zg1-@WAs>>#WdGVQ`1#_Z4E5KGHg{0;iwA{^y+95t6{maya=!*L?j`}?k1<+*al9oU ztt&cz*k@zk8Kr;sK9CJH&(E;!vHFe~-1c&{dWU6qC@m!>A=Hr{j2WXw51-HZnjbh` zq1$YSY+asnEmxULotv0HvR2~R-hFc_VKT|%!ci~%8QF8EF*QoZMkMlj7k3yM(e2M_ z1gi1D?_Ja7k8D1+LEkkK60uUShli}S={(z{dR%sln>sF*oheyZtTA`phA6B}<x{@; zgOIY08oclznDybZfnMCX1?SI`k1->*MOm1C3+iUk*BK~|cNe#CJbcGe>7pGc;)zb2 z#N55tSW``&)ycgS<>AV^>iigaEGr!q7@{9H(fwDnbEN)`L4uekPa$b4vY7rxjcvgV zWhLo3BR2&vyG%f>wZmk-5g5v@x-xH}qSG={i+C&9zUPQcpF7?HD!Aeo!LEE!sBw;t z;U)P~tVlY=gPu|B$_;rKG+BFmaI)){5ilcYpK{F$Mo-oUwK$52W(NIXP0GC4abgHY zPdc<Qp^__EiT~@F9HZtN`B|h5s^Ik}e@%nY_KyhaTVE>BzpkO=7llRUg)jlZb?JEA z{fTdmmi%9@t2(%DwV2bRy9za#O`ey@H1i&_hx`FLA*f;f;kD-MJ$CpyYt&kc_BmGA zCs**b3g#V!Jog}YNYJ6Ey{g%C;R@RgF~OpC%`Cg{fx2!aa|bB$Rz$f!a$<%xNYjAg zh_BbiC-^SSdA&X%P|dXqE{my2T5A=W;oBORT@aTSL!srnZ~kY@HKdQ8&)sW3oJ%^Z zMdHd%UsGBjlr^8P&>xE!UyQ7HVaPZ-DR7HJ)zMT>e_!><?@9~zS3L@RGi<q^sh9M+ z+=F+52RwgEMN)nr$S$NFxP+EEa0wEzMu}LX@k^z__D-D40OsQphia<*?CPcVPJ6iW z6#O}Ci*cJKE%`w2CLZ7zlNX&V>)y_%IY(5HHL)x#yMSi|SM+#Ap=$NZotOp|*mHch zmPzNdK7J@;rdW^Lum_>G`HcMLG^<QrVduNbMUyfwMT1XkSDJyj`X4UQrLVVDfvu)7 zxJd!01~WHaPQ)z)U^wvGeV`3SI;wJH2J_nI4$10T&K>TwH$z_1h@+6xYD_-a_a}k* zr6r;Y8&@q5;;t7X_laS3m7wH0{p`t)Oe|k@_)2L}UxN@dcMH>gLC0)8Tcv}M#>h%` zMAnN!oBLuepa&f^Koa3ac`u%5$TAjmnh&9t<Z9w_i(pa<$4?2DTVUm1CPt=X61;tK zU)*fX(9N~=j%Z~(8Y-<qwX{EBEc-xv);k7i8?g2!tR;F%<>3du_3H^v*ba2Y<(4Ys z1T->6wXnXWZnN43;}L#S<HU|=MOkoXVtxY0%{VC3psF$x$rxU*dhYj|DLzdZC9C(d zJ~)+}x88H5m!w;*nX_YFEjwBYC{6$~_9Ca!uR0`#72m88`Qu9@sHnW!eWLNT*xdwl z4e_PKPMz`sia7k}42vHU06t8IAa$_zJ1w%XZ*q<hjIqa4$c;JBt_o@~m+etdh$E=C z7dP%y`!sp#Z_S5`MI?v!0E}-gMs!zqv+H>6D)FF#u0qlZ8&{x&Obq*h!HUy=YFcMQ zLr@|*&a>lnu@ARzCKG$ct}X<)s@CfnVAAsxFx$LpE(m7wpAts_9fZNy1PB~1=Nu1& z)`W6$!HB#%@Ag;KLO}0O(f``EqD*=l^ZVptC25<-6=Pk}oZOGJQhW(nE~H#_!t2)j zb4XRpT%#PgSz#`#E6ux`iM`6mjjYVKtIs>In+5mfI%2G5V;*Iaq>yYrd7k=(S3;Um zt1y}bksZN!wq6oE0t<bZOa{IC@-T&Hv;D&*^MjybzK)|kZpP#LM&4UjCQe-1Tc0>V zH<J#L!-qmv^VSz{_tV;Sg|S{jE(o<Lk&6ndMA1WUXEz-bxL0PwTnl}ey+UhCm>MZL zJH1FJCI*5xQ;q-FQ&!Qzrynr)>jqh~cBy&(kb_oK>hQqa=2W3gy0ut5*sRz<T%#X? zqc`5$xD`?Ls41QuKU1%A*YRmMgi~w%9W!&d=vb)V*kuojeDmX}uNMTu@DedL8sZes zT)RNhfQ)G)QRtg2$>4!a$5?c5%8y(0!hG30V<%g2U`$2cf(V6&imy;BZLTwp5fUG~ zU;DU9P_|02;?$wjG}%KX4jjOVy}?sv^j-cG>cjN?+3rsSV;Pif93C%1T0EXzO9U(9 z)EK^d8aT!o*QHgF)8*fbD0H6P45!39hTUmaEiQm!ZiYL8Q3|3$=wPmQ25%VTQE{4b z!#5TOCXp#V4@R!GWfYdRT4d<T(1SmRcSM%fhm1&P^`*rmQ&h7pGrTg4HOD10UAiaG z<V`Wm*SFUgFBVvn=D6y(pG9}UXi@&D-%LmUK0p2n)yCvo8^Vd?Qof%|JcU1&pjCU~ zSHQ|9i%Lc+O|l!%dnhO)cSgiWz~Uez>75!);6xn6n#0+okLQU-JP4Tk;ET7EHYIed z%oFGr=a-H)e`opCc*8{Or?#+zeJu@w`d%{2Shl2Muv-@`&Ch}kW{@HsP|}<b%}rka zJOti=ox#s0F1w;IK;nf*&&ruB@_<2*&~Z_TeL%i1W=pyx^No(&?Bo+0A{2sx3F^o& z4l*)um&m@2+2b!$5ZmUr7u6Z4y#oUj!ww~Y51+M`1UGK<PjRwkNIZjW%&~a=&$;Ug zYH~RznR=sx#u>U8-9+97i~&(ix?5BtlxK&dfN~rkv=$RBT6C!FvVer%LDH0FH&b2O zL_5Ugt1oT%vD28~qWmeih#XY-omKfSRoXj4idjS8JtwX_g-w`APUktZ_9Djn>5Fb0 z-GQ*QDoFU~{4dMVaBsKaJL*P3-J$#Dnbwoeoc|J8fOZr#;=<-bGUR%-3D^;mFlBJn zIWa+M@wwi?5R;6CIf&jcow}@AD9<<8#9b1*rd{o$_p0~OQI#Kd-iQ+~owYk->IAig zFsQ55|5fh5?;V%^RPWkn<?Lh|H8O|9+$=S#pBFtq;Mu^G>;rSgHUPgzo=q(E;@AYa zLJ0s8iHHzgR2*gOUKfDFYIrvgB>tMc!S<;IG{`jYa+$d)>g-{UmG<q;=28`-)c3Cb z$dCX{&zyaxC8k(&xwS_8Tj6k*7dSG6ELT&ATjeUVeCAhDl%|ONR(rtfRJ1%RtGj;( z+mxU1>PwYuA7i9Bfcxt)JIVV^m-YeLp|RS;kTH!j!u(M}S{#6R1LpHU$`!v4gcAzu zJP~P1)ixC4H$J6KV6)ci#r(6}^m!I-Ly^1e{<kXG4~V0aTl$ygpRTWwb+Q~U-<*nj zsK*Ll2Y_5~n`jdAiGfftN70r9D<W6^X7Rs}=6m4G116)QCWw68Df|_K+RAlMAEGwa zG-tBp+^0l|l@hJP&NPKU%Hy6khND<Ak8v;Da85ourod|y6HY^9<~-*-Au{+eWSS_7 zjM5pwU)e-t?jjI?GnnhfBQ4s+i^@^qUw#SMl6nMvH%=7gs@9_h1)Z=4=O&(ghIoB2 znuA>0+a~H0p@3$zJ86#Ni-Xldz!0TI53N$d=Hc`wb-36c#NH(H&)Nv|G&3lf5R`OR zzvjlb3dzm?_$*gwlswB)v1P{+<tfB??qhQo{9X2($DF7MrwWwAJNlJADK)mi7Ufx; z7gN(#_I>k_DB3muuL7WA)#4w1i#dIDwJzO78S5jzM)vTWu!&)nByJxQH1h>1+>X#X zX8Q$2(6lT2eCrk!6n?R3EMZqwF4}M(usuH+TBxkk&C!#5F-=RP;1|wmSz^;oT4474 zf=bR*_ztm*!yIUh1+zK+JMV(xrdf-I`QPxhL{XpyS7(;rnYZ0~QO~R~#Tg|Xz4hMc zEtL+X>kpwvra6MBs^vXt@Yq|~Mzz%N0h4Vt==%>xUdKsl@>d^s0={OkgSSk0Iz^x& zd`Us#fhVNtpi~9vSKF^ThG%BV&-{3QZ7Q&(RKb^DD&5)RjG6?ff$@1}9%FV^A%@k7 zk-1Vsui!JtTuU9H_7@fpe0EJ!?v5@<!sSlVhL;Gc_4l1e*tFY^zd5lfbMmLa{;>_x z#`laF&9y{%6B?(<ieq1aT1>jAczYo2+PUCQixwb-Y+2W9qMeq5l1bl)VjVAYw_SAc z00Ko(54C|MxHKFHmxL2&!;`Np4OhBM+2C`^55G0Qs8JYQA4e?c{`Zvz@%Vlg*US(K z^7Ki4;ZbF7q;B%_QZJatB4*=oB4SfT{`#*<u$AJ3o#bzhT-ZO<c4X$essrND>eA9q zQNFh>U-6)cFwaI@<6hT;LxL)ypJoV!Vb_m6RN_<4yNL%YUnplW-d&A_vpNU4R{fVz zK&h=j%A%{w&p)V-Gf9d!7g|YqqEa4h3!~J;d$-2LII5S)iBC60Pvt#|(A6zF@P-3j z+6?l#Dzp#hU#k+(GHzN$w@qK-qUdE-t9MnL@@pX*28Nc}m>!P0+V*R_q6cpta>cEq z=i<V98YnlZME0Dj2;S7=>(;L;Iq2R;M=@T474Fq!-PqUkN!9ghESRF%jwS~wVDhn9 zsk+8C;Yt9gPfB=t=<`=580J2^`&1rH*azN&V3actm23jVjNERHDviCJmmFFo7B{tb zxD_O9t-5Fcf|%xO6&&USUPHu1*pztc#qas`l%OOS^^Jrv{|^2PXm^zp@9|zg+4tes z+<xJ;`oV1SC*-4bMne=-K>n0jG!#B9+%MKQ4z6zFzC&_)V(g}-qCES=^_n=`k6}nt z;$_h;^)(U@21?*dg<!ZVu;TZ35_`^moG00dC@CR>J`gUps{;C_ZcagMj$UOJlExQD zI<>ta3a(by%RmiEElDsP5A#(DZz=#CPQF-tb+LKH<?f<xn=wmTz$*fS(JLQ-i*O_( zD-&u@Xv*3R9w%N%ond2FDh#B_IFKazXjBQTTJ2au@w>|LeQJNT46i@dmJbjGs=?98 zDgtet1E5Eawyw-VjNIJ`a?jqwWXl4f)tCZb-H_?$OtNFynZSSJd>8HX?=7!l-<(*K zm@{v6#U+|(M^JC|7h2IpG=SEwX(u?@)X-!ydGSV+*Pc6|8Gc8ao6h0#KWfW4L1*~N z9y|!$QXuH-x8FK7UV)9Lm|5<m8RTYXkNh)*<pNS;qnUcZlyjQVN~?3DcLISHV_{|> zr~0ihneRsCjv7}>>-9?H`Zb0fqd#;v>UNTSVp2?Y`_SJbUzw7hEVoQjw6;xsS@-@+ z^uXd&SCU=FMUxEo(DfJFMb3Qwn}1ji^pRJ~B+}%!V@@!Do8mW5DRPP4u~5MDw)YO< zTnYy?U$aY#>%=&EM&X$^)fZ(CB<wQmG6v=~p5k3iG6>=^g4Cza95}I*>MpcNBaNMI zDxyqk_SC^IWOxG;M8qkqwz;2^BB2%}Jo6(1B*rLDA~BJrnx|NN3WB1N*+ni7jNNhP z?4NSUvB;WXNy(tMj^+hV$jAu$MC4QyD_iw$4u?9oK5z*?dVBz~gW@DU7o!5hbUfBL zo5OuWDZoPuNxR$x!1c&XovM3vFKB+k_>?vUm_)0Qbl*C?^V!A-v~@fus3vWytE?P% zuO=o%izN$T)UIisI-U3(i>w`b(Uz(n_x!`0zg4N|Yr)fh?mfA<u6n9j#0;hoW{`8q zCbXfsVVHPXfSr0y?rs?bG3a}u2=15Xy|_EUbu3%Svwhh&EunoGm&3kP+a56xQxLm3 z*Y<-*`^0HLhKn8U$qK)fey}8iD_pm$_Z($ZZ^L9qkxzD>B)O7Q295e_UAX*oA0QOA z?GQPk3Db_v8gu@9FM9HEdg#&n)$Lcx6B^TO3UPu*^*5Kk>h?80dj54ip!=7DYREK) zv;6lNvJ`;;mv-sgF!g0<hB2`_N^+u;*I3f`lVNKyH*hGfs4J~MnzNER9-Wt;)OwNT zNu0JVIPxyTY0_y-_K)IdN!a7AJ6{Foj?BDsRg)#%!6cvQsc?$T0~%f&(k4PGgm@U_ z?X{!>?Tj-J4D1P8NwOA$h)@x-A{X`|luluF#8+y4WMfaG`5@?I_T*;>WtMulg+*Bm zQlPNOc+U^ciImGx$$=#*uvYZPMT{%|!F$WF8T3nSwFpb|ZtD^~o5C0iCGN>4-!e7n z;m_Mk7>E=OC_oF?f0v{#Oh{%T?|vMzNj6gt{HoID=Yl{bZDX6t*zj|zuDK6@rZ|Oc zVAMN;mSjNW!L2y?QhqaehhwXX=}2slpA%Y1{>2KR_;i7VH8}d;e_1(Xp?TX<sH4AZ zETZUP7XJB_tvMTaj-)6fWZ4%GC8jzo?Rm9c@72BUihQ7${L427VABokn0jck;KFJ~ zL}OV?qF#xb&BIrsk|!Cr{X7ly8Q6bWw{|%V4_qjb`qF*C!0%_#ndu8%9O@~Iretdw zTH5f#ZQ8tI(x5IE;puaC8Ba2~?Sy$zK;&(hGiZ<K?&(bp`^3pdU!M1sI@}*Q=?c3X zawTVS>U?+jzXPL5VJ+UASUt7pqS2RRL`I^PHduTdRnKz>6+GFSswOZ2V?oY;7Xp^% zCVj7;b~Il~P<~Kzvai4kI6RR3T}eD5JN$)ds^UxBvQL2TLisNZ3F)Qoo39gosPXg5 zo$b|@>wZG34qnk<@xad?6uxSjVS7)8XOAKLqYm^yR8xs24@Ko^G6gLw!T0qZf8;ba zf`L*QV7ICc21jDVww$=I#<5($;Z}0iGYjbHWrFsRYWP34_r3mC_GAgaWV@n8NsOG0 z!L&_o<M_a=zD=1VJksJPgF$Zb7v&@w;HP{=|3{s~|KPk$p(_U<x4k%;QxD>Y>NSt_ zt2TkN)`D48w&rw@{iur9H}MUW&xuPgEp+)n#`ua8Xo9p7<B+gyCjvISNvs(2{iAm3 z=8;&T86JAuLgQzG6&OKn5Q^{aYE50c0}8(Tx~KZo2EVhn$@5MqR_J;8<*hC%eNp*5 zU&@80U!uwWZ%2;4!yy&8=oROVWmo+EgODmQ?l?J~8U+=Z>5~CD|Hw&%UwW6h_^N%C zP%PrxH2mc&s;MjZz=;{AYNry9h=3y~jhP8JurC|L5ygUj-A`~T?9+0MeBK6DhQ>^I z!fTwck(3L^0}2hV<nWUv?LCI}fxZ$W0E5h%ypA)D1Yo&B9y1l#_d5H4ztda$=O3}o zV4X`6A8&l;=jQYo;zh*ZFY$qu*fqL=6Ad^MzEDteG$R(F$p4?w^Djd0KQZV{;wrdg z8%ERSUjH|tnD4!)JW-Le4_Iis$CK_L@)-Y8N^BT)Na<t(-0LhpSfPWvt><<fSBQdw zGXAtk(Z+9Il~cEMROI5ZJy=1SrE=y3sRJYkCrYb(|9mYBfz&DQEn&}Y@mgkq@#aQj zr9YX#v5C3*#-0XU*(J;MC|R8Tqz_j7sAHjm`^%?+bv$6cmqI0CwfO&f1)<Fa0~sX3 zUp!A#wQ#UkUdlG1sQa*erKBcbiB$w1*+{}1zswV^U}4~D5|dT&GqoEly9|M)<UC}> zh$pj)Y^06kD~coBtTZ4BaPi6q+&|#srNx`fm}KShB18bve&=}*PY>0oYV*JwiUc=0 zN<7Q8+>+g(4MrSl$V?UtyC_#Se&#SwCR?`O$fDAi#V1$4`OE!}psekBVtoFUrwD%i zs2l&dn1%<tRpjtmBV`D(h~~4k84<{rFCO<2*rm3vt5R0W8>nafg54;0f&+@CHgmDp z0(I)QZR9R1F8^Pt=4IOhrDXDq9iXUTJs4v-RVU}R<kIWP$9HB~jS_8^&>rD>n3`Go z`}~1-CBl7#^0lw1M=z`W>_uu!Z4!(EJ^#%=Ej_qPno{D<>&Y-N?{oBZ{>%k3M)xJh zfTItQj|T8#=B{bVj^szB6jbAIf}vAd&*HMn-`L*4h<;3|%{cqJ{H3GpKJa}X;K>}S z4~E_o#E5t2$3!dSmgB<UURDbi!jtggQ7DFDJB!f~6ZE^vIzzExdxd5>8SwkOC_b-L z=7vBLE+AJDhXPfJiYQ3|;KL0Kk|G!S^DrHNIY0`-V9+`9I~dHDu_3aykthJDK%qr| zckTR2iORQtR*u+Pm=}V*8Hu%Hq7&0D84D}DNxOe72b)U4fH$VPYQ^uO1lcCZh36FO zs2r6H@@B`yO7~^YMXgdigE;cU@#iGk1)9vuq1G<=LRNxVpH$+1SdEw3T&8BZ(RlzL z)N6WSY8sEy@e)U?0;l3E%3+nIrE{xU{I6I3t@gqTy3n3a!ZEkK?&j_Tp8yRJ!`pX2 z|FZ3)&UeDi3ae@~!%!dmdc*h=WsUQ@8{50ap{AHYH4|B!hHRJ7Cv9r!Luu#!Y-th# zjE)ta5r)#BId54-hc+o8Nw8M;>Atdxb00CoY9<DeM@}BPk#wjG7qY@a-1+qEby>jH z$qd&Z_q?)16<((0w?emShq7<bPyBZL0wKRL6(_(OTpIS?pO=U)bflt8Og;YGY*f39 zHVfTtcQ3V#SXa{SPyRP9q&awNR_oEDwhq^^!+)Q02;EgRX;l<UR1;RxN`#^bt1@fY zr~ky${q^PyA}&w1j>Oo7KA685>(B%)*OCtFNpkwdl6?{UE3j0h@pg;mpq5*1m1AKw zqi6E<okBcYW5lUeUuar>fC_}VNZvkPA=EK?*WHF&C}-%oUssHX5FocEapkEWWW58` z5BPC`2^fl{R-`Ws>63KZi+^;cv#nnW-!a(3n8S-TgaO`NfByRAI#f`OZWBkm;2_{k zV-i<nQ6ES#@2pF%pZPu5>ku3_Uln!1sgUCTCIaInN##uK6c8o(-%ngp;D-otbV*T` zu<c#s()N27_Vi#f$hC_nAdbli2PH)Va+7~;h*wQHv~<ja{u?-STf<Kb)|e(hhuH7L z<UsE~t!%}e?mAwxS!xH*TV*P8-l%f*{L1I9{lI(H3)>%dhwTG6`>(6x;xn5b=CY)R zvp2KEU`546FrTlyQ<`i%*niRNT!jWaxY0)G2A$@H%~9fMGR$@uj)HkFJWgtV7N>R* zN0F<Sa*teGx<y5;zX_C1`I5>?P-%HIyz}a>Xo(9_@vos_K6VisnJ{T)Jz5zO6F2j? zzSNNmBUZSL!5;gIR~+Kv-wq7!DzoC)-fDsq{3q0OAR5|tG~qq0$78jruvjOsQ0}n$ z>o_urZF{QamKuytZ1D6F_QC~sF~}yRe}&>mA3;C|Mc|H~&#gLB;i`*&A3`0P42<k* zzAO0FJSG|qFMem@v~i-aw3<cNQmcCdg%}P6=Y^EN;RKl=9q)zi#^N*e#;2kELf#xL z6jpO^=SmKoWXOlM?gj_aLarAlffv8XSMW>SjeV`!)`@}kNWS{%gz3nAjCnuEfd2gM zW~D9FZ1?jK39#txZv4XX&i7V3n(`g<+d&7r>8vM=EX-W{_lYaURf9u{U4(nn`qBKB zIanr#xT473cFIbR3ZnCFJ30Y;eQ8TmNBiOjfyIOM#-gILGWZa`xHHzcF|3*G!kkTN z-!9#lW&O&{ww(1JK%=fC2R_A<)BWHmsr2)ctdp5<|FgNRz|~czh5)CcANcxU+9UG! zKXZOqnAO(Jt&?0xxdi&HBVhgq?eT&$gXlrGEgt^hUqO*quH(k^wjn}ylQ%X3QIRIS zC*n6OjWLlWS5NXmT2n$X@8r$CGtM;jTmr{cD7*Xs-aTEK&2JTi)P<yVgViQ?pK>QH zkJL+^Eh;qvRXiHxo_JoakP#UO=GFwH1`MMFT?}7BlcS29d_t;tM`c-H2xsD;;c==C zZs2AKoU8=6;&Mj)E2U(6%V1_xUA=1Y!A(|}<wuu_%m0M%DRPTFb{Z7YluS&J|2a@D zNrKW(Jh-muL~XL-eqT4@kFk(D|3TD`T5UzImJ}L7ONU`m`vCXvz7j2iGl#wJt`a(@ zTUzqSzK?8m*PWeP=P#s>X|k1Hb}=%%8hG5)a*eUF&(7FEA^$-@&gsA(V=r^?!r=x{ z(J9AERE3<0nWaUHEZ5Q9#_pa5H_xDO5lv_w)cQj`<W|{&Fo}_Vi>E<2`n5Ux*grv^ zWlkfdOpQfMF5hg~J}`s>@XIETrn)$^eQH<2mNa~3@@PVI^+nPZw{QWiQs`$Q8*7>{ zPL~u~m>_qg!3Q`vwn}z5)sptB;3T~Q550?o>kWI6o0MF4Sn%0T7@yZXhXh9|Evl1j zvbg$CV1>LK+9_r>zNfwZJb^;HDz%Nd#({<uCS$F{UO>5OLa=2teYNp$4O(_n>Vefl zK_LKm&fWaR1?l*`z4JpbGT^+lLemGDX0V>pxUJRu4W@U>c@vLp2s};J|Gj1KB5~r) zA7_26^|3hzMUOg~+ewnMTC(|*MNY%o%=%*xzqW3^-P4=UQR7Jap6;`(lJ6^r>bp7V z)<R9{z@Hl@q~ZQDY4`6swk4hM#!+|y$VnDY0;v3zv-a3qPE9cko@sWn#U1-`8S(rC z&jUw^XL(%DL+5OM>n7D%io8674ukL;MaHvS>(BY&(l0p23se-YHn?8R907eAft90W z=HP~6GH*Se;xJIzc6*^)BBG7%iOj;em5MVyZP#-i4>cGGltLu~u`ndR%%#gzT)j1g zd_9>5-Npr<<P?$m`YRPD#v6i|&|Mns<Izq-M*rvj8gWrcs#)wl@cHOSKt|!86Qxib zv-nXh-tgNoL48Ft*b^EgF-^c+`n2K*3>JvMwpPn_y;raK^M?lmVEY69nDR;o<PNHQ z=FG%_;WI%zJ`!Dxu?J_g&38HVud82HL>mbu-?0`0_+_0BY!Ih%daX{JuT+)Yga{P} zg>q;b9#qS0P4y|8k=#HJI?o0rhaJ(yDn*D6XwLjw^G!#hK+8#_;3a?PuWYb#LphzX zK;R&Nk6o9;BM2hg>*>$uyt$y6N|{wB+1@)pumkUOq)$Zu;LHi?#Ki6WIeZit@y|c~ zyrVYaR>q>F*O!&s&jt#Ir|=*3zw85g>7;W($^Yb)-w{hAgA#lGMsrjW6X(fsO?qVJ zp?$#N>70<w0(Jtr+ax~k%O52O3_^{j<Wt)dB{ML0rWNvnp1;HOue~YQ<+S5eX|qy4 zy@p|W7Rn74`x&~*c}-E(+RML8alB?}py|xzzv6KJevWp!mSF97^J?`4itKA5sIyin ze~4{fe@SAhaS@xM@2wUvV>4yh18a&;P7qQ!{pXH*8O!C9ALjmx>TDRK=UTPc!!H$5 zTP^}EoVFbPkPrS_^Cc)4|C!*g9F~y7_I<W9VtqK{*LFQK{RT5$L?_!$Hcd+#G<{b; zE~Yxyf9Z$~i=X|v`Bp?xHL@jNO@K2HwS#&%ZVTqEd^OozXo{YTm}6z&<|NOP7QS8+ z$FC2Jn&B$R1VkP)0i=7fuAYz2LiX}+Mll8@b<p3JlEzb^jlE3=ZPtc1tjtAl+Q{Mn z=k(?;Uo?E0NC{MdXJxbhpdTCqj*bFK&gDEa;_fyAOpwy+ePC;k*m>yf^P@=Rte=<; zIjsCDUz9xOXq1_y$=o<yXh-zklIGVslnM*t(f_lJY8uz4^!%(Yl614-K7L3|kSnyA z1w(=neqL8H@P?AP4+&LLn;0VoHM%63XPa<1>Cf!MNm?}9hohTC-LBaOK47Gtb4G`D z8G|kd{v;UN*?+!ut#DX=bEG}udsP^*b($5pFv_&APEwNk@-ItHy2wthzld=eZ9#(u z3kBeHb7wAze@N8<AUws!!Rv3WpI`o7C6{Z3qBK@lbI~8icT5{axXLFYAkUeYN1pal zu#NG3U<0n>8T=;b<~XPWlezamdi719fSy@Wy5VNv2Jtc5dI(<sD;qZ5`u6+ZK^M74 z;mHrhFc%}#t~@7<1SBb2SL2=F2xW`)TlNz;OTP~bZ%QjU3eS_}qFMLu56tu%Wt_Ax z+ipm?Q2(0u)_2Z`N_CC{zGAvwuv{eKSdCtWA^?kt!#C`G{+$gu{IH-A9Kc;(^HXEh zdU7vn{Vdot!#p%<?V5vK-ic2Ij<+sijBfp!MBniuZnk@fjPjuGsM&8nw|Ucd*M`3L zBO9BN=IadOM>C(J#j(B0H}(N~MtD@=LTRP6iQLz{S!_UUb&GS7Aj$!fp>FOeXyhBC z?On$ho2OW>I1`k==$y54wDhz{wXP04XfAQxfqXwo6%T8&1CQTtY-C`hC;J)1GHi0D Sse&CiQ+RTS)_Y=q`u_k!0j`Vy diff --git a/libraries/cherrypy/test/static/index.html b/libraries/cherrypy/test/static/index.html deleted file mode 100644 index a5c19667..00000000 --- a/libraries/cherrypy/test/static/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello, world diff --git a/libraries/cherrypy/test/style.css b/libraries/cherrypy/test/style.css deleted file mode 100644 index b266e93d..00000000 --- a/libraries/cherrypy/test/style.css +++ /dev/null @@ -1 +0,0 @@ -Dummy stylesheet diff --git a/libraries/cherrypy/test/test.pem b/libraries/cherrypy/test/test.pem deleted file mode 100644 index 47a47042..00000000 --- a/libraries/cherrypy/test/test.pem +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ -R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn -da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB -AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj -9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT -enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 -8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 -tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i -0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR -MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB -yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb -8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 -yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD -VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv -MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW -MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy -cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG -A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn -bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx -FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl -cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A -ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M -C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg -KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ -2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ -/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p -YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 -MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G -CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S -8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 -D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T -NluCaWQys3MS ------END CERTIFICATE----- diff --git a/libraries/cherrypy/test/test_auth_basic.py b/libraries/cherrypy/test/test_auth_basic.py deleted file mode 100644 index d7e69a9b..00000000 --- a/libraries/cherrypy/test/test_auth_basic.py +++ /dev/null @@ -1,135 +0,0 @@ -# This file is part of CherryPy <http://www.cherrypy.org/> -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - -from hashlib import md5 - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.lib import auth_basic -from cherrypy.test import helper - - -class BasicAuthTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'This is public.' - - class BasicProtected: - - @cherrypy.expose - def index(self): - 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) - - class BasicProtected2_u: - - @cherrypy.expose - def index(self): - 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()} - - def checkpasshash(realm, user, password): - p = userhashdict.get(user) - return p and p == md5(ntob(password)).hexdigest() or False - - def checkpasshash_u(realm, user, password): - p = userhashdict_u.get(user) - 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 - }, - '/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', - }, - } - - root = Root() - root.basic = BasicProtected() - root.basic2 = BasicProtected2() - root.basic2_u = BasicProtected2_u() - 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.') - - def testBasic(self): - self.getPage('/basic/') - self.assertStatus(401) - self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' - ) - - self.getPage('/basic/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - 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.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') - - self.getPage('/basic2/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - 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.assertStatus(401) - self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' - ) - - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) - self.assertStatus(401) - - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) - self.assertStatus('200 OK') - self.assertBody("Hello xюзер, you've been authorized.") diff --git a/libraries/cherrypy/test/test_auth_digest.py b/libraries/cherrypy/test/test_auth_digest.py deleted file mode 100644 index 512e39a5..00000000 --- a/libraries/cherrypy/test/test_auth_digest.py +++ /dev/null @@ -1,134 +0,0 @@ -# This file is part of CherryPy <http://www.cherrypy.org/> -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - -import six - - -import cherrypy -from cherrypy.lib import auth_digest -from cherrypy._cpcompat import ntob - -from cherrypy.test import helper - - -def _fetch_users(): - 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.' - - 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'}} - - 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.' - - def _test_parametric_digest(self, username, realm): - 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, - )) - assert len(www_auth_digest) == 1, msg - - items = www_auth_digest[0][-1][7:].split(', ') - tokens = {} - for item in items: - 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"' - - 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"') - - encoded_user = username - if six.PY3: - 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', - ) - 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', - ) - 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') - assert self.status_code == 401 - - def test_ascii_user(self): - 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' - assert self.body == ntob( - "Hello ☃йюзер, you've been authorized.", 'utf-8', - ) - - def test_wrong_scheme(self): - basic_auth = { - 'Authorization': 'Basic foo:bar', - } - self.getPage('/digest/', headers=list(basic_auth.items())) - assert self.status_code == 401 diff --git a/libraries/cherrypy/test/test_bus.py b/libraries/cherrypy/test/test_bus.py deleted file mode 100644 index 6026b47e..00000000 --- a/libraries/cherrypy/test/test_bus.py +++ /dev/null @@ -1,274 +0,0 @@ -import threading -import time -import unittest - -from cherrypy.process import wspbus - - -msg = 'Listener %d on channel %s: %s.' - - -class PublishSubscribeTests(unittest.TestCase): - - def get_listener(self, channel, index): - def listener(arg=None): - self.responses.append(msg % (index, channel, arg)) - return listener - - def test_builtin_channels(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - - for channel in b.listeners: - for index, priority in enumerate([100, 50, 0, 51]): - b.subscribe(channel, - self.get_listener(channel, index), priority) - - for channel in b.listeners: - b.publish(channel) - expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) - b.publish(channel, arg=79347) - expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) - - self.assertEqual(self.responses, expected) - - def test_custom_channels(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - - custom_listeners = ('hugh', 'louis', 'dewey') - for channel in custom_listeners: - for index, priority in enumerate([None, 10, 60, 40]): - b.subscribe(channel, - self.get_listener(channel, index), priority) - - for channel in custom_listeners: - b.publish(channel, 'ah so') - expected.extend([msg % (i, channel, 'ah so') - for i in (1, 3, 0, 2)]) - b.publish(channel) - expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) - - self.assertEqual(self.responses, expected) - - def test_listener_errors(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - channels = [c for c in b.listeners if c != 'log'] - - for channel in channels: - b.subscribe(channel, self.get_listener(channel, 1)) - # This will break since the lambda takes no args. - b.subscribe(channel, lambda: None, priority=20) - - for channel in channels: - self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) - expected.append(msg % (1, channel, 123)) - - self.assertEqual(self.responses, expected) - - -class BusMethodTests(unittest.TestCase): - - def log(self, bus): - self._log_entries = [] - - def logit(msg, level): - self._log_entries.append(msg) - bus.subscribe('log', logit) - - def assertLog(self, entries): - self.assertEqual(self._log_entries, entries) - - def get_listener(self, channel, index): - def listener(arg=None): - self.responses.append(msg % (index, channel, arg)) - return listener - - def test_start(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('start', self.get_listener('start', index)) - - b.start() - try: - # The start method MUST call all 'start' listeners. - self.assertEqual( - set(self.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) - self.assertEqual(b.state, b.states.STARTED) - # The start method MUST log its states. - self.assertLog(['Bus STARTING', 'Bus STARTED']) - finally: - # Exit so the atexit handler doesn't complain. - b.exit() - - def test_stop(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('stop', self.get_listener('stop', index)) - - b.stop() - - # The stop method MUST call all 'stop' listeners. - self.assertEqual(set(self.responses), - set([msg % (i, 'stop', None) for i in range(num)])) - # The stop method MUST move the state to STOPPED - self.assertEqual(b.state, b.states.STOPPED) - # The stop method MUST log its states. - self.assertLog(['Bus STOPPING', 'Bus STOPPED']) - - def test_graceful(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('graceful', self.get_listener('graceful', index)) - - b.graceful() - - # The graceful method MUST call all 'graceful' listeners. - self.assertEqual( - set(self.responses), - set([msg % (i, 'graceful', None) for i in range(num)])) - # The graceful method MUST log its states. - self.assertLog(['Bus graceful']) - - def test_exit(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('stop', self.get_listener('stop', index)) - b.subscribe('exit', self.get_listener('exit', index)) - - b.exit() - - # The exit method MUST call all 'stop' listeners, - # and then all 'exit' listeners. - self.assertEqual(set(self.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 - self.assertEqual(b.state, b.states.EXITING) - # The exit method MUST log its states. - self.assertLog( - ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) - - def test_wait(self): - b = wspbus.Bus() - - def f(method): - time.sleep(0.2) - getattr(b, method)() - - for method, states in [('start', [b.states.STARTED]), - ('stop', [b.states.STOPPED]), - ('start', - [b.states.STARTING, b.states.STARTED]), - ('exit', [b.states.EXITING]), - ]: - threading.Thread(target=f, args=(method,)).start() - b.wait(states) - - # The wait method MUST wait for the given state(s). - if b.state not in states: - self.fail('State %r not in %r' % (b.state, states)) - - def test_block(self): - b = wspbus.Bus() - self.log(b) - - def f(): - time.sleep(0.2) - b.exit() - - def g(): - time.sleep(0.4) - threading.Thread(target=f).start() - threading.Thread(target=g).start() - threads = [t for t in threading.enumerate() if not t.daemon] - self.assertEqual(len(threads), 3) - - b.block() - - # The block method MUST wait for the EXITING state. - self.assertEqual(b.state, b.states.EXITING) - # The block method MUST wait for ALL non-main, non-daemon threads to - # finish. - threads = [t for t in threading.enumerate() if not t.daemon] - self.assertEqual(len(threads), 1) - # The last message will mention an indeterminable thread name; ignore - # it - self.assertEqual(self._log_entries[:-1], - ['Bus STOPPING', 'Bus STOPPED', - 'Bus EXITING', 'Bus EXITED', - 'Waiting for child threads to terminate...']) - - def test_start_with_callback(self): - b = wspbus.Bus() - self.log(b) - try: - events = [] - - def f(*args, **kwargs): - events.append(('f', args, kwargs)) - - def g(): - events.append('g') - b.subscribe('start', g) - b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) - # Give wait() time to run f() - time.sleep(0.2) - - # The callback method MUST wait for the STARTED state. - self.assertEqual(b.state, b.states.STARTED) - # The callback method MUST run after all start methods. - self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})]) - finally: - b.exit() - - def test_log(self): - b = wspbus.Bus() - self.log(b) - self.assertLog([]) - - # Try a normal message. - expected = [] - for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: - b.log(msg) - expected.append(msg) - self.assertLog(expected) - - # Try an error message - try: - foo - except NameError: - b.log('You are lost and gone forever', traceback=True) - lastmsg = self._log_entries[-1] - if 'Traceback' not in lastmsg or 'NameError' not in lastmsg: - self.fail('Last log message %r did not contain ' - 'the expected traceback.' % lastmsg) - else: - self.fail('NameError was not raised as expected.') - - -if __name__ == '__main__': - unittest.main() diff --git a/libraries/cherrypy/test/test_caching.py b/libraries/cherrypy/test/test_caching.py deleted file mode 100644 index 1a6ed4f2..00000000 --- a/libraries/cherrypy/test/test_caching.py +++ /dev/null @@ -1,392 +0,0 @@ -import datetime -from itertools import count -import os -import threading -import time - -from six.moves import range -from six.moves import urllib - -import pytest - -import cherrypy -from cherrypy.lib import httputil - -from cherrypy.test import helper - - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -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;' -) - - -class CacheTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - - @cherrypy.config(**{'tools.caching.on': True}) - class Root: - - def __init__(self): - self.counter = 0 - self.control_counter = 0 - self.longlock = threading.Lock() - - @cherrypy.expose - def index(self): - self.counter += 1 - msg = 'visit #%s' % self.counter - return msg - - @cherrypy.expose - def control(self): - self.control_counter += 1 - return 'visit #%s' % self.control_counter - - @cherrypy.expose - def a_gif(self): - cherrypy.response.headers[ - 'Last-Modified'] = httputil.HTTPDate() - return gif_bytes - - @cherrypy.expose - def long_process(self, seconds='1'): - try: - self.longlock.acquire() - time.sleep(float(seconds)) - finally: - self.longlock.release() - 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') - ], - }) - 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, - }) - class UnCached(object): - - @cherrypy.expose - @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.expose - def dynamic(self): - 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' - return "Hi, I'm cacheable." - - @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': 86400}) - def specific(self): - 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()}) - 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 - }) - 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}) - - def testCaching(self): - elapsed = 0.0 - for trial in range(10): - self.getPage('/') - # The response should be the same every time, - # except for the Age response header. - self.assertBody('visit #1') - if trial != 0: - age = int(self.assertHeader('Age')) - self.assert_(age >= elapsed) - elapsed = age - - # POST, PUT, DELETE should not be cached. - 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') - # The previous request should have invalidated the cache, - # so this request will recalc the response. - 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') - - # 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') - - # 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') - - # 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') - - def testVaryHeader(self): - 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/') - 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') - - # test exceptions for bad time values - self.getPage('/expires/wrongtype') - self.assertStatus(500) - 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') - - # 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!') - # the Cache-Control header should be untouched - 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') - # 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') - - # 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') - - # 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!') - # 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') - - 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')] - content_len = None - - for _ in range(3): - 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') - - 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') - # non-gzipped version should have a different content length - 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') - - 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.assertStatus(200) - self.assertBody(gif_bytes) - lm1 = self.assertHeader('Last-Modified') - - # this request should get the cached copy. - self.getPage('/a.gif') - self.assertStatus(200) - self.assertBody(gif_bytes) - 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.assertStatus(304) - self.assertNoHeader('Last-Modified') - if not getattr(cherrypy.server, 'using_apache', False): - self.assertHeader('Age') - - @pytest.mark.xfail(reason='#1536') - def test_antistampede(self): - SECONDS = 4 - 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.assertStatus(200) - - start = datetime.datetime.now() - - def run(): - self.getPage(slow_url) - # The response should be the same every time - self.assertBody('success!') - ts = [threading.Thread(target=run) for i in range(100)] - for t in ts: - t.start() - for t in ts: - t.join() - finish = datetime.datetime.now() - # Allow for overhead, two seconds for slow hosts - allowance = SECONDS + 2 - 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', 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') - - time.sleep(1) - self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) - self.assertBody('visit #4') - self.getPage('/control') - self.assertBody('visit #4') diff --git a/libraries/cherrypy/test/test_compat.py b/libraries/cherrypy/test/test_compat.py deleted file mode 100644 index 44a9fa31..00000000 --- a/libraries/cherrypy/test/test_compat.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Test Python 2/3 compatibility module.""" -from __future__ import unicode_literals - -import unittest - -import pytest -import six - -from cherrypy import _cpcompat as compat - - -class StringTester(unittest.TestCase): - """Tests for string conversion.""" - - @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2') - def test_ntob_non_native(self): - """ntob should raise an Exception on unicode. - - (Python 2 only) - - See #1132 for discussion. - """ - self.assertRaises(TypeError, compat.ntob, 'fight') - - -class EscapeTester(unittest.TestCase): - """Class to test escape_html function from _cpcompat.""" - - def test_escape_quote(self): - """test_escape_quote - Verify the output for &<>"' chars.""" - self.assertEqual( - """xx&<>"aa'""", - compat.escape_html("""xx&<>"aa'"""), - ) diff --git a/libraries/cherrypy/test/test_config.py b/libraries/cherrypy/test/test_config.py deleted file mode 100644 index be17df90..00000000 --- a/libraries/cherrypy/test/test_config.py +++ /dev/null @@ -1,303 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import io -import os -import sys -import unittest - -import six - -import cherrypy - -from cherrypy.test import helper - - -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -def StringIOFromNative(x): - return io.StringIO(six.text_type(x)) - - -def setup_server(): - - @cherrypy.config(foo='this', bar='that') - class Root: - - def __init__(self): - cherrypy.config.namespaces['db'] = self.db_namespace - - def db_namespace(self, k, v): - if k == 'scheme': - self.db = v - - @cherrypy.expose(alias=('global_', 'xyz')) - def index(self, key): - return cherrypy.request.config.get(key, 'None') - - @cherrypy.expose - def repr(self, key): - return repr(cherrypy.request.config.get(key, None)) - - @cherrypy.expose - def dbscheme(self): - return self.db - - @cherrypy.expose - @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')) - - @cherrypy.config(foo='this2', baz='that2') - class Foo: - - @cherrypy.expose - def index(self, key): - return cherrypy.request.config.get(key, 'None') - nex = index - - @cherrypy.expose - @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) - def silly(self): - return 'Hello world' - - # Test the expose and config decorators - @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')) - - def raw_namespace(key, value): - if key == 'input.map': - handler = cherrypy.request.handler - - def wrapper(): - params = cherrypy.request.params - for name, coercer in list(value.items()): - try: - params[name] = coercer(params[name]) - except KeyError: - pass - return handler() - cherrypy.request.handler = wrapper - 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}) - class Raw: - - @cherrypy.expose - @cherrypy.config(**{'raw.input.map': {'num': int}}) - def incr(self, num): - return num + 1 - - if not six.PY3: - thing3 = "thing3: unicode('test', errors='ignore')" - else: - thing3 = '' - - ioconf = StringIOFromNative(""" -[/] -neg: -1234 -filename: os.path.join(sys.prefix, "hello.py") -thing1: cherrypy.lib.httputil.response_codes[404] -thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 -%s -complex: 3+2j -mul: 6*3 -ones: "11" -twos: "22" -stradd: %%(ones)s + %%(twos)s + "33" - -[/favicon.ico] -tools.staticfile.filename = %r -""" % (thing3, 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 - - cherrypy.tree.mount(Another(), '/another') - cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', - 'db.scheme': r'sqlite///memory', - }) - - -# Client-side code # - - -class ConfigTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - 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'), - # If 'foo' == 'this', then the mount point '/another' leaks into - # '/'. - ('/another/', 'foo', 'None'), - ] - for path, key, expected in tests: - 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, - # From global config - 'luxuryyacht': 'throatwobblermangrove', - # From Root._cp_config - 'bar': 'that', - # From Foo._cp_config - 'baz': 'that2', - # From Foo.bar._cp_config - 'foo': 'this3', - 'bax': 'this4', - } - for key, expected in expectedconf.items(): - 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=filename') - self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) - - self.getPage('/repr?key=thing1') - self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) - - 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') - from cherrypy.tutorial import thing2 - self.assertBody(repr(thing2)) - - if not six.PY3: - self.getPage('/repr?key=thing3') - self.assertBody(repr(six.text_type('test'))) - - self.getPage('/repr?key=complex') - self.assertBody('(3+2j)') - - self.getPage('/repr?key=mul') - self.assertBody('18') - - 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') - - def testCustomNamespaces(self): - self.getPage('/raw/incr?num=12') - self.assertBody('13') - - 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') - self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'), - 'rb').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') - - -class VariableSubstitutionTests(unittest.TestCase): - setup_server = staticmethod(setup_server) - - def test_config(self): - from textwrap import dedent - - # variable substitution with [DEFAULT] - conf = dedent(""" - [DEFAULT] - dir = "/some/dir" - my.dir = %(dir)s + "/sub" - - [my] - my.dir = %(dir)s + "/my/dir" - my.dir2 = %(my.dir)s + '/dir2' - - """) - - 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') - - -class CallablesInConfigTest(unittest.TestCase): - setup_server = staticmethod(setup_server) - - 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'}) - - 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' - } - 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) - del cherrypy._test_dict diff --git a/libraries/cherrypy/test/test_config_server.py b/libraries/cherrypy/test/test_config_server.py deleted file mode 100644 index 7b183530..00000000 --- a/libraries/cherrypy/test/test_config_server.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import os - -import cherrypy -from cherrypy.test import helper - - -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -# Client-side code # - - -class ServerConfigTests(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class Root: - - @cherrypy.expose - def index(self): - return cherrypy.request.wsgi_environ['SERVER_PORT'] - - @cherrypy.expose - def upload(self, file): - return 'Size: %s' % len(file.file.read()) - - @cherrypy.expose - @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 <servername> - # Also test default server.instance = builtin server - 'server.yetanother.socket_port': 9878, - }) - - PORT = 9876 - - def testBasicConfig(self): - self.getPage('/') - self.assertBody(str(self.PORT)) - - def testAdditionalServers(self): - if self.scheme == 'https': - return self.skip('not available under ssl') - self.PORT = 9877 - self.getPage('/') - self.assertBody(str(self.PORT)) - self.PORT = 9878 - 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) - self.assertStatus(200) - self.assertBody('x' * 100) - - 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... ') - - for size in (500, 5000, 50000): - 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)]) - - # Test upload - 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) - self.assertStatus(413) diff --git a/libraries/cherrypy/test/test_conn.py b/libraries/cherrypy/test/test_conn.py deleted file mode 100644 index 7d60c6fb..00000000 --- a/libraries/cherrypy/test/test_conn.py +++ /dev/null @@ -1,873 +0,0 @@ -"""Tests for TCP connection handling, including proper and timely close.""" - -import errno -import socket -import sys -import time - -import six -from six.moves import urllib -from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected - -import pytest - -from cheroot.test import webtest - -import cherrypy -from cherrypy._cpcompat import HTTPSConnection, ntob, tonative -from cherrypy.test import helper - - -timeout = 1 -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!' - - @cherrypy.expose - def timeout(self, t): - return str(cherrypy.server.httpserver.timeout) - - @cherrypy.expose - @cherrypy.config(**{'response.stream': True}) - def stream(self, set_cl=False): - if set_cl: - cherrypy.response.headers['Content-Length'] = 10 - - def content(): - for x in range(10): - yield str(x) - - return content() - - @cherrypy.expose - def error(self, code=500): - raise cherrypy.HTTPError(code) - - @cherrypy.expose - def upload(self): - 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 - - @cherrypy.expose - @cherrypy.config(**{'hooks.on_start_resource': raise500}) - def err_before_read(self): - return 'ok' - - @cherrypy.expose - def one_megabyte_of_a(self): - 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}) - def custom_cl(self, body, cl): - cherrypy.response.headers['Content-Length'] = cl - if not isinstance(body, list): - body = [body] - newbody = [] - for chunk in body: - if isinstance(chunk, six.text_type): - 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, - }) - - -class ConnectionCloseTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_HTTP11(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - 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.assertBody(pov) - self.assertNoHeader('Connection') - - # Make another request on the same connection. - self.getPage('/page1') - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader('Connection') - - # Test client-side close. - self.getPage('/page2', headers=[('Connection', 'close')]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader('Connection', 'close') - - # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, '/') - - def test_Streaming_no_len(self): - try: - self._streaming(set_cl=False) - finally: - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - def test_Streaming_with_len(self): - try: - self._streaming(set_cl=True) - finally: - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - def _streaming(self, set_cl): - 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.assertBody(pov) - 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.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') - - chunked_response = False - for k, v in self.headers: - if k.lower() == 'transfer-encoding': - if str(v) == 'chunked': - chunked_response = True - - if chunked_response: - self.assertNoHeader('Connection', 'close') - else: - self.assertHeader('Connection', 'close') - - # Make another request on the same connection, which should - # error. - 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') - else: - 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.assertBody(pov) - 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') - 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.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, '/') - - def test_HTTP10_KeepAlive(self): - 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.assertBody(pov) - # Apache, for example, may emit a Connection header even for HTTP/1.0 - # self.assertNoHeader("Connection") - - # Test a keep-alive HTTP/1.0 request. - self.persistent = True - - self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader('Connection', 'Keep-Alive') - - # Remove the keep-alive header again. - 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") - - -class PipelineTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - 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': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Connect but send nothing. - self.persistent = True - conn = self.HTTP_CONN - conn.auto_open = False - conn.connect() - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The request should have returned 408 already. - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 408) - conn.close() - - # Connect but send half the headers only. - self.persistent = True - 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')) - - # 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.begin() - self.assertEqual(response.status, 408) - conn.close() - - 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': - return self.skip() - - 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.endheaders() - 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._send_output() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - 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._send_output() - response = conn.response_class(conn.sock, method='GET') - try: - response.begin() - except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail("Writing to timed out socket didn't fail" - ' as it should have: %s' % sys.exc_info()[1]) - else: - if response.status != 408: - self.fail("Writing to timed out socket didn't fail" - ' as it should have: %s' % - response.read()) - - conn.close() - - # 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.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - - # Make another request on the same socket, - # but timeout on the headers - 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') - try: - response.begin() - except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail("Writing to timed out socket didn't fail" - ' as it should have: %s' % sys.exc_info()[1]) - else: - self.fail("Writing to timed out socket didn't fail" - ' as it should have: %s' % - response.read()) - - conn.close() - - # 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.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - conn.close() - - def test_HTTP11_pipelining(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - 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.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._send_output() - - # Retrieve previous response - 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 - if six.PY3: - response.fp = conn.sock.makefile('rb', 0) - response.begin() - body = response.read(13) - self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello, world!') - - # Retrieve final response - response = conn.response_class(conn.sock, method='GET') - response.begin() - body = response.read() - self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello, world!') - - conn.close() - - def test_100_Continue(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - self.persistent = True - conn = self.HTTP_CONN - - # Try a page without an Expect request header first. - # 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.endheaders() - conn.send(ntob("d'oh")) - response = conn.response_class(conn.sock, method='POST') - version, status, reason = response._read_status() - self.assertNotEqual(status, 100) - finally: - conn.close() - - # 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.endheaders() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - line = response.fp.readline().strip() - if line: - self.fail( - '100 Continue should not output any headers. Got %r' % - line) - else: - break - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - finally: - conn.close() - - -class ConnectionTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_readall_or_close(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - if self.scheme == 'https': - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - # Test a max of 0 (the default) and then reset to what it was above. - old_max = cherrypy.server.max_request_body_size - for new_max in (0, old_max): - cherrypy.server.max_request_body_size = new_max - - self.persistent = True - 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.endheaders() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - conn.send(ntob('x' * 1000)) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - 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._send_output() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - conn.close() - - def test_No_Message_Body(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - 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.assertBody(pov) - self.assertNoHeader('Connection') - - # Make a 204 request on the same connection. - self.getPage('/custom/204') - self.assertStatus(204) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') - - # Make a 304 request on the same connection. - self.getPage('/custom/304') - self.assertStatus(304) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') - - def test_Chunked_Encoding(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - 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' - - # 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') - # 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.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') - - # 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') - # Chunked requests don't need a content-length - # # conn.putheader("Content-Length", len(body)) - conn.endheaders() - conn.send(body) - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(413) - conn.close() - - def test_Content_Length_in(self): - # Try a non-chunked request where Content-Length exceeds - # 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.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.') - conn.close() - - def test_Content_Length_out_preheaders(self): - # Try a non-chunked response where Content-Length is less than - # 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.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.') - conn.close() - - def test_Content_Length_out_postheaders(self): - # Try a non-chunked response where Content-Length is less than - # the actual bytes in the response body. - 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) - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody('I too') - conn.close() - - def test_598(self): - tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' - url = tmpl.format( - scheme=self.scheme, - host=self.HOST, - port=self.PORT, - ) - remote_data_conn = urllib.request.urlopen(url) - buf = remote_data_conn.read(512) - time.sleep(timeout * 0.6) - remaining = (1024 * 1024) - 512 - while remaining: - data = remote_data_conn.read(remaining) - if not data: - break - else: - buf += data - remaining -= len(data) - - self.assertEqual(len(buf), 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) - 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, - }) - - -reset_names = 'ECONNRESET', 'WSAECONNRESET' -socket_reset_errors = [ - getattr(errno, name) - for name in reset_names - if hasattr(errno, name) -] -'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', -] - - -class LimitedRequestQueueTests(helper.CPWebCase): - setup_server = staticmethod(setup_upload_server) - - @pytest.mark.xfail(reason='#1535') - def test_queue_full(self): - conns = [] - overflow_conn = None - - try: - # Make 15 initial requests and leave them open, which should use - # 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.endheaders() - conns.append(conn) - - # Now try a 16th conn, which should be closed by the - # 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): - 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.endheaders() - response = overflow_conn.response_class( - overflow_conn.sock, - method='GET', - ) - try: - response.begin() - except socket.error as exc: - if exc.args[0] in socket_reset_errors: - pass # Expected. - else: - 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' - else: - 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') - response.begin() - self.body = response.read() - self.assertBody("thanks for 'done'") - self.assertEqual(response.status, 200) - conn.close() - if overflow_conn: - overflow_conn.close() - - -class BadRequestTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - 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') - response.begin() - self.body = response.read() - 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') - response.begin() - self.body = response.read() - self.assertBody('HTTP requires CRLF terminators') - conn.close() diff --git a/libraries/cherrypy/test/test_core.py b/libraries/cherrypy/test/test_core.py deleted file mode 100644 index 9834c1f3..00000000 --- a/libraries/cherrypy/test/test_core.py +++ /dev/null @@ -1,823 +0,0 @@ -# coding: utf-8 - -"""Basic tests for the CherryPy core: request handling.""" - -import os -import sys -import types - -import six - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy import _cptools, tools -from cherrypy.lib import httputil, static - -from cherrypy.test._test_decorators import ExposeExamples -from cherrypy.test import helper - - -localDir = os.path.dirname(__file__) -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' - - 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)]}) - - @cherrypy.expose - def baseurl(self, path_info, relative=None): - return cherrypy.url(path_info, relative=bool(relative)) - - root = Root() - root.expose_dec = ExposeExamples() - - class TestType(type): - - """Metaclass which automatically exposes all functions in each - 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 six.itervalues(dct): - 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): - - def index(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - def leaf(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - def qs(self, qs): - return cherrypy.url(qs=qs) - - def log_status(): - Status.statuses.append(cherrypy.response.status) - cherrypy.tools.log_status = cherrypy.Tool( - 'on_end_resource', log_status) - - class Status(Test): - - def index(self): - return 'normal' - - def blank(self): - cherrypy.response.status = '' - - # According to RFC 2616, new status codes are OK as long as they - # are between 100 and 599. - - # Here is an illegal code... - def illegal(self): - cherrypy.response.status = 781 - return 'oops' - - # ...and here is an unknown but legal code. - def unknown(self): - cherrypy.response.status = '431 My custom error' - return 'funky' - - # Non-numeric code - def bad(self): - cherrypy.response.status = 'error' - return 'bad news' - - statuses = [] - - @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, - }) - class Error: - @cherrypy.expose - def index(self): - raise NameError('redirect_test') - - error = Error() - - def index(self): - return 'child' - - def custom(self, url, code): - raise cherrypy.HTTPRedirect(url, code) - - @cherrypy.config(**{'tools.trailing_slash.extra': True}) - def by_code(self, code): - raise cherrypy.HTTPRedirect('somewhere%20else', code) - - def nomodify(self): - raise cherrypy.HTTPRedirect('', 304) - - def proxy(self): - raise cherrypy.HTTPRedirect('proxy', 305) - - def stringify(self): - return str(cherrypy.HTTPRedirect('/')) - - def fragment(self, 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( - "/some<script>alert(1);</script>url/that'we/want") - - def url_with_unicode(self): - 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) - - def redir_custom(): - raise cherrypy.InternalRedirect('/internalredirect/custom_err') - - class InternalRedirect(Test): - - def index(self): - raise cherrypy.InternalRedirect('/') - - @cherrypy.expose - @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') - - def cousin(self, t): - assert cherrypy.request.prev.closed - return cherrypy.request.prev.query_string - - def petshop(self, user_id): - if user_id == 'parrot': - # Trade it for a slug when redirecting - raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=slug') - elif user_id == 'terrier': - # Trade it for a fish when redirecting - raise cherrypy.InternalRedirect( - '/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)) - - # 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: - # tools.login_redir()(secure) - - def login(self): - return 'Please log in' - - def custom_err(self): - return 'Something went horribly wrong.' - - @cherrypy.config(**{'hooks.before_request_body': redir_custom}) - 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' - - def as_list(self): - return ['con', 'tent'] - - def as_yield(self): - yield b'content' - - @cherrypy.config(**{'tools.flatten.on': True}) - def as_dblyield(self): - yield self.as_yield() - - def as_refyield(self): - for chunk in self.as_yield(): - 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')) - - class Cookies(Test): - - def single(self, name): - cookie = cherrypy.request.cookie[name] - # Python2's SimpleCookie.__setitem__ won't take unicode keys. - cherrypy.response.cookie[str(name)] = cookie.value - - def multiple(self, names): - list(map(self.single, names)) - - def append_headers(header_list, debug=False): - if debug: - cherrypy.log( - '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): - - 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) - - def commas(self): - cherrypy.response.headers[ - 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' - - cherrypy.tree.mount(root) - - def testStatus(self): - self.getPage('/status/') - self.assertBody('normal') - self.assertStatus(200) - - self.getPage('/status/blank') - self.assertBody('') - self.assertStatus(200) - - self.getPage('/status/illegal') - self.assertStatus(500) - 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') - self.assertStatus(431) - - 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'])) - - 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.assertStatus(301) - self.assertMatchesBody( - '<a href=([\'"])%s/redirect/[?]id=3\\1>' - '%s/redirect/[?]id=3</a>' % (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.assertStatus(301) - self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" % - (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.assertStatus(301) - self.assertMatchesBody( - "<a href=(['\"])%s/redirect/by_code[?]code=307\\1>" - '%s/redirect/by_code[?]code=307</a>' - % (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()) - - def testRedirect(self): - self.getPage('/redirect/') - self.assertBody('child') - self.assertStatus(200) - - self.getPage('/redirect/by_code?code=300') - self.assertMatchesBody( - r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") - self.assertStatus(300) - - self.getPage('/redirect/by_code?code=301') - self.assertMatchesBody( - r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") - self.assertStatus(301) - - self.getPage('/redirect/by_code?code=302') - self.assertMatchesBody( - r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") - self.assertStatus(302) - - self.getPage('/redirect/by_code?code=303') - self.assertMatchesBody( - r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") - self.assertStatus(303) - - self.getPage('/redirect/by_code?code=307') - self.assertMatchesBody( - r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>") - self.assertStatus(307) - - self.getPage('/redirect/nomodify') - self.assertBody('') - self.assertStatus(304) - - 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') - - # Make sure str(HTTPRedirect()) works. - 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') - 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) - self.assertMatchesBody( - r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % ( - 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') - self.assertStatus(303) - loc = self.assertHeader('Location') - assert 'Set-Cookie' in loc - self.assertNoHeader('Set-Cookie') - - def assertValidXHTML(): - from xml.etree import ElementTree - try: - ElementTree.fromstring( - '<html><body>%s</body></html>' % self.body, - ) - except ElementTree.ParseError: - self._handlewebError( - '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.assertStatus(303) - assertValidXHTML() - - # do the same with a url containing quote characters. - 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.assertStatus(303) - assert b'<script>' not in self.body - assert b'<script>' in self.body - - def test_redirect_with_unicode(self): - """ - A redirect to a URL with Unicode should return a Location - header containing that Unicode URL. - """ - # test disabled due to #1440 - return - self.getPage('/redirect/url_with_unicode') - self.assertStatus(303) - loc = self.assertHeader('Location') - assert ntou('тест', encoding='utf-8') in loc - - def test_InternalRedirect(self): - # InternalRedirect - self.getPage('/internalredirect/') - self.assertBody('hello') - self.assertStatus(200) - - # Test passthrough - self.getPage( - '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film') - self.assertBody('0 images for Sir-not-appearing-in-this-film') - self.assertStatus(200) - - # Test args - self.getPage('/internalredirect/petshop?user_id=parrot') - self.assertBody('0 images for slug') - self.assertStatus(200) - - # Test POST - 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.assertBody('Something went horribly wrong.') - self.assertStatus(200) - - self.getPage('/internalredirect/secure') - self.assertBody('Please log in') - self.assertStatus(200) - - # Relative path in InternalRedirect. - # Also tests request.prev. - self.getPage('/internalredirect/relative?a=3&b=5') - self.assertBody('a=3&b=5') - self.assertStatus(200) - - # InternalRedirect on error - self.getPage('/internalredirect/choke') - self.assertStatus(200) - 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']: - self.getPage(url) - self.assertBody('content') - - def testRanges(self): - self.getPage('/ranges/get_ranges?bytes=3-6') - self.assertBody('[(3, 7)]') - - # Test multiple ranges and a suffix-byte-range-spec, for good measure. - self.getPage('/ranges/get_ranges?bytes=2-4,-1') - self.assertBody('[(2, 5), (7, 8)]') - - # Test a suffix-byte-range longer than the content - # length. Note that in this test, the content length - # is 8 bytes. - self.getPage('/ranges/get_ranges?bytes=-100') - self.assertBody('[(0, 8)]') - - # Get a partial file. - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')]) - self.assertStatus(206) - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertHeader('Content-Range', 'bytes 2-5/14') - self.assertBody('llo,') - - # What happens with overlapping ranges (and out of order, too)? - self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')]) - self.assertStatus(206) - ct = self.assertHeader('Content-Type') - expected_type = 'multipart/byteranges; boundary=' - self.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)) - self.assertBody(expected_body) - self.assertHeader('Content-Length') - - # Test "416 Requested Range Not Satisfiable" - self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')]) - self.assertStatus(416) - # "When this status code is returned for a byte-range request, - # the response SHOULD include a Content-Range entity-header - # field specifying the current length of the selected resource" - self.assertHeader('Content-Range', 'bytes */14') - elif cherrypy.server.protocol_version == 'HTTP/1.0': - # Test Range behavior with HTTP/1.0 request - self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')]) - self.assertStatus(200) - self.assertBody('Hello, world\r\n') - - 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() - - self.getPage('/favicon.ico') - self.assertBody(data) - - def skip_if_bad_cookies(self): - """ - cookies module fails to reject invalid cookies - https://github.com/cherrypy/cherrypy/issues/1405 - """ - cookies = sys.modules.get('http.cookies') - _is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False) - if not _is_legal_key(','): - return - issue = 'http://bugs.python.org/issue26302' - tmpl = 'Broken cookies module ({issue})' - self.skip(tmpl.format(**locals())) - - def testCookies(self): - self.skip_if_bad_cookies() - - 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.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.assertStatus(400) - - def testDefaultContentType(self): - self.getPage('/') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.getPage('/defct/plain') - self.getPage('/') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - self.getPage('/defct/html') - - 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"'), - ]) - self.getPage('/multiheader/commas') - self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') - - def test_cherrypy_url(self): - # Input relative to current - 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()) - # Other host header - host = 'www.mydomain.example' - 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 - self.getPage('/url/leaf?path_info=/page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/?path_info=/page1') - self.assertBody('%s/page1' % self.base()) - - # Single dots - self.getPage('/url/leaf?path_info=./page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf?path_info=other/./page1') - self.assertBody('%s/url/other/page1' % self.base()) - self.getPage('/url/?path_info=/other/./page1') - self.assertBody('%s/other/page1' % self.base()) - self.getPage('/url/?path_info=/other/././././page1') - self.assertBody('%s/other/page1' % self.base()) - - # Double dots - self.getPage('/url/leaf?path_info=../page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/leaf?path_info=other/../page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf?path_info=/other/../page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/leaf?path_info=/other/../../../page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/leaf?path_info=/other/../../../../../page1') - self.assertBody('%s/page1' % self.base()) - - # qs param is not normalized as a path - self.getPage('/url/qs?qs=/other') - self.assertBody('%s/url/qs?/other' % self.base()) - self.getPage('/url/qs?qs=/other/../page1') - self.assertBody('%s/url/qs?/other/../page1' % self.base()) - self.getPage('/url/qs?qs=../page1') - self.assertBody('%s/url/qs?../page1' % self.base()) - self.getPage('/url/qs?qs=../../page1') - self.assertBody('%s/url/qs?../../page1' % self.base()) - - # Output relative to current path or script_name - self.getPage('/url/?path_info=page1&relative=True') - self.assertBody('page1') - self.getPage('/url/leaf?path_info=/page1&relative=True') - self.assertBody('../page1') - self.getPage('/url/leaf?path_info=page1&relative=True') - self.assertBody('page1') - self.getPage('/url/leaf?path_info=leaf/page1&relative=True') - self.assertBody('leaf/page1') - self.getPage('/url/leaf?path_info=../page1&relative=True') - self.assertBody('../page1') - self.getPage('/url/?path_info=other/../page1&relative=True') - self.assertBody('page1') - - # Output relative to / - self.getPage('/baseurl?path_info=ab&relative=True') - self.assertBody('ab') - # Output relative to / - self.getPage('/baseurl?path_info=/ab&relative=True') - self.assertBody('ab') - - # absolute-path references ("server-relative") - # Input relative to current - self.getPage('/url/leaf?path_info=page1&relative=server') - self.assertBody('/url/page1') - self.getPage('/url/?path_info=page1&relative=server') - self.assertBody('/url/page1') - # Input is 'absolute'; that is, relative to script_name - self.getPage('/url/leaf?path_info=/page1&relative=server') - self.assertBody('/page1') - self.getPage('/url/?path_info=/page1&relative=server') - self.assertBody('/page1') - - def test_expose_decorator(self): - # Test @expose - self.getPage('/expose_dec/no_call') - self.assertStatus(200) - self.assertBody('Mr E. R. Bradshaw') - - # Test @expose() - self.getPage('/expose_dec/call_empty') - self.assertStatus(200) - self.assertBody('Mrs. B.J. Smegma') - - # Test @expose("alias") - self.getPage('/expose_dec/call_alias') - self.assertStatus(200) - self.assertBody('Mr Nesbitt') - # Does the original name work? - self.getPage('/expose_dec/nesbitt') - self.assertStatus(200) - self.assertBody('Mr Nesbitt') - - # Test @expose(["alias1", "alias2"]) - self.getPage('/expose_dec/alias1') - self.assertStatus(200) - self.assertBody('Mr Ken Andrews') - self.getPage('/expose_dec/alias2') - self.assertStatus(200) - self.assertBody('Mr Ken Andrews') - # Does the original name work? - self.getPage('/expose_dec/andrews') - self.assertStatus(200) - self.assertBody('Mr Ken Andrews') - - # Test @expose(alias="alias") - self.getPage('/expose_dec/alias3') - self.assertStatus(200) - self.assertBody('Mr. and Mrs. Watson') - - -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) - - class Root: - - @cherrypy.expose - def index(self): - return 'hello' - - @cherrypy.config(**{'tools.break_header.on': True}) - def start_response_error(self): - return 'salud!' - - @cherrypy.expose - def stat(self, path): - with cherrypy.HTTPError.handle(OSError, 404): - os.stat(path) - - root = Root() - - cherrypy.tree.mount(root) - - 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.') - - 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 - ) - - -class TestBinding: - def test_bind_ephemeral_port(self): - """ - A server configured to bind to port 0 will bind to an ephemeral - port and indicate that port number on startup. - """ - cherrypy.config.reset() - bind_ephemeral_conf = { - 'server.socket_port': 0, - } - cherrypy.config.update(bind_ephemeral_conf) - cherrypy.engine.start() - assert cherrypy.server.bound_addr != cherrypy.server.bind_addr - _host, port = cherrypy.server.bound_addr - assert port > 0 - cherrypy.engine.stop() - assert cherrypy.server.bind_addr == cherrypy.server.bound_addr diff --git a/libraries/cherrypy/test/test_dynamicobjectmapping.py b/libraries/cherrypy/test/test_dynamicobjectmapping.py deleted file mode 100644 index 725a3ce0..00000000 --- a/libraries/cherrypy/test/test_dynamicobjectmapping.py +++ /dev/null @@ -1,424 +0,0 @@ -import six - -import cherrypy -from cherrypy.test import helper - -script_names = ['', '/foo', '/users/fred/blog', '/corp/blog'] - - -def setup_server(): - class SubSubRoot: - - @cherrypy.expose - def index(self): - return 'SubSubRoot index' - - @cherrypy.expose - def default(self, *args): - return 'SubSubRoot default' - - @cherrypy.expose - def handler(self): - return 'SubSubRoot handler' - - @cherrypy.expose - def dispatch(self): - return 'SubSubRoot dispatch' - - subsubnodes = { - '1': SubSubRoot(), - '2': SubSubRoot(), - } - - class SubRoot: - - @cherrypy.expose - def index(self): - return 'SubRoot index' - - @cherrypy.expose - def default(self, *args): - return 'SubRoot %s' % (args,) - - @cherrypy.expose - def handler(self): - return 'SubRoot handler' - - def _cp_dispatch(self, vpath): - return subsubnodes.get(vpath[0], None) - - subnodes = { - '1': SubRoot(), - '2': SubRoot(), - } - - class Root: - - @cherrypy.expose - def index(self): - return 'index' - - @cherrypy.expose - def default(self, *args): - return 'default %s' % (args,) - - @cherrypy.expose - def handler(self): - return 'handler' - - def _cp_dispatch(self, vpath): - return subnodes.get(vpath[0]) - - # ------------------------------------------------------------------------- - # DynamicNodeAndMethodDispatcher example. - # This example exposes a fairly naive HTTP api - class User(object): - - def __init__(self, id, name): - self.id = id - self.name = name - - def __unicode__(self): - return six.text_type(self.name) - - def __str__(self): - return str(self.name) - - user_lookup = { - 1: User(1, 'foo'), - 2: User(2, 'bar'), - } - - def make_user(name, id=None): - if not id: - id = max(*list(user_lookup.keys())) + 1 - user_lookup[id] = User(id, name) - return id - - @cherrypy.expose - class UserContainerNode(object): - - def POST(self, name): - """ - Allow the creation of a new Object - """ - return 'POST %d' % make_user(name) - - def GET(self): - return six.text_type(sorted(user_lookup.keys())) - - def dynamic_dispatch(self, vpath): - try: - id = int(vpath[0]) - except (ValueError, IndexError): - return None - return UserInstanceNode(id) - - @cherrypy.expose - class UserInstanceNode(object): - - def __init__(self, id): - self.id = id - self.user = user_lookup.get(id, None) - - # For all but PUT methods there MUST be a valid user identified - # by self.id - if not self.user and cherrypy.request.method != 'PUT': - raise cherrypy.HTTPError(404) - - def GET(self, *args, **kwargs): - """ - Return the appropriate representation of the instance. - """ - return six.text_type(self.user) - - def POST(self, name): - """ - Update the fields of the user instance. - """ - self.user.name = name - return 'POST %d' % self.user.id - - def PUT(self, name): - """ - Create a new user with the specified id, or edit it if it already - exists - """ - if self.user: - # Edit the current user - self.user.name = name - return 'PUT %d' % self.user.id - else: - # Make a new user with said attributes. - return 'PUT %d' % make_user(name, self.id) - - def DELETE(self): - """ - Delete the user specified at the id. - """ - id = self.user.id - del user_lookup[self.user.id] - del self.user - return 'DELETE %d' % id - - class ABHandler: - - class CustomDispatch: - - @cherrypy.expose - def index(self, a, b): - return 'custom' - - def _cp_dispatch(self, vpath): - """Make sure that if we don't pop anything from vpath, - processing still works. - """ - return self.CustomDispatch() - - @cherrypy.expose - def index(self, a, b=None): - body = ['a:' + str(a)] - if b is not None: - body.append(',b:' + str(b)) - return ''.join(body) - - @cherrypy.expose - 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. - """ - while vpath: - vpath.pop() - return self - - @cherrypy.expose - def index(self): - return 'IndexOnly index' - - class DecoratedPopArgs: - - """Test _cp_dispatch with @cherrypy.popargs.""" - - @cherrypy.expose - def index(self): - return 'no params' - - @cherrypy.expose - def hi(self): - return "hi was not interpreted as 'a' param" - DecoratedPopArgs = cherrypy.popargs( - 'a', 'b', handler=ABHandler())(DecoratedPopArgs) - - class NonDecoratedPopArgs: - - """Test _cp_dispatch = cherrypy.popargs()""" - - _cp_dispatch = cherrypy.popargs('a') - - @cherrypy.expose - def index(self, a): - return 'index: ' + str(a) - - class ParameterizedHandler: - - """Special handler created for each request""" - - def __init__(self, a): - self.a = a - - @cherrypy.expose - def index(self): - if 'a' in cherrypy.request.params: - raise Exception( - '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) - - Root.decorated = DecoratedPopArgs() - Root.undecorated = NonDecoratedPopArgs() - Root.index_only = IndexOnly() - Root.parameter_test = ParameterizedPopArgs() - - Root.users = UserContainerNode() - - md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch') - for url in script_names: - conf = { - '/': { - 'user': (url or '/').split('/')[-2], - }, - '/users': { - 'request.dispatch': md - }, - } - cherrypy.tree.mount(Root(), url, conf) - - -class DynamicObjectMappingTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testObjectMapping(self): - for url in script_names: - self.script_name = url - - self.getPage('/') - self.assertBody('index') - - self.getPage('/handler') - self.assertBody('handler') - - # Dynamic dispatch will succeed here for the subnodes - # so the subroot gets called - self.getPage('/1/') - self.assertBody('SubRoot index') - - self.getPage('/2/') - self.assertBody('SubRoot index') - - self.getPage('/1/handler') - self.assertBody('SubRoot handler') - - self.getPage('/2/handler') - self.assertBody('SubRoot handler') - - # Dynamic dispatch will fail here for the subnodes - # so the default gets called - self.getPage('/asdf/') - self.assertBody("default ('asdf',)") - - self.getPage('/asdf/asdf') - self.assertBody("default ('asdf', 'asdf')") - - self.getPage('/asdf/handler') - self.assertBody("default ('asdf', 'handler')") - - # Dynamic dispatch will succeed here for the subsubnodes - # so the subsubroot gets called - self.getPage('/1/1/') - self.assertBody('SubSubRoot index') - - self.getPage('/2/2/') - self.assertBody('SubSubRoot index') - - self.getPage('/1/1/handler') - self.assertBody('SubSubRoot handler') - - self.getPage('/2/2/handler') - self.assertBody('SubSubRoot handler') - - self.getPage('/2/2/dispatch') - self.assertBody('SubSubRoot dispatch') - - # The exposed dispatch will not be called as a dispatch - # method. - self.getPage('/2/2/foo/foo') - self.assertBody('SubSubRoot default') - - # Dynamic dispatch will fail here for the subsubnodes - # so the SubRoot gets called - self.getPage('/1/asdf/') - self.assertBody("SubRoot ('asdf',)") - - self.getPage('/1/asdf/asdf') - self.assertBody("SubRoot ('asdf', 'asdf')") - - self.getPage('/1/asdf/handler') - self.assertBody("SubRoot ('asdf', 'handler')") - - def testMethodDispatch(self): - # GET acts like a container - self.getPage('/users') - self.assertBody('[1, 2]') - self.assertHeader('Allow', 'GET, HEAD, POST') - - # POST to the container URI allows creation - self.getPage('/users', method='POST', body='name=baz') - self.assertBody('POST 3') - self.assertHeader('Allow', 'GET, HEAD, POST') - - # POST to a specific instanct URI results in a 404 - # as the resource does not exit. - self.getPage('/users/5', method='POST', body='name=baz') - self.assertStatus(404) - - # PUT to a specific instanct URI results in creation - self.getPage('/users/5', method='PUT', body='name=boris') - self.assertBody('PUT 5') - self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT') - - # GET acts like a container - self.getPage('/users') - self.assertBody('[1, 2, 3, 5]') - self.assertHeader('Allow', 'GET, HEAD, POST') - - test_cases = ( - (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'), - (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'), - (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'), - (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'), - ) - for id, name, updatedname, headers in test_cases: - self.getPage('/users/%d' % id) - self.assertBody(name) - self.assertHeader('Allow', headers) - - # Make sure POSTs update already existings resources - 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.assertBody('PUT %d' % id) - self.assertHeader('Allow', headers) - - # Make sure DELETES Remove already existing resources. - self.getPage('/users/%d' % id, method='DELETE') - self.assertBody('DELETE %d' % id) - self.assertHeader('Allow', headers) - - # GET acts like a container - self.getPage('/users') - self.assertBody('[]') - self.assertHeader('Allow', 'GET, HEAD, POST') - - def testVpathDispatch(self): - self.getPage('/decorated/') - self.assertBody('no params') - - self.getPage('/decorated/hi') - self.assertBody("hi was not interpreted as 'a' param") - - self.getPage('/decorated/yo/') - self.assertBody('a:yo') - - self.getPage('/decorated/yo/there/') - self.assertBody('a:yo,b:there') - - self.getPage('/decorated/yo/there/delete') - self.assertBody('deleting yo and there') - - self.getPage('/decorated/yo/there/handled_by_dispatch/') - self.assertBody('custom') - - self.getPage('/undecorated/blah/') - self.assertBody('index: blah') - - self.getPage('/index_only/a/b/c/d/e/f/g/') - self.assertBody('IndexOnly index') - - self.getPage('/parameter_test/argument2/') - self.assertBody('argument2') diff --git a/libraries/cherrypy/test/test_encoding.py b/libraries/cherrypy/test/test_encoding.py deleted file mode 100644 index ab24ab93..00000000 --- a/libraries/cherrypy/test/test_encoding.py +++ /dev/null @@ -1,426 +0,0 @@ -# coding: utf-8 - -import gzip -import io -from unittest import mock - -from six.moves.http_client import IncompleteRead -from six.moves.urllib.parse import quote as url_quote - -import cherrypy -from cherrypy._cpcompat import ntob, ntou - -from cherrypy.test import helper - - -europoundUnicode = ntou('£', encoding='utf-8') -sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8') - -sing8 = sing.encode('utf-8') -sing16 = sing.encode('utf-16') - - -class EncodingTests(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self, param): - assert param == europoundUnicode, '%r != %r' % ( - param, europoundUnicode) - yield europoundUnicode - - @cherrypy.expose - def mao_zedong(self): - return sing - - @cherrypy.expose - @cherrypy.config(**{'tools.encode.encoding': 'utf-8'}) - def utf8(self): - return sing8 - - @cherrypy.expose - def cookies_and_headers(self): - # if the headers have non-ascii characters and a cookie has - # 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.headers[ - 'Some-Header'] = 'My d\xc3\xb6g has fleas' - 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())] - ) - - @cherrypy.expose - @cherrypy.config(**{ - 'tools.encode.text_only': False, - 'tools.encode.add_charset': True, - }) - def nontext(self, *args, **kwargs): - cherrypy.response.headers[ - 'Content-Type'] = 'application/binary' - return '\x00\x01\x02\x03' - - class GZIP: - - @cherrypy.expose - def index(self): - yield 'Hello, world' - - @cherrypy.expose - # Turn encoding off so the gzip tool is the one doing the collapse. - @cherrypy.config(**{'tools.encode.on': False}) - def noshow(self): - # Test for ticket #147, where yield showed no exceptions - # (content-encoding was still gzip even though traceback - # wasn't zipped). - raise IndexError() - yield 'Here be dragons' - - @cherrypy.expose - @cherrypy.config(**{'response.stream': True}) - def noshow_stream(self): - # Test for ticket #147, where yield showed no exceptions - # (content-encoding was still gzip even though traceback - # wasn't zipped). - raise IndexError() - yield 'Here be dragons' - - class Decode: - - @cherrypy.expose - @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()]) - - @cherrypy.expose - @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()]) - - root = Root() - root.gzip = GZIP() - root.decode = Decode() - cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}}) - - def test_query_string_decoding(self): - URI_TMPL = '/reqparams?q={q}' - - europoundUtf8_2_bytes = europoundUnicode.encode('utf-8') - europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2] - - # Encoded utf8 query strings MUST be parsed correctly. - # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX - self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes))) - # The return value will be encoded as utf8. - self.assertBody(b'q: ' + europoundUtf8_2_bytes) - - # Query strings that are incorrectly encoded MUST raise 404. - # Here, q is the second byte of POUND SIGN U+A3 encoded in utf8 - # and then %HEX - # TODO: check whether this shouldn't raise 400 Bad Request instead - self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte))) - self.assertStatus(404) - self.assertErrorPage( - 404, - 'The given query string could not be processed. Query ' - "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.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.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.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.assertStatus(400) - self.assertErrorPage( - 400, - 'The request entity could not be decoded. The following charsets ' - "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.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.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.assertErrorPage( - 400, - 'The request entity could not be decoded. The following charsets ' - "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), - self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc') - - @mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1) - def test_multipart_decoding_bigger_maxrambytes(self): - """ - Decoding of a multipart entity should also pass when - the entity is bigger than maxrambytes. See ticket #1352. - """ - self.test_multipart_decoding() - - 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), - 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), - self.assertStatus(400) - self.assertErrorPage( - 400, - 'The request entity could not be decoded. The following charsets ' - "were attempted: ['us-ascii', 'utf-8']") - - def test_nontext(self): - self.getPage('/nontext') - self.assertHeader('Content-Type', 'application/binary;charset=utf-8') - self.assertBody('\x00\x01\x02\x03') - - def testEncoding(self): - # Default encoding should be utf-8 - self.getPage('/mao_zedong') - self.assertBody(sing8) - - # Ask for utf-16. - self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')]) - self.assertHeader('Content-Type', 'text/html;charset=utf-16') - self.assertBody(sing16) - - # 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.assertBody(sing16) - - # The "*" value should default to our default_encoding, utf-8 - self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')]) - self.assertBody(sing8) - - # 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.') - - # 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.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.') - - # Test the 'encoding' arg to encode. - self.getPage('/utf8') - self.assertBody(sing8) - self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')]) - 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.assertStatus('400 Bad Request') - - def testGzip(self): - zbuf = io.BytesIO() - zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) - zfile.write(b'Hello, world') - zfile.close() - - self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')]) - self.assertInBody(zbuf.getvalue()[:3]) - self.assertHeader('Vary', 'Accept-Encoding') - self.assertHeader('Content-Encoding', 'gzip') - - # Test when gzip is denied. - self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')]) - self.assertHeader('Vary', 'Accept-Encoding') - self.assertNoHeader('Content-Encoding') - self.assertBody('Hello, world') - - self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')]) - self.assertHeader('Vary', 'Accept-Encoding') - self.assertNoHeader('Content-Encoding') - self.assertBody('Hello, world') - - # Test that trailing comma doesn't cause IndexError - # Ref: https://github.com/cherrypy/cherrypy/issues/988 - self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')]) - self.assertStatus(200) - self.assertNotInBody('IndexError') - - self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')]) - self.assertStatus(406) - self.assertNoHeader('Content-Encoding') - self.assertErrorPage(406, 'identity, gzip') - - # Test for ticket #147 - self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')]) - self.assertNoHeader('Content-Encoding') - self.assertStatus(500) - self.assertErrorPage(500, pattern='IndexError\n') - - # In this case, there's nothing we can do to deliver a - # 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')]) - 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')]) - - def test_UnicodeHeaders(self): - self.getPage('/cookies_and_headers') - self.assertBody('Any content') diff --git a/libraries/cherrypy/test/test_etags.py b/libraries/cherrypy/test/test_etags.py deleted file mode 100644 index 293eb866..00000000 --- a/libraries/cherrypy/test/test_etags.py +++ /dev/null @@ -1,84 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy.test import helper - - -class ETagTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def resource(self): - return 'Oh wah ta goo Siam.' - - @cherrypy.expose - def fail(self, code): - code = int(code) - if 300 <= code <= 399: - raise cherrypy.HTTPRedirect([], code) - else: - raise cherrypy.HTTPError(code) - - @cherrypy.expose - # In Python 3, tools.encode is on by default - @cherrypy.config(**{'tools.encode.on': True}) - def unicoded(self): - return ntou('I am a \u1ee4nicode string.', 'escape') - - conf = {'/': {'tools.etags.on': True, - 'tools.etags.autotags': True, - }} - cherrypy.tree.mount(Root(), config=conf) - - def test_etags(self): - self.getPage('/resource') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('Oh wah ta goo Siam.') - etag = self.assertHeader('ETag') - - # Test If-Match (both valid and invalid) - self.getPage('/resource', headers=[('If-Match', etag)]) - self.assertStatus('200 OK') - self.getPage('/resource', headers=[('If-Match', '*')]) - self.assertStatus('200 OK') - self.getPage('/resource', headers=[('If-Match', '*')], method='POST') - self.assertStatus('200 OK') - self.getPage('/resource', headers=[('If-Match', 'a bogus tag')]) - self.assertStatus('412 Precondition Failed') - - # 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.assertStatus('412 Precondition Failed') - self.getPage('/resource', headers=[('If-None-Match', '*')]) - self.assertStatus(304) - self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')]) - self.assertStatus('200 OK') - - def test_errors(self): - self.getPage('/resource') - self.assertStatus(200) - etag = self.assertHeader('ETag') - - # Test raising errors in page handler - self.getPage('/fail/412', headers=[('If-Match', etag)]) - self.assertStatus(412) - self.getPage('/fail/304', headers=[('If-Match', etag)]) - self.assertStatus(304) - self.getPage('/fail/412', headers=[('If-None-Match', '*')]) - self.assertStatus(412) - self.getPage('/fail/304', headers=[('If-None-Match', '*')]) - self.assertStatus(304) - - def test_unicode_body(self): - self.getPage('/unicoded') - self.assertStatus(200) - etag1 = self.assertHeader('ETag') - self.getPage('/unicoded', headers=[('If-Match', etag1)]) - self.assertStatus(200) - self.assertHeader('ETag', etag1) diff --git a/libraries/cherrypy/test/test_http.py b/libraries/cherrypy/test/test_http.py deleted file mode 100644 index 0899d4d0..00000000 --- a/libraries/cherrypy/test/test_http.py +++ /dev/null @@ -1,307 +0,0 @@ -# coding: utf-8 -"""Tests for managing HTTP issues (malformed requests, etc).""" - -import errno -import mimetypes -import socket -import sys -from unittest import mock - -import six -from six.moves.http_client import HTTPConnection -from six.moves import urllib - -import cherrypy -from cherrypy._cpcompat import HTTPSConnection, quote - -from cherrypy.test import helper - - -def is_ascii(text): - """ - Return True if the text encodes as ascii. - """ - try: - text.encode('ascii') - return True - except Exception: - pass - return False - - -def encode_filename(filename): - """ - Given a filename to be used in a multipart/form-data, - encode the name. Return the key and encoded filename. - """ - if is_ascii(filename): - return 'filename', '"{filename}"'.format(**locals()) - encoded = quote(filename, encoding='utf-8') - return 'filename*', "'".join(( - 'UTF-8', - '', # lang - encoded, - )) - - -def encode_multipart_formdata(files): - """Return (content_type, body) ready for httplib.HTTP instance. - - files: a sequence of (name, filename, value) tuples for multipart uploads. - filename can be a string or a tuple ('filename string', 'encoding') - """ - BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$' - L = [] - for key, filename, value in files: - L.append('--' + BOUNDARY) - - fn_key, encoded = encode_filename(filename) - 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) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = '\r\n'.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - -class HTTPTests(helper.CPWebCase): - - def make_connection(self): - if self.scheme == 'https': - return HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - return HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self, *args, **kwargs): - return 'Hello world!' - - @cherrypy.expose - @cherrypy.config(**{'request.process_request_body': False}) - def no_body(self, *args, **kwargs): - return 'Hello world!' - - @cherrypy.expose - def post_multipart(self, file): - """Return a summary ("a * 65536\nb * 65536") of the uploaded - file. - """ - contents = file.file.read() - summary = [] - curchar = None - count = 0 - for c in contents: - if c == curchar: - count += 1 - else: - if count: - if six.PY3: - curchar = chr(curchar) - summary.append('%s * %d' % (curchar, count)) - count = 1 - curchar = c - if count: - if six.PY3: - curchar = chr(curchar) - summary.append('%s * %d' % (curchar, count)) - return ', '.join(summary) - - @cherrypy.expose - def post_filename(self, myfile): - '''Return the name of the file which was uploaded.''' - return myfile.filename - - cherrypy.tree.mount(Root()) - cherrypy.config.update({'server.max_request_body_size': 30000000}) - - def test_no_content_length(self): - # "The presence of a message-body in a request is signaled by the - # inclusion of a Content-Length or Transfer-Encoding header field in - # the request's message-headers." - # - # Send a message with neither header and no body. Even though - # the request is of method POST, this should be OK because we set - # request.process_request_body to False for our handler. - c = self.make_connection() - c.request('POST', '/no_body') - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(200) - self.assertBody(b'Hello world!') - - # Now send a message that has no Content-Length, but does send a body. - # Verify that CP times out the socket and responds - # with 411 Length Required. - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - - # `_get_content_length` is needed for Python 3.6+ - with mock.patch.object( - c, - '_get_content_length', - lambda body, method: None, - create=True): - # `_set_content_length` is needed for Python 2.7-3.5 - with mock.patch.object(c, '_set_content_length', create=True): - c.request('POST', '/') - - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(411) - - def test_post_multipart(self): - alphabet = 'abcdefghijklmnopqrstuvwxyz' - # generate file contents for a large post - contents = ''.join([c * 65536 for c in alphabet]) - - # encode as multipart form data - files = [('file', 'file.txt', contents)] - content_type, body = encode_multipart_formdata(files) - body = body.encode('Latin-1') - - # post file - c = self.make_connection() - c.putrequest('POST', '/post_multipart') - c.putheader('Content-Type', content_type) - c.putheader('Content-Length', str(len(body))) - c.endheaders() - c.send(body) - - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(200) - parts = ['%s * 65536' % ch for ch in alphabet] - self.assertBody(', '.join(parts)) - - def test_post_filename_with_special_characters(self): - '''Testing that we can handle filenames with special characters. This - was reported as a bug in: - https://github.com/cherrypy/cherrypy/issues/1146/ - https://github.com/cherrypy/cherrypy/issues/1397/ - https://github.com/cherrypy/cherrypy/issues/1694/ - ''' - # 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', - ] - for fname in fnames: - files = [('myfile', fname, 'yunyeenyunyue')] - content_type, body = encode_multipart_formdata(files) - body = body.encode('Latin-1') - - # post file - c = self.make_connection() - c.putrequest('POST', '/post_filename') - c.putheader('Content-Type', content_type) - c.putheader('Content-Length', str(len(body))) - c.endheaders() - c.send(body) - - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(200) - self.assertBody(fname) - - def test_malformed_request_line(self): - if getattr(cherrypy.server, 'using_apache', False): - return self.skip('skipped due to known Apache differences...') - - # Test missing version in Request-Line - c = self.make_connection() - c._output(b'geT /') - c._send_output() - if hasattr(c, 'strict'): - response = c.response_class(c.sock, strict=c.strict, method='GET') - else: - # Python 3.2 removed the 'strict' feature, saying: - # "http.client now always assumes HTTP/1.x compliant servers." - response = c.response_class(c.sock, method='GET') - response.begin() - self.assertEqual(response.status, 400) - self.assertEqual(response.fp.read(22), b'Malformed Request-Line') - c.close() - - def test_request_line_split_issue_1220(self): - params = { - 'intervenant-entreprise-evenement_classaction': - 'evenement-mailremerciements', - '_path': 'intervenant-entreprise-evenement', - 'intervenant-entreprise-evenement_action-id': 19404, - 'intervenant-entreprise-evenement_id': 19404, - 'intervenant-entreprise_id': 28092, - } - Request_URI = '/index?' + urllib.parse.urlencode(params) - self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256) - self.getPage(Request_URI) - self.assertBody('Hello world!') - - def test_malformed_header(self): - c = self.make_connection() - c.putrequest('GET', '/') - c.putheader('Content-Type', 'text/plain') - # See https://github.com/cherrypy/cherrypy/issues/941 - c._output(b're, 1.2.3.4#015#012') - c.endheaders() - - response = c.getresponse() - self.status = str(response.status) - self.assertStatus(400) - self.body = response.fp.read(20) - self.assertBody('Illegal header line.') - - def test_http_over_https(self): - if self.scheme != 'https': - return self.skip('skipped (not running HTTPS)... ') - - # Try connecting without SSL. - conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - conn.putrequest('GET', '/', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - try: - 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.') - except socket.error: - e = sys.exc_info()[1] - # "Connection reset by peer" is also acceptable. - if e.errno != errno.ECONNRESET: - raise - - def test_garbage_in(self): - # Connect without SSL regardless of server.scheme - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c._output(b'gjkgjklsgjklsgjkljklsg') - c._send_output() - response = c.response_class(c.sock, method='GET') - try: - response.begin() - self.assertEqual(response.status, 400) - self.assertEqual(response.fp.read(22), - b'Malformed Request-Line') - c.close() - except socket.error: - e = sys.exc_info()[1] - # "Connection reset by peer" is also acceptable. - if e.errno != errno.ECONNRESET: - raise diff --git a/libraries/cherrypy/test/test_httputil.py b/libraries/cherrypy/test/test_httputil.py deleted file mode 100644 index 656b8a3d..00000000 --- a/libraries/cherrypy/test/test_httputil.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Test helpers from ``cherrypy.lib.httputil`` module.""" -import pytest -from six.moves import http_client - -from cherrypy.lib import httputil - - -@pytest.mark.parametrize( - 'script_name,path_info,expected_url', - [ - ('/sn/', '/pi/', '/sn/pi/'), - ('/sn/', '/pi', '/sn/pi'), - ('/sn/', '/', '/sn/'), - ('/sn/', '', '/sn/'), - ('/sn', '/pi/', '/sn/pi/'), - ('/sn', '/pi', '/sn/pi'), - ('/sn', '/', '/sn/'), - ('/sn', '', '/sn'), - ('/', '/pi/', '/pi/'), - ('/', '/pi', '/pi'), - ('/', '/', '/'), - ('/', '', '/'), - ('', '/pi/', '/pi/'), - ('', '/pi', '/pi'), - ('', '/', '/'), - ('', '', '/'), - ] -) -def test_urljoin(script_name, path_info, expected_url): - """Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO.""" - actual_url = httputil.urljoin(script_name, path_info) - assert actual_url == expected_url - - -EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows') -EXPECTED_500 = ( - 500, - 'Internal Server Error', - 'The server encountered an unexpected condition which ' - 'prevented it from fulfilling the request.', -) -EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI') -EXPECTED_444 = (444, 'Non-existent reason', '') - - -@pytest.mark.parametrize( - 'status,expected_status', - [ - (None, EXPECTED_200), - (200, EXPECTED_200), - ('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 - statuses processing.""" - assert httputil.valid_status(status) == expected_status - - -@pytest.mark.parametrize( - 'status_code,error_msg', - [ - ('hey', "Illegal response status from server ('hey' is non-numeric)."), - ( - {'hey': 'hi'}, - 'Illegal response status from server ' - "({'hey': 'hi'} is non-numeric).", - ), - (1, 'Illegal response status from server (1 is out of range).'), - (600, '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.""" - with pytest.raises(ValueError) as excinfo: - httputil.valid_status(status_code) - - assert error_msg in str(excinfo) diff --git a/libraries/cherrypy/test/test_iterator.py b/libraries/cherrypy/test/test_iterator.py deleted file mode 100644 index 92f08e7c..00000000 --- a/libraries/cherrypy/test/test_iterator.py +++ /dev/null @@ -1,196 +0,0 @@ -import six - -import cherrypy -from cherrypy.test import helper - - -class IteratorBase(object): - - created = 0 - datachunk = 'butternut squash' * 256 - - @classmethod - def incr(cls): - cls.created += 1 - - @classmethod - def decr(cls): - cls.created -= 1 - - -class OurGenerator(IteratorBase): - - def __iter__(self): - self.incr() - try: - for i in range(1024): - yield self.datachunk - finally: - self.decr() - - -class OurIterator(IteratorBase): - - started = False - closed_off = False - count = 0 - - def increment(self): - self.incr() - - def decrement(self): - if not self.closed_off: - self.closed_off = True - self.decr() - - def __iter__(self): - return self - - def __next__(self): - if not self.started: - self.started = True - self.increment() - self.count += 1 - if self.count > 1024: - raise StopIteration - return self.datachunk - - next = __next__ - - def __del__(self): - self.decrement() - - -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() - - -class OurUnclosableIterator(OurIterator): - close = 'close' # not callable! - - -class IteratorTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class Root(object): - - @cherrypy.expose - def count(self, clsname): - cherrypy.response.headers['Content-Type'] = 'text/plain' - return six.text_type(globals()[clsname].created) - - @cherrypy.expose - def getall(self, clsname): - cherrypy.response.headers['Content-Type'] = 'text/plain' - return globals()[clsname]() - - @cherrypy.expose - @cherrypy.config(**{'response.stream': True}) - def stream(self, clsname): - return self.getall(clsname) - - cherrypy.tree.mount(Root()) - - def test_iterator(self): - try: - self._test_iterator() - except Exception: - 'Test fails intermittently. See #1419' - - def _test_iterator(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Check the counts of all the classes, they should be zero. - closables = ['OurClosableIterator', 'OurGenerator'] - unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator'] - all_classes = closables + unclosables - - import random - random.shuffle(all_classes) - - for clsname in all_classes: - self.getPage('/count/' + clsname) - self.assertStatus(200) - self.assertBody('0') - - # We should also be able to read the entire content body - # successfully, though we don't need to, we just want to - # check the header. - for clsname in all_classes: - itr_conn = self.get_conn() - itr_conn.putrequest('GET', '/getall/' + clsname) - itr_conn.endheaders() - response = itr_conn.getresponse() - self.assertEqual(response.status, 200) - headers = response.getheaders() - for header_name, header_value in headers: - if header_name.lower() == 'content-length': - expected = six.text_type(1024 * 16 * 256) - assert header_value == expected, header_value - break - else: - raise AssertionError('No Content-Length header found') - - # As the response should be fully consumed by CherryPy - # before sending back, the count should still be at zero - # by the time the response has been sent. - self.getPage('/count/' + clsname) - self.assertStatus(200) - self.assertBody('0') - - # Now we do the same check with streaming - some classes will - # be automatically closed, while others cannot. - stream_counts = {} - for clsname in all_classes: - itr_conn = self.get_conn() - itr_conn.putrequest('GET', '/stream/' + clsname) - itr_conn.endheaders() - response = itr_conn.getresponse() - self.assertEqual(response.status, 200) - response.fp.read(65536) - - # Let's check the count - this should always be one. - self.getPage('/count/' + clsname) - self.assertBody('1') - - # Now if we close the connection, the count should go back - # to zero. - itr_conn.close() - self.getPage('/count/' + clsname) - - # If this is a response which should be easily closed, then - # 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) - - stream_counts[clsname] = int(self.body) - - # Check that we closed off the classes which should provide - # easy mechanisms for doing so. - for clsname in closables: - assert stream_counts[clsname] == 0, ( - 'did not close off stream response correctly, expected ' - 'count of zero for %s: %s' % (clsname, stream_counts) - ) diff --git a/libraries/cherrypy/test/test_json.py b/libraries/cherrypy/test/test_json.py deleted file mode 100644 index 1585f6e6..00000000 --- a/libraries/cherrypy/test/test_json.py +++ /dev/null @@ -1,102 +0,0 @@ -import cherrypy -from cherrypy.test import helper - -from cherrypy._cpcompat import json - - -json_out = cherrypy.config(**{'tools.json_out.on': True}) -json_in = cherrypy.config(**{'tools.json_in.on': True}) - - -class JsonTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root(object): - - @cherrypy.expose - def plain(self): - return 'hello' - - @cherrypy.expose - @json_out - def json_string(self): - return 'hello' - - @cherrypy.expose - @json_out - def json_list(self): - return ['a', 'b', 42] - - @cherrypy.expose - @json_out - def json_dict(self): - return {'answer': 42} - - @cherrypy.expose - @json_in - def json_post(self): - if cherrypy.request.json == [13, 'c']: - return 'ok' - else: - return 'nok' - - @cherrypy.expose - @json_out - @cherrypy.config(**{'tools.caching.on': True}) - def json_cached(self): - return 'hello there' - - root = Root() - cherrypy.tree.mount(root) - - def test_json_output(self): - if json is None: - self.skip('json not found ') - return - - self.getPage('/plain') - self.assertBody('hello') - - self.getPage('/json_string') - self.assertBody('"hello"') - - self.getPage('/json_list') - self.assertBody('["a", "b", 42]') - - self.getPage('/json_dict') - self.assertBody('{"answer": 42}') - - def test_json_input(self): - if json is None: - self.skip('json not found ') - return - - body = '[13, "c"]' - 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)))] - 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)))] - self.getPage('/json_post', method='POST', headers=headers, body=body) - self.assertStatus(400, 'Invalid JSON document') - - def test_cached(self): - if json is None: - self.skip('json not found ') - return - - self.getPage('/json_cached') - self.assertStatus(200, '"hello"') - - self.getPage('/json_cached') # 2'nd time to hit cache - self.assertStatus(200, '"hello"') diff --git a/libraries/cherrypy/test/test_logging.py b/libraries/cherrypy/test/test_logging.py deleted file mode 100644 index c4948c20..00000000 --- a/libraries/cherrypy/test/test_logging.py +++ /dev/null @@ -1,209 +0,0 @@ -"""Basic tests for the CherryPy core: request handling.""" - -import os -from unittest import mock - -import six - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy.test import helper, logtest - -localDir = os.path.dirname(__file__) -access_log = os.path.join(localDir, 'access.log') -error_log = os.path.join(localDir, 'error.log') - -# Some unicode strings. -tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') -erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') - - -def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'hello' - - @cherrypy.expose - def uni_code(self): - cherrypy.request.login = tartaros - cherrypy.request.remote.name = erebos - - @cherrypy.expose - def slashes(self): - cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1' - - @cherrypy.expose - def whitespace(self): - # User-Agent = "User-Agent" ":" 1*( product | comment ) - # comment = "(" *( ctext | quoted-pair | comment ) ")" - # ctext = <any TEXT excluding "(" and ")"> - # TEXT = <any OCTET except CTLs, but including LWS> - # LWS = [CRLF] 1*( SP | HT ) - cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)' - - @cherrypy.expose - def as_string(self): - return 'content' - - @cherrypy.expose - def as_yield(self): - yield 'content' - - @cherrypy.expose - @cherrypy.config(**{'tools.log_tracebacks.on': True}) - def error(self): - raise ValueError() - - root = Root() - - cherrypy.config.update({ - 'log.error_file': error_log, - 'log.access_file': access_log, - }) - cherrypy.tree.mount(root) - - -class AccessLogTests(helper.CPWebCase, logtest.LogCase): - setup_server = staticmethod(setup_server) - - logfile = access_log - - def testNormalReturn(self): - self.markLog() - self.getPage('/as_string', - headers=[('Referer', 'http://www.cherrypy.org/'), - ('User-Agent', 'Mozilla/5.0')]) - self.assertBody('content') - self.assertStatus(200) - - intro = '%s - - [' % self.interface() - - self.assertLog(-1, intro) - - if [k for k, v in self.headers if k.lower() == 'content-length']: - self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 ' - '"http://www.cherrypy.org/" "Mozilla/5.0"' - % self.prefix()) - else: - self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - ' - '"http://www.cherrypy.org/" "Mozilla/5.0"' - % self.prefix()) - - def testNormalYield(self): - self.markLog() - self.getPage('/as_yield') - self.assertBody('content') - self.assertStatus(200) - - intro = '%s - - [' % self.interface() - - self.assertLog(-1, intro) - if [k for k, v in self.headers if k.lower() == 'content-length']: - self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' % - self.prefix()) - else: - self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""' - % self.prefix()) - - @mock.patch( - 'cherrypy._cplogging.LogManager.access_log_format', - '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}' - if six.PY3 else - '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' - ) - def testCustomLogFormat(self): - """Test a customized access_log_format string, which is a - feature of _cplogging.LogManager.access().""" - self.markLog() - self.getPage('/as_string', headers=[('Referer', 'REFERER'), - ('User-Agent', 'USERAGENT'), - ('Host', 'HOST')]) - self.assertLog(-1, '%s - - [' % self.interface()) - self.assertLog(-1, '] "GET /as_string HTTP/1.1" ' - '200 7 "REFERER" "USERAGENT" HOST') - - @mock.patch( - 'cherrypy._cplogging.LogManager.access_log_format', - '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}' - if six.PY3 else - '%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' - ) - def testTimezLogFormat(self): - """Test a customized access_log_format string, which is a - feature of _cplogging.LogManager.access().""" - self.markLog() - - expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime()) - with mock.patch( - 'cherrypy._cplogging.LazyRfc3339UtcTime', - lambda: expected_time): - self.getPage('/as_string', headers=[('Referer', 'REFERER'), - ('User-Agent', 'USERAGENT'), - ('Host', 'HOST')]) - - self.assertLog(-1, '%s - - ' % self.interface()) - self.assertLog(-1, expected_time) - self.assertLog(-1, ' "GET /as_string HTTP/1.1" ' - '200 7 "REFERER" "USERAGENT" HOST') - - @mock.patch( - 'cherrypy._cplogging.LogManager.access_log_format', - '{i}' if six.PY3 else '%(i)s' - ) - def testUUIDv4ParameterLogFormat(self): - """Test rendering of UUID4 within access log.""" - self.markLog() - self.getPage('/as_string') - self.assertValidUUIDv4() - - def testEscapedOutput(self): - # Test unicode in access log pieces. - self.markLog() - self.getPage('/uni_code') - self.assertStatus(200) - if six.PY3: - # The repr of a bytestring in six.PY3 includes a b'' prefix - self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) - else: - self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) - # Test the erebos value. Included inline for your enlightenment. - # Note the 'r' prefix--those backslashes are literals. - self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') - - # Test backslashes in output. - self.markLog() - self.getPage('/slashes') - self.assertStatus(200) - if six.PY3: - self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"') - else: - self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') - - # Test whitespace in output. - self.markLog() - self.getPage('/whitespace') - self.assertStatus(200) - # Again, note the 'r' prefix. - self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') - - -class ErrorLogTests(helper.CPWebCase, logtest.LogCase): - setup_server = staticmethod(setup_server) - - logfile = error_log - - def testTracebacks(self): - # Test that tracebacks get written to the error log. - self.markLog() - ignore = helper.webtest.ignored_exceptions - ignore.append(ValueError) - try: - self.getPage('/error') - self.assertInBody('raise ValueError()') - self.assertLog(0, 'HTTP') - self.assertLog(1, 'Traceback (most recent call last):') - self.assertLog(-2, 'raise ValueError()') - finally: - ignore.pop() diff --git a/libraries/cherrypy/test/test_mime.py b/libraries/cherrypy/test/test_mime.py deleted file mode 100644 index ef35d10e..00000000 --- a/libraries/cherrypy/test/test_mime.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Tests for various MIME issues, including the safe_multipart Tool.""" - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy.test import helper - - -def setup_server(): - - class Root: - - @cherrypy.expose - def multipart(self, parts): - return repr(parts) - - @cherrypy.expose - def multipart_form_data(self, **kwargs): - return repr(list(sorted(kwargs.items()))) - - @cherrypy.expose - def flashupload(self, Filedata, Upload, Filename): - 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()) - - -# Client-side code # - - -class MultipartTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_multipart(self): - text_part = ntou('This is the text version') - html_part = ntou( - """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> -<html> -<head> - <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type"> -</head> -<body bgcolor="#ffffff" text="#000000"> - -This is the <strong>HTML</strong> version -</body> -</html> -""") - 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))), - ] - self.getPage('/multipart', headers, 'POST', body) - 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), - self.assertBody( - repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) - - -class SafeMultipartHandlingTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_Flash_Upload(self): - headers = [ - ('Accept', 'text/*'), - ('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'<?xml version="1.0" encoding="UTF-8"?>\r\n' - b'<projectDescription>\r\n' - b'</projectDescription>\r\n') - body = ( - b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - b'Content-Disposition: form-data; name="Filename"\r\n' - b'\r\n' - b'.project\r\n' - b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - 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'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - b'Content-Disposition: form-data; name="Upload"\r\n' - b'\r\n' - b'Submit Query\r\n' - # Flash apps omit the trailing \r\n on the last line: - b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' - ) - self.getPage('/flashupload', headers, 'POST', body) - self.assertBody('Upload: Submit Query, Filename: .project, ' - 'Filedata: %r' % filedata) diff --git a/libraries/cherrypy/test/test_misc_tools.py b/libraries/cherrypy/test/test_misc_tools.py deleted file mode 100644 index fb85b8f8..00000000 --- a/libraries/cherrypy/test/test_misc_tools.py +++ /dev/null @@ -1,210 +0,0 @@ -import os - -import cherrypy -from cherrypy import tools -from cherrypy.test import helper - - -localDir = os.path.dirname(__file__) -logfile = os.path.join(localDir, 'test_misc_tools.log') - - -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, - }) - def other(self): - return 'salut' - - @cherrypy.config(**{'tools.accept.on': True}) - class Accept: - - @cherrypy.expose - def index(self): - return '<a href="feed">Atom feed</a>' - - @cherrypy.expose - @tools.accept(media='application/atom+xml') - def feed(self): - return """<?xml version="1.0" encoding="utf-8"?> -<feed xmlns="http://www.w3.org/2005/Atom"> - <title>Unknown Blog</title> -</feed>""" - - @cherrypy.expose - def select(self): - # We could also write this: mtype = cherrypy.lib.accept.accept(...) - mtype = tools.accept.callable(['text/html', 'text/plain']) - if mtype == 'text/html': - return '<h2>Page Title</h2>' - else: - 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' - cherrypy.request.headers.get('Accept-Encoding') - # Read a header directly with '__getitem__' - cherrypy.request.headers['Host'] - # Read a header directly with '__contains__' - 'If-Modified-Since' in cherrypy.request.headers - # Read a header directly - 'Range' in cherrypy.request.headers - # Call a lib function - 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}, - } - - root = Root() - root.referer = Referer() - root.accept = Accept() - root.autovary = AutoVary() - cherrypy.tree.mount(root, config=conf) - cherrypy.config.update({'log.error_file': logfile}) - - -class ResponseHeadersTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testResponseHeadersDecorator(self): - self.getPage('/') - self.assertHeader('Content-Language', 'en-GB') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - - def testResponseHeaders(self): - self.getPage('/other') - self.assertHeader('Content-Language', 'fr') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - - -class RefererTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testReferer(self): - self.getPage('/referer/accept') - self.assertErrorPage(403, 'Forbidden Referer header.') - - self.getPage('/referer/accept', - headers=[('Referer', 'http://www.example.com/')]) - self.assertStatus(200) - self.assertBody('Accepted!') - - # Reject - self.getPage('/referer/reject') - self.assertStatus(200) - self.assertBody('Accepted!') - - self.getPage('/referer/reject', - headers=[('Referer', 'http://www.example.com/')]) - self.assertErrorPage(403, 'Forbidden Referer header.') - - -class AcceptTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_Accept_Tool(self): - # Test with no header provided - self.getPage('/accept/feed') - self.assertStatus(200) - self.assertInBody('<title>Unknown Blog</title>') - - # Specify exact media type - self.getPage('/accept/feed', - headers=[('Accept', 'application/atom+xml')]) - self.assertStatus(200) - self.assertInBody('<title>Unknown Blog</title>') - - # Specify matching media range - self.getPage('/accept/feed', headers=[('Accept', 'application/*')]) - self.assertStatus(200) - self.assertInBody('<title>Unknown Blog</title>') - - # Specify all media ranges - self.getPage('/accept/feed', headers=[('Accept', '*/*')]) - self.assertStatus(200) - self.assertInBody('<title>Unknown Blog</title>') - - # 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.') - - # Test resource where tool is 'on' but media is None (not set). - self.getPage('/accept/') - self.assertStatus(200) - self.assertBody('<a href="feed">Atom feed</a>') - - def test_accept_selection(self): - # Try both our expected media types - self.getPage('/accept/select', [('Accept', 'text/html')]) - self.assertStatus(200) - self.assertBody('<h2>Page Title</h2>') - 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.assertStatus(200) - self.assertBody('PAGE TITLE') - - # text/* and */* should prefer text/html since it comes first - # in our 'media' argument to tools.accept - self.getPage('/accept/select', [('Accept', 'text/*')]) - self.assertStatus(200) - self.assertBody('<h2>Page Title</h2>') - self.getPage('/accept/select', [('Accept', '*/*')]) - self.assertStatus(200) - self.assertBody('<h2>Page Title</h2>') - - # Try unacceptable media types - self.getPage('/accept/select', [('Accept', 'application/xml')]) - self.assertErrorPage( - 406, - 'Your client sent this Accept header: application/xml. ' - 'But this resource only emits these media types: ' - 'text/html, text/plain.') - - -class AutoVaryTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testAutoVary(self): - self.getPage('/autovary/') - self.assertHeader( - 'Vary', - 'Accept, Accept-Charset, Accept-Encoding, ' - 'Host, If-Modified-Since, Range' - ) diff --git a/libraries/cherrypy/test/test_native.py b/libraries/cherrypy/test/test_native.py deleted file mode 100644 index caebc3f4..00000000 --- a/libraries/cherrypy/test/test_native.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Test the native server.""" - -import pytest -from requests_toolbelt import sessions - -import cherrypy._cpnative_server - - -pytestmark = pytest.mark.skipif( - 'sys.platform == "win32"', - reason='tests fail on Windows', -) - - -@pytest.fixture -def cp_native_server(request): - """A native server.""" - class Root(object): - @cherrypy.expose - def index(self): - return 'Hello World!' - - cls = cherrypy._cpnative_server.CPHTTPServer - cherrypy.server.httpserver = cls(cherrypy.server) - - cherrypy.tree.mount(Root(), '/') - cherrypy.engine.start() - request.addfinalizer(cherrypy.engine.stop) - url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals()) - return sessions.BaseUrlSession(url) - - -def test_basic_request(cp_native_server): - """A request to a native server should succeed.""" - cp_native_server.get('/') diff --git a/libraries/cherrypy/test/test_objectmapping.py b/libraries/cherrypy/test/test_objectmapping.py deleted file mode 100644 index 98402b8b..00000000 --- a/libraries/cherrypy/test/test_objectmapping.py +++ /dev/null @@ -1,430 +0,0 @@ -import sys -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy._cptree import Application -from cherrypy.test import helper - -script_names = ['', '/foo', '/users/fred/blog', '/corp/blog'] - - -class ObjectMappingTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self, name='world'): - return name - - @cherrypy.expose - def foobar(self): - return 'bar' - - @cherrypy.expose - def default(self, *params, **kwargs): - return 'default:' + repr(params) - - @cherrypy.expose - def other(self): - return 'other' - - @cherrypy.expose - def extra(self, *p): - return repr(p) - - @cherrypy.expose - def redirect(self): - raise cherrypy.HTTPRedirect('dir1/', 302) - - def notExposed(self): - return 'not exposed' - - @cherrypy.expose - def confvalue(self): - return cherrypy.request.config.get('user') - - @cherrypy.expose - def redirect_via_url(self, path): - raise cherrypy.HTTPRedirect(cherrypy.url(path)) - - @cherrypy.expose - def translate_html(self): - return 'OK' - - @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' - - @cherrypy.expose - @cherrypy.config(**{'tools.trailing_slash.extra': True}) - def myMethod(self): - return 'myMethod from dir1, path_info is:' + repr( - 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 - - @cherrypy.expose - def script_name(self): - return cherrypy.tree.script_name() - - @cherrypy.expose - def cherrypy_url(self): - return cherrypy.url('/extra') - - @cherrypy.expose - 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') - - # MethodDispatcher code - @cherrypy.expose - class ByMethod: - - def __init__(self, *things): - self.things = list(things) - - def GET(self): - return repr(self.things) - - def POST(self, thing): - self.things.append(thing) - - class Collection: - default = ByMethod('a', 'bit') - - Root.exposing = Exposing() - Root.exposingnew = ExposingNewStyle() - Root.dir1 = Dir1() - Root.dir1.dir2 = Dir2() - Root.dir1.dir2.dir3 = Dir3() - Root.dir1.dir2.dir3.dir4 = Dir4() - Root.defnoindex = DefNoIndex() - Root.bymethod = ByMethod('another') - Root.collection = Collection() - - d = cherrypy.dispatch.MethodDispatcher() - for url in script_names: - 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!' - - cherrypy.tree.mount(Isolated(), '/isolated') - - @cherrypy.expose - class AnotherApp: - - def GET(self): - return 'milk' - - cherrypy.tree.mount(AnotherApp(), '/app', - {'/': {'request.dispatch': d}}) - - def testObjectMapping(self): - for url in script_names: - self.script_name = url - - self.getPage('/') - self.assertBody('world') - - self.getPage('/dir1/myMethod') - self.assertBody( - "myMethod from dir1, path_info is:'/dir1/myMethod'") - - self.getPage('/this/method/does/not/exist') - self.assertBody( - "default:('this', 'method', 'does', 'not', 'exist')") - - self.getPage('/extra/too/much') - self.assertBody("('too', 'much')") - - self.getPage('/other') - self.assertBody('other') - - self.getPage('/notExposed') - self.assertBody("default:('notExposed',)") - - self.getPage('/dir1/dir2/') - self.assertBody('index for dir2, path is:/dir1/dir2/') - - # Test omitted trailing slash (should be redirected by default). - self.getPage('/dir1/dir2') - self.assertStatus(301) - self.assertHeader('Location', '%s/dir1/dir2/' % self.base()) - - # Test extra trailing slash (should be redirected if configured). - self.getPage('/dir1/myMethod/') - self.assertStatus(301) - self.assertHeader('Location', '%s/dir1/myMethod' % self.base()) - - # 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')") - - # Test *vpath when default() is defined but not index() - # This also tests HTTPRedirect with default. - self.getPage('/defnoindex') - self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/contact' % self.base()) - self.getPage('/defnoindex/') - self.assertStatus((302, 303)) - 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.getPage('/redirect') - self.assertStatus('302 Found') - self.assertHeader('Location', '%s/dir1/' % self.base()) - - if not getattr(cherrypy.server, 'using_apache', False): - # Test that we can use URL's which aren't all valid Python - # identifiers - # This should also test the %XX-unquoting of URL's. - self.getPage('/Von%20B%fclow?ID=14') - self.assertBody('ID is 14') - - # Test that %2F in the path doesn't get unquoted too early; - # that is, it should not be used to separate path components. - # See ticket #393. - self.getPage('/page%2Fname') - self.assertBody("default:('page/name',)") - - self.getPage('/dir1/dir2/script_name') - self.assertBody(url) - self.getPage('/dir1/dir2/cherrypy_url') - self.assertBody('%s/extra' % self.base()) - - # Test that configs don't overwrite each other from different apps - self.getPage('/confvalue') - self.assertBody((url or '/').split('/')[-2]) - - self.script_name = '' - - # Test absoluteURI's in the Request-Line - 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.assertBody("default:('abs',)") - - self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') - self.assertBody("default:('rel',)") - - # Test that the "isolated" app doesn't leak url's into the root app. - # If it did leak, Root.default() would answer with - # "default:('isolated', 'doesnt', 'exist')". - self.getPage('/isolated/') - self.assertStatus('200 OK') - self.assertBody('made it!') - self.getPage('/isolated/doesnt/exist') - self.assertStatus('404 Not Found') - - # Make sure /foobar maps to Root.foobar and not to the app - # mounted at /foo. See - # https://github.com/cherrypy/cherrypy/issues/573 - self.getPage('/foobar') - self.assertBody('bar') - - def test_translate(self): - self.getPage('/translate_html') - self.assertStatus('200 OK') - self.assertBody('OK') - - self.getPage('/translate.html') - self.assertStatus('200 OK') - self.assertBody('OK') - - self.getPage('/translate-html') - self.assertStatus('200 OK') - self.assertBody('OK') - - def test_redir_using_url(self): - for url in script_names: - self.script_name = url - - # Test the absolute path to the parent (leading slash) - self.getPage('/redirect_via_url?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the relative path to the parent (no leading slash) - self.getPage('/redirect_via_url?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the absolute path to the parent (leading slash) - self.getPage('/redirect_via_url/?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the relative path to the parent (no leading slash) - self.getPage('/redirect_via_url/?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - def testPositionalParams(self): - self.getPage('/dir1/dir2/posparam/18/24/hut/hike') - self.assertBody('18/24/hut/hike') - - # intermediate index methods should not receive posparams; - # only the "final" index method should do so. - self.getPage('/dir1/dir2/5/3/sir') - self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')") - - # test that extra positional args raises an 404 Not Found - # See https://github.com/cherrypy/cherrypy/issues/733. - self.getPage('/dir1/dir2/script_name/extra/stuff') - self.assertStatus(404) - - def testExpose(self): - # Test the cherrypy.expose function/decorator - self.getPage('/exposing/base') - self.assertBody('expose works!') - - self.getPage('/exposing/1') - self.assertBody('expose works!') - - self.getPage('/exposing/2') - self.assertBody('expose works!') - - self.getPage('/exposingnew/base') - self.assertBody('expose works!') - - self.getPage('/exposingnew/1') - self.assertBody('expose works!') - - self.getPage('/exposingnew/2') - self.assertBody('expose works!') - - def testMethodDispatch(self): - self.getPage('/bymethod') - self.assertBody("['another']") - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage('/bymethod', method='HEAD') - self.assertBody('') - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage('/bymethod', method='POST', body='thing=one') - self.assertBody('') - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage('/bymethod') - self.assertBody(repr(['another', ntou('one')])) - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage('/bymethod', method='PUT') - self.assertErrorPage(405) - self.assertHeader('Allow', 'GET, HEAD, POST') - - # Test default with posparams - self.getPage('/collection/silly', method='POST') - self.getPage('/collection', method='GET') - self.assertBody("['a', 'bit', 'silly']") - - # Test custom dispatcher set on app root (see #737). - self.getPage('/app') - self.assertBody('milk') - - def testTreeMounting(self): - class Root(object): - - @cherrypy.expose - def hello(self): - return 'Hello world!' - - # When mounting an application instance, - # we can't specify a different script name in the call to mount. - a = Application(Root(), '/somewhere') - self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse') - - # When mounting an application instance... - a = Application(Root(), '/somewhere') - # ...we MUST allow in identical script name in the call to mount... - cherrypy.tree.mount(a, '/somewhere') - self.getPage('/somewhere/hello') - self.assertStatus(200) - # ...and MUST allow a missing script_name. - del cherrypy.tree.apps['/somewhere'] - cherrypy.tree.mount(a) - self.getPage('/somewhere/hello') - self.assertStatus(200) - - # In addition, we MUST be able to create an Application using - # script_name == None for access to the wsgi_environ. - a = Application(Root(), script_name=None) - # However, this does not apply to tree.mount - self.assertRaises(TypeError, cherrypy.tree.mount, a, None) - - def testKeywords(self): - if sys.version_info < (3,): - return self.skip('skipped (Python 3 only)') - exec("""class Root(object): - @cherrypy.expose - def hello(self, *, name='world'): - return 'Hello %s!' % name -cherrypy.tree.mount(Application(Root(), '/keywords'))""") - - self.getPage('/keywords/hello') - self.assertStatus(200) - self.getPage('/keywords/hello/extra') - self.assertStatus(404) diff --git a/libraries/cherrypy/test/test_params.py b/libraries/cherrypy/test/test_params.py deleted file mode 100644 index 73b4cb4c..00000000 --- a/libraries/cherrypy/test/test_params.py +++ /dev/null @@ -1,61 +0,0 @@ -import sys -import textwrap - -import cherrypy -from cherrypy.test import helper - - -class ParamsTest(helper.CPWebCase): - @staticmethod - def setup_server(): - class Root: - @cherrypy.expose - @cherrypy.tools.json_out() - @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) - - def test_pass(self): - self.getPage('/resource') - self.assertStatus(200) - self.assertBody('"NoneType"') - - self.getPage('/resource?limit=0') - self.assertStatus(200) - self.assertBody('"int"') - - def test_error(self): - self.getPage('/resource?limit=') - self.assertStatus(400) - self.assertInBody('invalid literal for int') - - cherrypy.config['tools.params.error'] = 422 - self.getPage('/resource?limit=') - self.assertStatus(422) - self.assertInBody('invalid literal for int') - - cherrypy.config['tools.params.exception'] = TypeError - self.getPage('/resource?limit=') - self.assertStatus(500) - - def test_syntax(self): - if sys.version_info < (3,): - return self.skip('skipped (Python 3 only)') - code = textwrap.dedent(""" - class Root: - @cherrypy.expose - @cherrypy.tools.params() - def resource(self, limit: int): - return type(limit).__name__ - conf = {'/': {'tools.params.on': True}} - cherrypy.tree.mount(Root(), config=conf) - """) - exec(code) - - self.getPage('/resource?limit=0') - self.assertStatus(200) - self.assertBody('int') diff --git a/libraries/cherrypy/test/test_plugins.py b/libraries/cherrypy/test/test_plugins.py deleted file mode 100644 index 4d3aa6b1..00000000 --- a/libraries/cherrypy/test/test_plugins.py +++ /dev/null @@ -1,14 +0,0 @@ -from cherrypy.process import plugins - - -__metaclass__ = type - - -class TestAutoreloader: - def test_file_for_file_module_when_None(self): - """No error when module.__file__ is None. - """ - class test_module: - __file__ = None - - assert plugins.Autoreloader._file_for_file_module(test_module) is None diff --git a/libraries/cherrypy/test/test_proxy.py b/libraries/cherrypy/test/test_proxy.py deleted file mode 100644 index 4d34440a..00000000 --- a/libraries/cherrypy/test/test_proxy.py +++ /dev/null @@ -1,154 +0,0 @@ -import cherrypy -from cherrypy.test import helper - -script_names = ['', '/path/to/myapp'] - - -class ProxyTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - - # Set up site - 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) - - @cherrypy.expose - def pageurl(self): - return self.thisnewpage - - @cherrypy.expose - def index(self): - raise cherrypy.HTTPRedirect('dummy') - - @cherrypy.expose - def remoteip(self): - return cherrypy.request.remote.ip - - @cherrypy.expose - @cherrypy.config(**{ - 'tools.proxy.local': 'X-Host', - 'tools.trailing_slash.extra': True, - }) - def xhost(self): - raise cherrypy.HTTPRedirect('blah') - - @cherrypy.expose - def base(self): - return cherrypy.request.base - - @cherrypy.expose - @cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'}) - def ssl(self): - return cherrypy.request.base - - @cherrypy.expose - def newurl(self): - return ("Browse to <a href='%s'>this page</a>." - % cherrypy.url('/this/new/page')) - - @cherrypy.expose - @cherrypy.config(**{ - 'tools.proxy.base': None, - }) - def base_no_base(self): - return cherrypy.request.base - - for sn in script_names: - cherrypy.tree.mount(Root(sn), sn) - - def testProxy(self): - self.getPage('/') - 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')]) - 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) - # Test multiple X-Forwarded-Host headers - 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.assertBody('192.168.0.20') - # Fix bug #1268 - 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) - - # Test X-Forwarded-Proto (lighttpd) - self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')]) - self.assertBody('https://www.mydomain.test') - - # Test X-Forwarded-Ssl (webfaction?) - self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')]) - self.assertBody('https://www.mydomain.test') - - # Test cherrypy.url() - for sn in script_names: - # Test the value inside requests - self.getPage(sn + '/newurl') - self.assertBody( - "Browse to <a href='%s://www.mydomain.test" % self.scheme + - sn + "/this/new/page'>this page</a>.") - self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host', - 'http://www.example.test')]) - self.assertBody("Browse to <a href='http://www.example.test" + - sn + "/this/new/page'>this page</a>.") - - # Test the value outside requests - port = '' - if self.scheme == 'http' and self.PORT != 80: - port = ':%s' % self.PORT - elif self.scheme == 'https' and self.PORT != 443: - port = ':%s' % self.PORT - 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)) - 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) - - def test_no_base_port_in_host(self): - """ - If no base is indicated, and the host header is used to resolve - the base, it should rely on the host header for the port also. - """ - headers = {'Host': 'localhost:8080'}.items() - self.getPage('/base_no_base', headers=headers) - self.assertBody('http://localhost:8080') diff --git a/libraries/cherrypy/test/test_refleaks.py b/libraries/cherrypy/test/test_refleaks.py deleted file mode 100644 index c2fe9e66..00000000 --- a/libraries/cherrypy/test/test_refleaks.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Tests for refleaks.""" - -import itertools -import platform -import threading - -from six.moves.http_client import HTTPConnection - -import cherrypy -from cherrypy._cpcompat import HTTPSConnection -from cherrypy.test import helper - - -data = object() - - -class ReferenceTests(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class Root: - - @cherrypy.expose - def index(self, *args, **kwargs): - cherrypy.request.thing = data - return 'Hello world!' - - cherrypy.tree.mount(Root()) - - def test_threadlocal_garbage(self): - if platform.system() == 'Darwin': - self.skip('queue issues; see #1474') - success = itertools.count() - - def getpage(): - host = '%s:%s' % (self.interface(), self.PORT) - if self.scheme == 'https': - c = HTTPSConnection(host) - else: - c = HTTPConnection(host) - try: - c.putrequest('GET', '/') - c.endheaders() - response = c.getresponse() - body = response.read() - self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello world!') - finally: - c.close() - next(success) - - ITERATIONS = 25 - - ts = [ - threading.Thread(target=getpage) - for _ in range(ITERATIONS) - ] - - for t in ts: - t.start() - - for t in ts: - t.join() - - self.assertEqual(next(success), ITERATIONS) diff --git a/libraries/cherrypy/test/test_request_obj.py b/libraries/cherrypy/test/test_request_obj.py deleted file mode 100644 index 6b93e13d..00000000 --- a/libraries/cherrypy/test/test_request_obj.py +++ /dev/null @@ -1,932 +0,0 @@ -"""Basic tests for the cherrypy.Request object.""" - -from functools import wraps -import os -import sys -import types -import uuid - -import six -from six.moves.http_client import IncompleteRead - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy.lib import httputil -from cherrypy.test import helper - -localDir = os.path.dirname(__file__) - -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' - - @cherrypy.expose - def scheme(self): - return cherrypy.request.scheme - - @cherrypy.expose - def created_example_com_3128(self): - """Handle CONNECT method.""" - cherrypy.response.status = 204 - - @cherrypy.expose - def body_example_com_3128(self): - """Handle CONNECT method.""" - return ( - cherrypy.request.method - + 'ed to ' - + cherrypy.request.path_info - ) - - @cherrypy.expose - def request_uuid4(self): - return [ - str(cherrypy.request.unique_id), - ' ', - str(cherrypy.request.unique_id), - ] - - root = Root() - - class TestType(type): - """Metaclass which automatically exposes all functions in each - 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) - - def ismap(self, x, y): - return 'Coordinates: %s, %s' % (x, y) - - @cherrypy.config(**{'request.query_string_encoding': 'latin1'}) - def default(self, *args, **kwargs): - return 'args: %s kwargs: %s' % (args, sorted(kwargs.items())) - - @cherrypy.expose - class ParamErrorsCallable(object): - - def __call__(self): - return 'data' - - 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' - - @cherrypy.expose - def one_positional_args(self, param1, *args): - return 'data' - - @cherrypy.expose - def one_positional_args_kwargs(self, param1, *args, **kwargs): - return 'data' - - @cherrypy.expose - def one_positional_kwargs(self, param1, **kwargs): - return 'data' - - @cherrypy.expose - def no_positional(self): - return 'data' - - @cherrypy.expose - def no_positional_args(self, *args): - return 'data' - - @cherrypy.expose - def no_positional_args_kwargs(self, *args, **kwargs): - return 'data' - - @cherrypy.expose - def no_positional_kwargs(self, **kwargs): - return 'data' - - callable_object = ParamErrorsCallable() - - @cherrypy.expose - def raise_type_error(self, **kwargs): - raise TypeError('Client Error') - - @cherrypy.expose - def raise_type_error_with_default_param(self, x, y=None): - return '%d' % 'a' # throw an exception - - @cherrypy.expose - @handler_dec - def raise_type_error_decorated(self, *args, **kwargs): - raise TypeError('Client Error') - - def callable_error_page(status, **kwargs): - return "Error %s - Well, I'm very sorry but you haven't paid!" % ( - 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, - }) - def custom(self, err='404'): - raise cherrypy.HTTPError( - int(err), 'No, <b>really</b>, not found!') - - @cherrypy.config(**{ - 'error_page.default': callable_error_page, - }) - def custom_default(self): - return 1 + 'a' # raise an unexpected error - - @cherrypy.config(**{'error_page.404': 'nonexistent.html'}) - def noexist(self): - raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!') - - def page_method(self): - raise ValueError() - - def page_yield(self): - yield 'howdy' - raise ValueError() - - @cherrypy.config(**{'response.stream': True}) - def page_streamed(self): - yield 'word up' - raise ValueError() - yield 'very oops' - - @cherrypy.config(**{'request.show_tracebacks': False}) - def cause_err_in_finalize(self): - # Since status must start with an int, this should error. - cherrypy.response.status = 'ZOO OK' - - @cherrypy.config(**{'request.throw_errors': True}) - def rethrow(self): - """Test that an error raised here will be thrown out to - the server. - """ - raise ValueError() - - class Expect(Test): - - def expectation_failed(self): - expect = cherrypy.request.headers.elements('Expect') - if expect and expect[0].value != '100-continue': - raise cherrypy.HTTPError(400) - 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] - - def doubledheaders(self): - # From https://github.com/cherrypy/cherrypy/issues/165: - # "header field names should not be case sensitive sayes the - # rfc. if i set a headerfield in complete lowercase i end up - # with two header fields, one in lowercase, the other in - # mixed-case." - - # Set the most common headers - hMap = cherrypy.response.headers - 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)) - - # Set a rare header for fun - hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' - - return 'double header test' - - def ifmatch(self): - val = cherrypy.request.headers['If-Match'] - assert isinstance(val, six.text_type) - cherrypy.response.headers['ETag'] = val - return val - - class HeaderElements(Test): - - def get_elements(self, headername): - e = cherrypy.request.headers.elements(headername) - return '\n'.join([six.text_type(x) for x in e]) - - class Method(Test): - - def index(self): - m = cherrypy.request.method - if m in defined_http_methods or m == 'CONNECT': - return m - - if m == 'LINK': - raise cherrypy.HTTPError(405) - else: - raise cherrypy.HTTPError(501) - - def parameterized(self, data): - return data - - def request_body(self): - # This should be a file object (temp file), - # which CP will just pipe back out if we tell it to. - return cherrypy.request.body - - def reachable(self): - return 'success' - - class Divorce(Test): - - """HTTP Method handlers shouldn't collide with normal method names. - For example, a GET-handler shouldn't collide with a method named - 'get'. - - If you build HTTP method dispatching into CherryPy, rewrite this - class to use your new dispatch mechanism and make sure that: - "GET /divorce HTTP/1.1" maps to divorce.index() and - "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() - """ - - documents = {} - - @cherrypy.expose - def index(self): - yield '<h1>Choose your document</h1>\n' - yield '<ul>\n' - for id, contents in self.documents.items(): - yield ( - " <li><a href='/divorce/get?ID=%s'>%s</a>:" - ' %s</li>\n' % (id, id, contents)) - yield '</ul>' - - @cherrypy.expose - def get(self, ID): - 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' - return existing - - appconf = { - '/method': { - 'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND', - 'PATCH') - }, - } - cherrypy.tree.mount(root, config=appconf) - - def test_scheme(self): - self.getPage('/scheme') - self.assertBody(self.scheme) - - 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) - ) - - self.getPage('/request_uuid4') - third_uuid4, _, _ = self.body.decode().partition(' ') - assert ( - uuid.UUID(first_uuid4, version=4) - != uuid.UUID(third_uuid4, version=4) - ) - - def testRelativeURIPathInfo(self): - self.getPage('/pathinfo/foo/bar') - self.assertBody('/pathinfo/foo/bar') - - def testAbsoluteURIPathInfo(self): - # http://cherrypy.org/ticket/1061 - self.getPage('http://localhost/pathinfo/foo/bar') - self.assertBody('/pathinfo/foo/bar') - - def testParams(self): - self.getPage('/params/?thing=a') - self.assertBody(repr(ntou('a'))) - - self.getPage('/params/?thing=a&thing=b&thing=c') - self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) - - # Test friendly error message when given params are not accepted. - cherrypy.config.update({'request.show_mismatched_params': True}) - self.getPage('/params/?notathing=meeting') - self.assertInBody('Missing parameters: thing') - self.getPage('/params/?thing=meeting¬athing=meeting') - self.assertInBody('Unexpected query string parameters: notathing') - - # Test ability to turn off friendly error messages - cherrypy.config.update({'request.show_mismatched_params': False}) - self.getPage('/params/?notathing=meeting') - self.assertInBody('Not Found') - self.getPage('/params/?thing=meeting¬athing=meeting') - self.assertInBody('Not Found') - - # 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'))])) - - # Make sure that encoded = and & get parsed correctly - self.getPage( - '/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2') - self.assertBody('args: %s kwargs: %s' % - (('code',), - [('url', ntou('http://cherrypy.org/index?a=1&b=2'))])) - - # Test coordinates sent by <img ismap> - self.getPage('/params/ismap?223,114') - self.assertBody('Coordinates: 223, 114') - - # 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'))])) - - 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', - ): - self.getPage(uri) - self.assertStatus(200) - - error_msgs = [ - 'Missing parameters', - 'Nothing matches the given URI', - 'Multiple values for parameters', - 'Unexpected query string parameters', - 'Unexpected body parameters', - 'Invalid path in Request-URI', - 'Illegal #fragment in Request-URI', - ] - - # uri should be tested for valid absolute path, the status must be 400. - for uri, error_idx in ( - ('invalid/path/without/leading/slash', 5), - ('/valid/path#invalid=fragment', 6), - ): - self.getPage(uri) - self.assertStatus(400) - self.assertInBody(error_msgs[error_idx]) - - # query string parameters are part of the URI, so if they are wrong - # for a particular handler, the status MUST be a 404. - for uri, msg in ( - ('/paramerrors/one_positional', error_msgs[0]), - ('/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/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/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}) - self.getPage(uri) - self.assertStatus(404) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody('Not Found') - - # 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]), - ): - for show_mismatched_params in (True, False): - cherrypy.config.update( - {'request.show_mismatched_params': show_mismatched_params}) - self.getPage(uri, method='POST', body=body) - self.assertStatus(400) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody('400 Bad') - - # 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]), - ): - for show_mismatched_params in (True, False): - cherrypy.config.update( - {'request.show_mismatched_params': show_mismatched_params}) - self.getPage(uri, method='POST', body=body) - self.assertStatus(404) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody('Not Found') - - # 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', - ): - self.getPage(uri, method='GET') - self.assertStatus(500) - self.assertTrue('Client Error', self.body) - - def testErrorHandling(self): - self.getPage('/error/missing') - self.assertStatus(404) - self.assertErrorPage(404, "The path '/error/missing' was not found.") - - ignore = helper.webtest.ignored_exceptions - ignore.append(ValueError) - try: - valerr = '\n raise ValueError()\nValueError' - self.getPage('/error/page_method') - self.assertErrorPage(500, pattern=valerr) - - 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)): - 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. - self.assertStatus(200) - self.assertBody('word up') - 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') - - # No traceback should be present - self.getPage('/error/cause_err_in_finalize') - msg = "Illegal response status from server ('ZOO' is non-numeric)." - self.assertErrorPage(500, msg, None) - finally: - ignore.pop() - - # Test HTTPError with a reason-phrase in the status arg. - self.getPage('/error/reason_phrase') - self.assertStatus("410 Gone fishin'") - - # Test custom error page for a specific error. - self.getPage('/error/custom') - self.assertStatus(404) - self.assertBody('Hello, world\r\n' + (' ' * 499)) - - # Test custom error page for a specific error. - self.getPage('/error/custom?err=401') - self.assertStatus(401) - self.assertBody( - 'Error 401 Unauthorized - ' - "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)) - - # Test error in custom error page (ticket #305). - # Note that the message is escaped for HTML (ticket #310). - self.getPage('/error/noexist') - self.assertStatus(404) - if sys.version_info >= (3, 3): - exc_name = 'FileNotFoundError' - else: - exc_name = 'IOError' - msg = ('No, <b>really</b>, not found!<br />' - 'In addition, the custom error page failed:\n<br />' - '%s: [Errno 2] ' - "No such file or directory: 'nonexistent.html'") % (exc_name,) - self.assertInBody(msg) - - if getattr(cherrypy.server, 'using_apache', False): - pass - else: - # Test throw_errors (ticket #186). - self.getPage('/error/rethrow') - self.assertInBody('raise ValueError()') - - def testExpect(self): - e = ('Expect', '100-continue') - self.getPage('/headerelements/get_elements?headername=Expect', [e]) - self.assertBody('100-continue') - - self.getPage('/expect/expectation_failed', [e]) - self.assertStatus(417) - - def testHeaderElements(self): - # Accept-* header elements should be sorted, with most preferred first. - 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') - - h = [ - ('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') - - # 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' - '*/*') - - # Test Accept-Charset - h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] - self.getPage( - '/headerelements/get_elements?headername=Accept-Charset', h) - self.assertStatus('200 OK') - self.assertBody('iso-8859-5\n' - 'unicode-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) - self.assertStatus('200 OK') - self.assertBody('gzip;q=1.0\n' - 'identity;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) - self.assertStatus('200 OK') - self.assertBody('da\n' - 'en-gb;q=0.8\n' - 'en;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.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.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']: - 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')) - - 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)]) - # 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.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10) - # Note: this is different output for Python3, but it decodes fine. - etag = self.assertHeader( - 'ETag', - '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '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.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.assertBody('application/json') - - def test_basic_HTTPMethods(self): - helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND', - 'PATCH') - - # Test that all defined HTTP methods work. - for m in defined_http_methods: - self.getPage('/method/', method=m) - - # HEAD requests should not return any body. - if m == 'HEAD': - self.assertBody('') - elif m == 'TRACE': - # Some HTTP servers (like modpy) have their own TRACE support - self.assertEqual(self.body[:5], b'TRACE') - else: - self.assertBody(m) - - # 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.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)))] - self.getPage('/method/request_body', headers=h, method='PATCH', body=b) - self.assertStatus(200) - self.assertBody(b) - - # Request a PATCH method with a file body but no Content-Type. - # See https://github.com/cherrypy/cherrypy/issues/790. - b = b'one thing on top of another' - self.persistent = True - try: - conn = self.HTTP_CONN - conn.putrequest('PATCH', '/method/request_body', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Length', str(len(b))) - conn.endheaders() - conn.send(b) - response = conn.response_class(conn.sock, method='PATCH') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(b) - finally: - self.persistent = False - - # Request a PATCH method with no body whatsoever (not an empty one). - # See https://github.com/cherrypy/cherrypy/issues/650. - # Provide a C-T or webtest will provide one (and a C-L) for us. - h = [('Content-Type', 'text/plain')] - self.getPage('/method/reachable', headers=h, method='PATCH') - self.assertStatus(411) - - # 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.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)))] - self.getPage('/method/request_body', headers=h, method='PUT', body=b) - self.assertStatus(200) - self.assertBody(b) - - # Request a PUT method with a file body but no Content-Type. - # See https://github.com/cherrypy/cherrypy/issues/790. - b = b'one thing on top of another' - self.persistent = True - try: - conn = self.HTTP_CONN - conn.putrequest('PUT', '/method/request_body', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Length', str(len(b))) - conn.endheaders() - conn.send(b) - response = conn.response_class(conn.sock, method='PUT') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(b) - finally: - self.persistent = False - - # Request a PUT method with no body whatsoever (not an empty one). - # See https://github.com/cherrypy/cherrypy/issues/650. - # Provide a C-T or webtest will provide one (and a C-L) for us. - h = [('Content-Type', 'text/plain')] - self.getPage('/method/reachable', headers=h, method='PUT') - self.assertStatus(411) - - # Request a custom method with a request body - b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n' - '<propfind xmlns="DAV:"><prop><getlastmodified/>' - '</prop></propfind>') - 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) - - # Request a disallowed method - self.getPage('/method/', method='LINK') - self.assertStatus(405) - - # Request an unknown method - self.getPage('/method/', method='SEARCH') - self.assertStatus(501) - - # For method dispatchers: make sure that an HTTP method doesn't - # collide with a virtual path atom. If you build HTTP-method - # dispatching into the core, rewrite these handlers to use - # your dispatch idioms. - self.getPage('/divorce/get?ID=13') - self.assertBody('Divorce document 13: empty') - self.assertStatus(200) - self.getPage('/divorce/', method='GET') - self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>') - self.assertStatus(200) - - def test_CONNECT_method(self): - self.persistent = True - try: - conn = self.HTTP_CONN - conn.request('CONNECT', 'created.example.com:3128') - response = conn.response_class(conn.sock, method='CONNECT') - response.begin() - self.assertEqual(response.status, 204) - finally: - self.persistent = False - - self.persistent = True - try: - conn = self.HTTP_CONN - conn.request('CONNECT', 'body.example.com:3128') - response = conn.response_class(conn.sock, method='CONNECT') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(b'CONNECTed to /body.example.com:3128') - finally: - 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']: - self.persistent = True - try: - conn = self.HTTP_CONN - conn.request('CONNECT', request_target) - response = conn.response_class(conn.sock, method='CONNECT') - 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.') - finally: - self.persistent = False - - def testEmptyThreadlocals(self): - results = [] - for x in range(20): - self.getPage('/threadlocal/') - results.append(self.body) - self.assertEqual(results, [b'None'] * 20) diff --git a/libraries/cherrypy/test/test_routes.py b/libraries/cherrypy/test/test_routes.py deleted file mode 100644 index cc714765..00000000 --- a/libraries/cherrypy/test/test_routes.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Test Routes dispatcher.""" -import os -import importlib - -import pytest - -import cherrypy -from cherrypy.test import helper - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -class RoutesDispatchTest(helper.CPWebCase): - """Routes dispatcher test suite.""" - - @staticmethod - def setup_server(): - """Set up cherrypy test instance.""" - try: - importlib.import_module('routes') - except ImportError: - 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'), - ], - }) - def index(self, **kwargs): - return 'Welcome to %s, pop. %s' % (self.name, self.population) - - def update(self, **kwargs): - self.population = kwargs['pop'] - 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'])) - d.connect('main', ':action', controller=Dummy()) - - conf = {'/': {'request.dispatch': d}} - cherrypy.tree.mount(root=None, config=conf) - - def test_Routes_Dispatch(self): - """Check that routes package based URI dispatching works correctly.""" - self.getPage('/hounslow') - self.assertStatus('200 OK') - self.assertBody('Welcome to Hounslow, pop. 10000') - - self.getPage('/foo') - self.assertStatus('404 Not Found') - - self.getPage('/surbiton') - self.assertStatus('200 OK') - self.assertBody('Welcome to Surbiton, pop. 10000') - - self.getPage('/surbiton', method='POST', body='pop=1327') - self.assertStatus('200 OK') - self.assertBody('OK') - self.getPage('/surbiton') - self.assertStatus('200 OK') - self.assertHeader('Content-Language', 'en-GB') - self.assertBody('Welcome to Surbiton, pop. 1327') diff --git a/libraries/cherrypy/test/test_session.py b/libraries/cherrypy/test/test_session.py deleted file mode 100644 index 0083c97c..00000000 --- a/libraries/cherrypy/test/test_session.py +++ /dev/null @@ -1,512 +0,0 @@ -import os -import threading -import time -import socket -import importlib - -from six.moves.http_client import HTTPConnection - -import pytest -from path import Path - -import cherrypy -from cherrypy._cpcompat import ( - json_decode, - HTTPSConnection, -) -from cherrypy.lib import sessions -from cherrypy.lib import reprconf -from cherrypy.lib.httputil import response_codes -from cherrypy.test import helper - -localDir = os.path.dirname(__file__) - - -def http_methods_allowed(methods=['GET', 'HEAD']): - method = cherrypy.request.method.upper() - if method not in methods: - cherrypy.response.headers['Allow'] = ', '.join(methods) - raise cherrypy.HTTPError(405) - - -cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed) - - -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), - }) - class Root: - - @cherrypy.expose - def clear(self): - cherrypy.session.cache.clear() - - @cherrypy.expose - def data(self): - cherrypy.session['aha'] = 'foo' - return repr(cherrypy.session._data) - - @cherrypy.expose - def testGen(self): - counter = cherrypy.session.get('counter', 0) + 1 - cherrypy.session['counter'] = counter - yield str(counter) - - @cherrypy.expose - def testStr(self): - counter = cherrypy.session.get('counter', 0) + 1 - cherrypy.session['counter'] = counter - return str(counter) - - @cherrypy.expose - @cherrypy.config(**{'tools.sessions.on': False}) - def set_session_cls(self, new_cls_name): - new_cls = reprconf.attributes(new_cls_name) - cfg = {'tools.sessions.storage_class': new_cls} - self.__class__._cp_config.update(cfg) - if hasattr(cherrypy, 'session'): - del cherrypy.session - if new_cls.clean_thread: - new_cls.clean_thread.stop() - new_cls.clean_thread.unsubscribe() - del new_cls.clean_thread - - @cherrypy.expose - def index(self): - sess = cherrypy.session - c = sess.get('counter', 0) + 1 - time.sleep(0.01) - sess['counter'] = c - return str(c) - - @cherrypy.expose - def keyin(self, key): - return str(key in cherrypy.session) - - @cherrypy.expose - def delete(self): - cherrypy.session.delete() - sessions.expire() - return 'done' - - @cherrypy.expose - def delkey(self, key): - del cherrypy.session[key] - return 'OK' - - @cherrypy.expose - def redir_target(self): - return self._cp_config['tools.sessions.storage_class'].__name__ - - @cherrypy.expose - def iredir(self): - raise cherrypy.InternalRedirect('/redir_target') - - @cherrypy.expose - @cherrypy.config(**{ - 'tools.allow.on': True, - 'tools.allow.methods': ['GET'], - }) - def restricted(self): - return cherrypy.request.method - - @cherrypy.expose - def regen(self): - cherrypy.tools.sessions.regenerate() - return 'logged in' - - @cherrypy.expose - 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, - }) - def session_cookie(self): - # Must load() to start the clean thread. - cherrypy.session.load() - return cherrypy.session.id - - cherrypy.tree.mount(Root()) - - -class SessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def tearDown(self): - # Clean up sessions. - for fname in os.listdir(localDir): - if fname.startswith(sessions.FileSession.SESSION_PREFIX): - path = Path(localDir) / fname - path.remove_p() - - @pytest.mark.xfail(reason='#1534') - def test_0_Session(self): - self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') - self.getPage('/clear') - - # Test that a normal request gets the same id in the cookies. - # Note: this wouldn't work if /data didn't load the session. - self.getPage('/data') - self.assertBody("{'aha': 'foo'}") - c = self.cookies[0] - self.getPage('/data', self.cookies) - self.assertEqual(self.cookies[0], c) - - self.getPage('/testStr') - self.assertBody('1') - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(';')]) - # Assert there is an 'expires' param - self.assertEqual(set(cookie_parts.keys()), - set(['session_id', 'expires', 'Path'])) - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/data', self.cookies) - self.assertDictEqual(json_decode(self.body), - {'counter': 3, 'aha': 'foo'}) - self.getPage('/length', self.cookies) - self.assertBody('2') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession') - self.getPage('/testStr') - self.assertBody('1') - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - # Wait for the session.timeout (1 second) - time.sleep(2) - self.getPage('/') - self.assertBody('1') - self.getPage('/length', self.cookies) - self.assertBody('1') - - # Test session __contains__ - self.getPage('/keyin?key=counter', self.cookies) - self.assertBody('True') - cookieset1 = self.cookies - - # Make a new session and test __len__ again - self.getPage('/') - self.getPage('/length', self.cookies) - self.assertBody('2') - - # Test session delete - self.getPage('/delete', self.cookies) - self.assertBody('done') - self.getPage('/delete', cookieset1) - self.assertBody('done') - - def f(): - return [ - x - for x in os.listdir(localDir) - if x.startswith('session-') - ] - self.assertEqual(f(), []) - - # Wait for the cleanup thread to delete remaining session files - self.getPage('/') - self.assertNotEqual(f(), []) - time.sleep(2) - self.assertEqual(f(), []) - - def test_1_Ram_Concurrency(self): - self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') - self._test_Concurrency() - - @pytest.mark.xfail(reason='#1306') - def test_2_File_Concurrency(self): - self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession') - self._test_Concurrency() - - def _test_Concurrency(self): - client_thread_count = 5 - request_count = 30 - - # Get initial cookie - self.getPage('/') - self.assertBody('1') - cookies = self.cookies - - data_dict = {} - errors = [] - - def request(index): - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - for i in range(request_count): - c.putrequest('GET', '/') - for k, v in cookies: - c.putheader(k, v) - c.endheaders() - response = c.getresponse() - body = response.read() - if response.status != 200 or not body.isdigit(): - errors.append((response.status, body)) - else: - data_dict[index] = max(data_dict[index], int(body)) - # Uncomment the following line to prove threads overlap. - # sys.stdout.write("%d " % index) - - # Start <request_count> requests from each of - # <client_thread_count> concurrent clients - ts = [] - for c in range(client_thread_count): - data_dict[c] = 0 - t = threading.Thread(target=request, args=(c,)) - ts.append(t) - t.start() - - for t in ts: - t.join() - - hitcount = max(data_dict.values()) - expected = 1 + (client_thread_count * request_count) - - for e in errors: - print(e) - self.assertEqual(hitcount, expected) - - def test_3_Redirect(self): - # Start a new session - self.getPage('/testStr') - self.getPage('/iredir', self.cookies) - self.assertBody('FileSession') - - def test_4_File_deletion(self): - # Start a new session - self.getPage('/testStr') - # Delete the session file manually and retry. - id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - path = os.path.join(localDir, 'session-' + id) - os.unlink(path) - self.getPage('/testStr', self.cookies) - - def test_5_Error_paths(self): - self.getPage('/unknown/page') - self.assertErrorPage(404, "The path '/unknown/page' was not found.") - - # Note: this path is *not* the same as above. The above - # takes a normal route through the session code; this one - # skips the session code's before_handler and only calls - # before_finalize (save) and on_end (close). So the session - # code has to survive calling save/close without init. - self.getPage('/restricted', self.cookies, method='POST') - self.assertErrorPage(405, response_codes[405][1]) - - 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('/regen') - self.assertBody('logged in') - id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - self.assertNotEqual(id1, id2) - - 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=/;')]) - id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - self.assertNotEqual(id1, id2) - self.assertNotEqual(id2, 'maliciousid') - - def test_7_session_cookies(self): - self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') - 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(';')]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - id1 = cookie_parts['temp'] - self.assertEqual(list(sessions.RamSession.cache), [id1]) - - # Send another request in the same "browser session". - self.getPage('/session_cookie', self.cookies) - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(';')]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - self.assertBody(id1) - self.assertEqual(list(sessions.RamSession.cache), [id1]) - - # 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(';')]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - # Assert a new id has been generated... - id2 = cookie_parts['temp'] - self.assertNotEqual(id1, id2) - self.assertEqual(set(sessions.RamSession.cache.keys()), - set([id1, id2])) - - # Wait for the session.timeout on both sessions - time.sleep(2.5) - cache = list(sessions.RamSession.cache) - if cache: - if cache == [id2]: - self.fail('The second session did not time out.') - else: - self.fail('Unknown session id in cache: %r', cache) - - def test_8_Ram_Cleanup(self): - def lock(): - s1 = sessions.RamSession() - s1.acquire_lock() - time.sleep(1) - s1.release_lock() - - t = threading.Thread(target=lock) - t.start() - start = time.time() - while not sessions.RamSession.locks and time.time() - start < 5: - time.sleep(0.01) - assert len(sessions.RamSession.locks) == 1, 'Lock not acquired' - s2 = sessions.RamSession() - s2.clean_up() - msg = 'Clean up should not remove active lock' - assert len(sessions.RamSession.locks) == 1, msg - t.join() - - -try: - importlib.import_module('memcache') - - host, port = '127.0.0.1', 11211 - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None - try: - s = socket.socket(af, socktype, proto) - # See http://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - raise - break -except (ImportError, socket.error): - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test(self): - return self.skip('memcached not reachable ') -else: - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_0_Session(self): - self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession') - - self.getPage('/testStr') - self.assertBody('1') - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/length', self.cookies) - self.assertErrorPage(500) - self.assertInBody('NotImplementedError') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - # Wait for the session.timeout (1 second) - time.sleep(1.25) - self.getPage('/') - self.assertBody('1') - - # Test session __contains__ - self.getPage('/keyin?key=counter', self.cookies) - self.assertBody('True') - - # Test session delete - self.getPage('/delete', self.cookies) - self.assertBody('done') - - def test_1_Concurrency(self): - client_thread_count = 5 - request_count = 30 - - # Get initial cookie - self.getPage('/') - self.assertBody('1') - cookies = self.cookies - - data_dict = {} - - def request(index): - for i in range(request_count): - self.getPage('/', cookies) - # Uncomment the following line to prove threads overlap. - # sys.stdout.write("%d " % index) - if not self.body.isdigit(): - self.fail(self.body) - data_dict[index] = int(self.body) - - # Start <request_count> concurrent requests from - # each of <client_thread_count> clients - ts = [] - for c in range(client_thread_count): - data_dict[c] = 0 - t = threading.Thread(target=request, args=(c,)) - ts.append(t) - t.start() - - for t in ts: - t.join() - - hitcount = max(data_dict.values()) - expected = 1 + (client_thread_count * request_count) - self.assertEqual(hitcount, expected) - - def test_3_Redirect(self): - # Start a new session - self.getPage('/testStr') - self.getPage('/iredir', self.cookies) - self.assertBody('memcached') - - def test_5_Error_paths(self): - self.getPage('/unknown/page') - self.assertErrorPage( - 404, "The path '/unknown/page' was not found.") - - # Note: this path is *not* the same as above. The above - # takes a normal route through the session code; this one - # skips the session code's before_handler and only calls - # before_finalize (save) and on_end (close). So the session - # code has to survive calling save/close without init. - self.getPage('/restricted', self.cookies, method='POST') - self.assertErrorPage(405, response_codes[405][1]) diff --git a/libraries/cherrypy/test/test_sessionauthenticate.py b/libraries/cherrypy/test/test_sessionauthenticate.py deleted file mode 100644 index 63053fcb..00000000 --- a/libraries/cherrypy/test/test_sessionauthenticate.py +++ /dev/null @@ -1,61 +0,0 @@ -import cherrypy -from cherrypy.test import helper - - -class SessionAuthenticateTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - - def check(username, password): - # Dummy check_username_and_password function - if username != 'test' or password != 'password': - return 'Wrong login/password' - - def augment_params(): - # A simple tool to add some things to request.params - # This is to check to make sure that session_auth can handle - # request params (ticket #780) - cherrypy.request.params['test'] = 'test' - - cherrypy.tools.augment_params = cherrypy.Tool( - 'before_handler', augment_params, None, priority=30) - - class Test: - - _cp_config = { - 'tools.sessions.on': True, - 'tools.session_auth.on': True, - 'tools.session_auth.check_username_and_password': check, - 'tools.augment_params.on': True, - } - - @cherrypy.expose - def index(self, **kwargs): - return 'Hi %s, you are logged in' % cherrypy.request.login - - cherrypy.tree.mount(Test()) - - def testSessionAuthenticate(self): - # request a page and check for login form - self.getPage('/') - self.assertInBody('<form method="post" action="do_login">') - - # setup credentials - login_body = 'username=test&password=password&from_page=/' - - # attempt a login - self.getPage('/do_login', method='POST', body=login_body) - self.assertStatus((302, 303)) - - # get the page now that we are logged in - self.getPage('/', self.cookies) - self.assertBody('Hi test, you are logged in') - - # do a logout - self.getPage('/do_logout', self.cookies, method='POST') - self.assertStatus((302, 303)) - - # verify we are logged out - self.getPage('/', self.cookies) - self.assertInBody('<form method="post" action="do_login">') diff --git a/libraries/cherrypy/test/test_states.py b/libraries/cherrypy/test/test_states.py deleted file mode 100644 index 606ca4f6..00000000 --- a/libraries/cherrypy/test/test_states.py +++ /dev/null @@ -1,473 +0,0 @@ -import os -import signal -import time -import unittest -import warnings - -from six.moves.http_client import BadStatusLine - -import pytest -import portend - -import cherrypy -import cherrypy.process.servers -from cherrypy.test import helper - -engine = cherrypy.engine -thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -class Dependency: - - def __init__(self, bus): - self.bus = bus - self.running = False - self.startcount = 0 - self.gracecount = 0 - self.threads = {} - - def subscribe(self): - self.bus.subscribe('start', self.start) - self.bus.subscribe('stop', self.stop) - self.bus.subscribe('graceful', self.graceful) - self.bus.subscribe('start_thread', self.startthread) - self.bus.subscribe('stop_thread', self.stopthread) - - def start(self): - self.running = True - self.startcount += 1 - - def stop(self): - self.running = False - - def graceful(self): - self.gracecount += 1 - - def startthread(self, thread_id): - self.threads[thread_id] = None - - def stopthread(self, thread_id): - del self.threads[thread_id] - - -db_connection = Dependency(engine) - - -def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'Hello World' - - @cherrypy.expose - def ctrlc(self): - raise KeyboardInterrupt() - - @cherrypy.expose - def graceful(self): - engine.graceful() - return 'app was (gracefully) restarted succesfully' - - cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'environment': 'test_suite', - }) - - db_connection.subscribe() - -# ------------ Enough helpers. Time for real live test cases. ------------ # - - -class ServerStateTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def setUp(self): - cherrypy.server.socket_timeout = 0.1 - self.do_gc_test = False - - def test_0_NormalStateFlow(self): - engine.stop() - # Our db_connection should not be running - self.assertEqual(db_connection.running, False) - self.assertEqual(db_connection.startcount, 1) - self.assertEqual(len(db_connection.threads), 0) - - # Test server start - engine.start() - self.assertEqual(engine.state, engine.states.STARTED) - - host = cherrypy.server.socket_host - port = cherrypy.server.socket_port - portend.occupied(host, port, timeout=0.1) - - # The db_connection should be running now - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.startcount, 2) - self.assertEqual(len(db_connection.threads), 0) - - self.getPage('/') - self.assertBody('Hello World') - self.assertEqual(len(db_connection.threads), 1) - - # Test engine stop. This will also stop the HTTP server. - engine.stop() - self.assertEqual(engine.state, engine.states.STOPPED) - - # Verify that our custom stop function was called - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - - # Block the main thread now and verify that exit() works. - def exittest(): - self.getPage('/') - self.assertBody('Hello World') - engine.exit() - cherrypy.server.start() - engine.start_with_callback(exittest) - engine.block() - self.assertEqual(engine.state, engine.states.EXITING) - - def test_1_Restart(self): - cherrypy.server.start() - engine.start() - - # The db_connection should be running now - self.assertEqual(db_connection.running, True) - grace = db_connection.gracecount - - self.getPage('/') - self.assertBody('Hello World') - self.assertEqual(len(db_connection.threads), 1) - - # Test server restart from this thread - engine.graceful() - self.assertEqual(engine.state, engine.states.STARTED) - self.getPage('/') - self.assertBody('Hello World') - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.gracecount, grace + 1) - self.assertEqual(len(db_connection.threads), 1) - - # Test server restart from inside a page handler - self.getPage('/graceful') - self.assertEqual(engine.state, engine.states.STARTED) - self.assertBody('app was (gracefully) restarted succesfully') - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.gracecount, grace + 2) - # Since we are requesting synchronously, is only one thread used? - # Note that the "/graceful" request has been flushed. - self.assertEqual(len(db_connection.threads), 0) - - engine.stop() - self.assertEqual(engine.state, engine.states.STOPPED) - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - - def test_2_KeyboardInterrupt(self): - # Raise a keyboard interrupt in the HTTP server's main thread. - # We must start the server in this, the main thread - engine.start() - cherrypy.server.start() - - self.persistent = True - try: - # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') - self.assertBody('Hello World') - self.assertNoHeader('Connection') - - cherrypy.server.httpserver.interrupt = KeyboardInterrupt - engine.block() - - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - self.assertEqual(engine.state, engine.states.EXITING) - finally: - self.persistent = False - - # Raise a keyboard interrupt in a page handler; on multithreaded - # servers, this should occur in one of the worker threads. - # This should raise a BadStatusLine error, since the worker - # thread will just die without writing a response. - engine.start() - cherrypy.server.start() - # From python3.5 a new exception is retuned when the connection - # ends abruptly: - # http.client.RemoteDisconnected - # RemoteDisconnected is a subclass of: - # (ConnectionResetError, http.client.BadStatusLine) - # and ConnectionResetError is an indirect subclass of: - # OSError - # From python 3.3 an up socket.error is an alias to OSError - # following PEP-3151, therefore http.client.RemoteDisconnected - # is considered a socket.error. - # - # raise_subcls specifies the classes that are not going - # to be considered as a socket.error for the retries. - # Given that RemoteDisconnected is part BadStatusLine - # we can use the same call for all py3 versions without - # sideffects. python < 3.5 will raise directly BadStatusLine - # which is not a subclass for socket.error/OSError. - try: - self.getPage('/ctrlc', raise_subcls=BadStatusLine) - except BadStatusLine: - pass - else: - print(self.body) - self.fail('AssertionError: BadStatusLine not raised') - - engine.block() - 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. - if engine.state != engine.states.EXITING: - engine.exit() - - # Start the demo script in a new process - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) - p.write_conf(extra='test_case_name: "test_4_Autoreload"') - p.start(imports='cherrypy.test._test_states_demo') - try: - self.getPage('/start') - start = float(self.body) - - # Give the autoreloader time to cache the file time. - time.sleep(2) - - # Touch the file - os.utime(os.path.join(thisdir, '_test_states_demo.py'), None) - - # Give the autoreloader time to re-exec the process - time.sleep(2) - host = cherrypy.server.socket_host - port = cherrypy.server.socket_port - portend.occupied(host, port, timeout=5) - - self.getPage('/start') - if not (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') - p.join() - - def test_5_Start_Error(self): - # If test_3 has not been executed, the server won't be stopped, - # so we'll have to do it. - if engine.state != engine.states.EXITING: - engine.exit() - - # 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.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: - self.fail('Process failed to return nonzero exit code.') - - -class PluginTests(helper.CPWebCase): - - def test_daemonize(self): - if os.name not in ['posix']: - return self.skip('skipped (not on posix) ') - self.HOST = '127.0.0.1' - self.PORT = 8081 - # 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.start(imports='cherrypy.test._test_states_demo') - try: - # Just get the pid of the daemonization process. - self.getPage('/pid') - self.assertStatus(200) - page_pid = int(self.body) - self.assertEqual(page_pid, p.get_pid()) - finally: - # Shut down the spawned process - self.getPage('/exit') - p.join() - - # Wait until here to test the exit code because we want to ensure - # that we wait for the daemon to finish running before we fail. - if p.exit_code != 0: - self.fail('Daemonized parent process failed to exit cleanly.') - - -class SignalHandlingTests(helper.CPWebCase): - - def test_SIGHUP_tty(self): - # When not daemonized, SIGHUP should shut down the server. - try: - from signal import SIGHUP - except ImportError: - return self.skip('skipped (no SIGHUP) ') - - # Spawn the process. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) - 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) - # This might hang if things aren't working right, but meh. - p.join() - - def test_SIGHUP_daemonized(self): - # When daemonized, SIGHUP should restart the server. - try: - from signal import SIGHUP - except ImportError: - return self.skip('skipped (no SIGHUP) ') - - if os.name not in ['posix']: - return self.skip('skipped (not on posix) ') - - # 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.start(imports='cherrypy.test._test_states_demo') - - pid = p.get_pid() - try: - # Send a SIGHUP - os.kill(pid, SIGHUP) - # Give the server some time to restart - time.sleep(2) - self.getPage('/pid') - self.assertStatus(200) - new_pid = int(self.body) - self.assertNotEqual(new_pid, pid) - finally: - # Shut down the spawned process - self.getPage('/exit') - p.join() - - def _require_signal_and_kill(self, signal_name): - if not hasattr(signal, signal_name): - self.skip('skipped (no %(signal_name)s)' % vars()) - - if not hasattr(os, 'kill'): - self.skip('skipped (no os.kill)') - - def test_SIGTERM(self): - '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.start(imports='cherrypy.test._test_states_demo') - # Send a SIGTERM - os.kill(p.get_pid(), signal.SIGTERM) - # This might hang if things aren't working right, but meh. - p.join() - - 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.start(imports='cherrypy.test._test_states_demo') - # Send a SIGTERM - os.kill(p.get_pid(), signal.SIGTERM) - # This might hang if things aren't working right, but meh. - p.join() - - def test_signal_handler_unsubscribe(self): - self._require_signal_and_kill('SIGTERM') - - # Although Windows has `os.kill` and SIGTERM is defined, the - # platform does not implement signals and sending SIGTERM - # will result in a forced termination of the process. - # Therefore, this test is not suitable for Windows. - if os.name == 'nt': - self.skip('SIGTERM not available') - - # Spawn a normal, undaemonized process. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) - 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) - # This might hang if things aren't working right, but meh. - 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 - ) - - -class WaitTests(unittest.TestCase): - - def test_safe_wait_INADDR_ANY(self): - """ - Wait on INADDR_ANY should not raise IOError - - In cases where the loopback interface does not exist, CherryPy cannot - effectively determine if a port binding to INADDR_ANY was effected. - In this situation, CherryPy should assume that it failed to detect - the binding (not that the binding failed) and only warn that it could - not verify it. - """ - # At such a time that CherryPy can reliably determine one or more - # viable IP addresses of the host, this test may be removed. - - # Simulate the behavior we observe when no loopback interface is - # present by: finding a port that's not occupied, then wait on it. - - free_port = portend.find_available_local_port() - - servers = cherrypy.process.servers - - inaddr_any = '0.0.0.0' - - # Wait on the free port that's unbound - with warnings.catch_warnings(record=True) as w: - with servers._safe_wait(inaddr_any, free_port): - portend.occupied(inaddr_any, free_port, timeout=1) - self.assertEqual(len(w), 1) - self.assertTrue(isinstance(w[0], warnings.WarningMessage)) - self.assertTrue( - 'Unable to verify that the server is bound on ' in str(w[0])) - - # The wait should still raise an IO error if INADDR_ANY was - # not supplied. - with pytest.raises(IOError): - with servers._safe_wait('127.0.0.1', free_port): - portend.occupied('127.0.0.1', free_port, timeout=1) diff --git a/libraries/cherrypy/test/test_static.py b/libraries/cherrypy/test/test_static.py deleted file mode 100644 index 5dc5a144..00000000 --- a/libraries/cherrypy/test/test_static.py +++ /dev/null @@ -1,434 +0,0 @@ -# -*- coding: utf-8 -*- -import contextlib -import io -import os -import sys -import platform -import tempfile - -from six import text_type as str -from six.moves import urllib -from six.moves.http_client import HTTPConnection - -import pytest -import py.path - -import cherrypy -from cherrypy.lib import static -from cherrypy._cpcompat import HTTPSConnection, ntou, tonative -from cherrypy.test import helper - - -@pytest.fixture -def unicode_filesystem(tmpdir): - filename = tmpdir / ntou('☃', 'utf-8') - tmpl = 'File system encoding ({encoding}) cannot support unicode filenames' - msg = tmpl.format(encoding=sys.getfilesystemencoding()) - try: - io.open(str(filename), 'w').close() - except UnicodeEncodeError: - pytest.skip(msg) - - -def ensure_unicode_filesystem(): - """ - TODO: replace with simply pytest fixtures once webtest.TestCase - no longer implies unittest. - """ - tmpdir = py.path.local(tempfile.mkdtemp()) - try: - unicode_filesystem(tmpdir) - finally: - tmpdir.remove() - - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -has_space_filepath = os.path.join(curdir, 'static', 'has space.html') -bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log') - -# 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 -BIGFILE_SIZE = 32 * MB - - -class StaticTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - if not os.path.exists(has_space_filepath): - 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 - ) - 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): - self.f = static.serve_file(bigfile_filepath) - return self.f - - @cherrypy.expose - def tell(self): - if self.f.input.closed: - return '' - return repr(self.f.input.tell()).rstrip('L') - - @cherrypy.expose - def fileobj(self): - f = open(os.path.join(curdir, 'style.css'), 'rb') - return static.serve_fileobj(f, content_type='text/css') - - @cherrypy.expose - def bytesio(self): - f = io.BytesIO(b'Fee\nfie\nfo\nfum') - return static.serve_fileobj(f, content_type='text/plain') - - class Static: - - @cherrypy.expose - def index(self): - return 'You want the Baron? You can have the Baron!' - - @cherrypy.expose - def dynamic(self): - return 'This is a DYNAMIC page' - - root = Root() - root.static = Static() - - rootconf = { - '/static': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }, - '/static-long': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': r'\\?\%s' % curdir, - }, - '/style.css': { - 'tools.staticfile.on': True, - 'tools.staticfile.filename': os.path.join(curdir, 'style.css'), - }, - '/docroot': { - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.index': 'index.html', - }, - '/error': { - 'tools.staticdir.on': True, - 'request.show_tracebacks': True, - }, - '/404test': { - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - 'error_page.404': error_page_404, - } - } - rootApp = cherrypy.Application(root) - rootApp.merge(rootconf) - - test_app_conf = { - '/test': { - 'tools.staticdir.index': 'index.html', - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - }, - } - testApp = cherrypy.Application(Static()) - testApp.merge(test_app_conf) - - vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) - cherrypy.tree.graft(vhost) - - @staticmethod - def teardown_server(): - for f in (has_space_filepath, bigfile_filepath): - if os.path.exists(f): - try: - os.unlink(f) - except Exception: - pass - - def test_static(self): - self.getPage('/static/index.html') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - # Using a staticdir.root value in a subdir... - self.getPage('/docroot/index.html') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - # Check a filename with spaces in it - self.getPage('/static/has%20space.html') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - self.getPage('/style.css') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css') - # Note: The body should be exactly 'Dummy stylesheet\n', but - # unfortunately some tools such as WinZip sometimes turn \n - # into \r\n on Windows when extracting the CherryPy tarball so - # we just check the content - self.assertMatchesBody('^Dummy stylesheet') - - @pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only') - def test_static_longpath(self): - """Test serving of a file in subdir of a Windows long-path - staticdir.""" - self.getPage('/static-long/static/index.html') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - def test_fallthrough(self): - # Test that NotFound will then try dynamic handlers (see [878]). - self.getPage('/static/dynamic') - self.assertBody('This is a DYNAMIC page') - - # Check a directory via fall-through to dynamic handler. - self.getPage('/static/') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('You want the Baron? You can have the Baron!') - - def test_index(self): - # Check a directory via "staticdir.index". - self.getPage('/docroot/') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - # The same page should be returned even if redirected. - self.getPage('/docroot') - self.assertStatus(301) - self.assertHeader('Location', '%s/docroot/' % self.base()) - self.assertMatchesBody( - "This resource .* <a href=(['\"])%s/docroot/\\1>" - '%s/docroot/</a>.' - % (self.base(), self.base()) - ) - - def test_config_errors(self): - # Check that we get an error if no .file or .dir - self.getPage('/error/thing.html') - self.assertErrorPage(500) - if sys.version_info >= (3, 3): - errmsg = ( - r'TypeError: staticdir\(\) missing 2 ' - 'required positional arguments' - ) - else: - errmsg = ( - r'TypeError: staticdir\(\) takes at least 2 ' - r'(positional )?arguments \(0 given\)' - ) - self.assertMatchesBody(errmsg.encode('ascii')) - - def test_security(self): - # Test up-level security - self.getPage('/static/../../test/style.css') - self.assertStatus((400, 403)) - - def test_modif(self): - # Test modified-since on a reasonably-large file - self.getPage('/static/dirback.jpg') - self.assertStatus('200 OK') - lastmod = '' - for k, v in self.headers: - if k == 'Last-Modified': - lastmod = v - ims = ('If-Modified-Since', lastmod) - self.getPage('/static/dirback.jpg', headers=[ims]) - self.assertStatus(304) - self.assertNoHeader('Content-Type') - self.assertNoHeader('Content-Length') - self.assertNoHeader('Content-Disposition') - self.assertBody('') - - def test_755_vhost(self): - self.getPage('/test/', [('Host', 'virt.net')]) - self.assertStatus(200) - self.getPage('/test', [('Host', 'virt.net')]) - self.assertStatus(301) - self.assertHeader('Location', self.scheme + '://virt.net/test/') - - def test_serve_fileobj(self): - self.getPage('/fileobj') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css;charset=utf-8') - self.assertMatchesBody('^Dummy stylesheet') - - def test_serve_bytesio(self): - self.getPage('/bytesio') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - self.assertHeader('Content-Length', 14) - self.assertMatchesBody('Fee\nfie\nfo\nfum') - - @pytest.mark.xfail(reason='#1475') - def test_file_stream(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Make an initial request - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/bigfile', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - - body = b'' - remaining = BIGFILE_SIZE - while remaining > 0: - data = response.fp.read(65536) - if not data: - break - body += data - remaining -= len(data) - - if self.scheme == 'https': - newconn = HTTPSConnection - else: - newconn = HTTPConnection - s, h, b = helper.webtest.openURL( - 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 - else: - tell_position = int(b) - - read_so_far = len(body) - - # It is difficult for us to force the server to only read - # the bytes that we ask for - there are going to be buffers - # inbetween. - # - # CherryPy will attempt to write as much data as it can to - # the socket, and we don't have a way to determine what that - # size will be. So we make the following assumption - by - # the time we have read in the entire file on the server, - # we will have at least received half of it. If this is not - # the case, then this is an indicator that either: - # - machines that are running this test are using buffer - # sizes greater than half of BIGFILE_SIZE; or - # - streaming is broken. - # - # At the time of writing, we seem to have encountered - # buffer sizes bigger than 512K, so we've increased - # BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB. - # This test is going to keep failing according to the - # improvements in hardware and OS buffers. - if tell_position >= BIGFILE_SIZE: - if read_so_far < (BIGFILE_SIZE / 2): - self.fail( - '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) - 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)) - - if body != b'x' * BIGFILE_SIZE: - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (BIGFILE_SIZE, body[:50], len(body))) - conn.close() - - def test_file_stream_deadlock(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Make an initial request but abort early. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/bigfile', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - 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))) - response.close() - conn.close() - - # Make a second request, which should fetch the whole file. - 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))) - - def test_error_page_with_serve_file(self): - self.getPage('/404test/yunyeen') - self.assertStatus(404) - self.assertInBody("I couldn't find that thing") - - def test_null_bytes(self): - self.getPage('/static/\x00') - self.assertStatus('404 Not Found') - - @staticmethod - @contextlib.contextmanager - def unicode_file(): - filename = ntou('Слава Україні.html', 'utf-8') - filepath = os.path.join(curdir, 'static', filename) - with io.open(filepath, 'w', encoding='utf-8') as strm: - strm.write(ntou('Героям Слава!', 'utf-8')) - try: - yield - finally: - os.remove(filepath) - - py27_on_windows = ( - platform.system() == 'Windows' and - sys.version_info < (3,) - ) - @pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301 - def test_unicode(self): - ensure_unicode_filesystem() - with self.unicode_file(): - url = ntou('/static/Слава Україні.html', 'utf-8') - # quote function requires str - url = tonative(url, 'utf-8') - url = urllib.parse.quote(url) - self.getPage(url) - - expected = ntou('Героям Слава!', 'utf-8') - self.assertInBody(expected) - - -def error_page_404(status, message, traceback, version): - path = os.path.join(curdir, 'static', '404.html') - return static.serve_file(path, content_type='text/html') diff --git a/libraries/cherrypy/test/test_tools.py b/libraries/cherrypy/test/test_tools.py deleted file mode 100644 index a73a3898..00000000 --- a/libraries/cherrypy/test/test_tools.py +++ /dev/null @@ -1,468 +0,0 @@ -"""Test the various means of instantiating and invoking tools.""" - -import gzip -import io -import sys -import time -import types -import unittest -import operator - -import six -from six.moves import range, map -from six.moves.http_client import IncompleteRead - -import cherrypy -from cherrypy import tools -from cherrypy._cpcompat import ntou -from cherrypy.test import helper, _test_decorators - - -timeout = 0.2 -europoundUnicode = ntou('\x80\xa3') - - -# Client-side code # - - -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) - - def numerify(): - def number_it(body): - for chunk in 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(six.iteritems(m)) - cherrypy.request.hooks.attach('on_start_resource', makemap) - - def critical(): - cherrypy.request.error_response = cherrypy.HTTPError( - 502).set_response - critical.failsafe = True - - cherrypy.request.hooks.attach('on_start_resource', critical) - cherrypy.request.hooks.attach(self._point, self.callable) - - tools.numerify = NumTool('before_finalize', numerify) - - # It's not mandatory to inherit from cherrypy.Tool. - class NadsatTool: - - def __init__(self): - self.ended = {} - self._name = 'nadsat' - - def nadsat(self): - def nadsat_it_up(body): - for chunk in 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): - # This runs after the request has been completely written out. - cherrypy.response.body = [b'razdrez'] - 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(): - cherrypy.request.process_request_body = False - clen = int(cherrypy.request.headers['Content-Length']) - cherrypy.request.body = cherrypy.request.rfile.read(clen) - - # 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() - if six.PY3: - r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] - else: - r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] - cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) - - def stream_handler(next_handler, *args, **kwargs): - actual = cherrypy.request.config.get('tools.streamer.arg') - assert actual == 'arg value' - cherrypy.response.output = o = io.BytesIO() - try: - next_handler(*args, **kwargs) - # Ignore the response and return our accumulated output - # instead. - return o.getvalue() - finally: - o.close() - cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool( - 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', - }) - def tarfile(self): - actual = cherrypy.request.config.get('tools.streamer.arg') - assert actual == 'arg value' - cherrypy.response.output.write(b'I am ') - cherrypy.response.output.write(b'a tarfile') - - @cherrypy.expose - def euro(self): - hooks = list(cherrypy.request.hooks['before_finalize']) - hooks.sort() - cbnames = [x.callback.__name__ for x in hooks] - assert cbnames == ['gzip'], cbnames - priorities = [x.priority for x in hooks] - assert priorities == [80], priorities - yield ntou('Hello,') - yield ntou('world') - yield europoundUnicode - - # Bare hooks - @cherrypy.expose - @cherrypy.config(**{'hooks.before_request_body': pipe_body}) - def pipe(self): - return cherrypy.request.body - - # Multiple decorators; include kwargs just for fun. - # Note that rotator must run before gzip. - @cherrypy.expose - 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) - - root = Root() - - class TestType(type): - """Metaclass which automatically exposes all functions in each - 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 six.itervalues(dct): - 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' - - def ended(self, id): - return repr(tools.nadsat.ended[id]) - - def err(self, id=None): - raise ValueError() - - def errinstream(self, id=None): - yield 'nonconfidential' - raise ValueError() - yield 'confidential' - - # METHOD TWO: decorator using Tool() - # We support Python 2.3, but the @-deco syntax would look like - # this: - # @tools.check_access() - def restricted(self): - return 'Welcome!' - restricted = myauthtools.check_access()(restricted) - userid = restricted - - def err_in_onstart(self): - return 'success!' - - @cherrypy.config(**{'response.stream': True}) - def stream(self, id=None): - for x in range(100000000): - yield str(x) - - conf = { - # METHOD THREE: - # Declare Tools in detached config - '/demo': { - 'tools.numerify.on': True, - 'tools.numerify.map': {b'pie': b'3.14159'}, - }, - '/demo/restricted': { - 'request.show_tracebacks': False, - }, - '/demo/userid': { - 'request.show_tracebacks': False, - 'myauth.check_access.default': True, - }, - '/demo/errinstream': { - 'response.stream': True, - }, - '/demo/err_in_onstart': { - # Because this isn't a dict, on_start_resource will error. - 'tools.numerify.map': 'pie->3.14159' - }, - # Combined tools - '/euro': { - 'tools.gzip.on': True, - 'tools.encode.on': True, - }, - # Priority specified in config - '/decorated_euro/subpath': { - 'tools.gzip.priority': 10, - }, - # Handler wrappers - '/tarfile': {'tools.streamer.on': True} - } - app = cherrypy.tree.mount(root, config=conf) - app.request_class.namespaces['myauth'] = myauthtools - - root.tooldecs = _test_decorators.ToolExamples() - - def testHookErrors(self): - self.getPage('/demo/?id=1') - # If body is "razdrez", then on_end_request is being called too early. - self.assertBody('A horrorshow lomtick of cherry 3.14159') - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage('/demo/ended/1') - self.assertBody('True') - - valerr = '\n raise ValueError()\nValueError' - self.getPage('/demo/err?id=3') - # If body is "razdrez", then on_end_request is being called too early. - self.assertErrorPage(502, pattern=valerr) - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage('/demo/ended/3') - 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)): - 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. - self.assertStatus('200 OK') - self.assertBody('nonconfidential') - else: - # 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') - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage('/demo/ended/5') - self.assertBody('True') - - # Test the "__call__" technique (compile-time decorator). - self.getPage('/demo/restricted') - self.assertErrorPage(401) - - # Test compile-time decorator with kwargs from config. - self.getPage('/demo/userid') - self.assertBody('Welcome!') - - def testEndRequestOnDrop(self): - old_timeout = None - try: - httpserver = cherrypy.server.httpserver - old_timeout = httpserver.timeout - except (AttributeError, IndexError): - return self.skip() - - try: - httpserver.timeout = timeout - - # Test that on_end_request is called even if the client drops. - self.persistent = True - try: - conn = self.HTTP_CONN - conn.putrequest('GET', '/demo/stream?id=9', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - # Skip the rest of the request and close the conn. This will - # cause the server's active socket to error, which *should* - # result in the request being aborted, and request.close being - # called all the way up the stack (including WSGI middleware), - # eventually calling our on_end_request hook. - finally: - self.persistent = False - time.sleep(timeout * 2) - # Test that the on_end_request hook was called. - self.getPage('/demo/ended/9') - self.assertBody('True') - finally: - if old_timeout is not None: - httpserver.timeout = old_timeout - - def testGuaranteedHooks(self): - # The 'critical' on_start_resource hook is 'failsafe' (guaranteed - # to run even if there are failures in other on_start methods). - # This is NOT true of the other hooks. - # Here, we have set up a failure in NumerifyTool.numerify_map, - # but our 'critical' hook should run and set the error to 502. - self.getPage('/demo/err_in_onstart') - self.assertErrorPage(502) - tmpl = "AttributeError: 'str' object has no attribute '{attr}'" - expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems') - self.assertInBody(expected_msg) - - def testCombinedTools(self): - 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.assertInBody(zbuf.getvalue()[:3]) - - zbuf = io.BytesIO() - zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6) - zfile.write(expectedResult) - zfile.close() - - self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')]) - self.assertInBody(zbuf.getvalue()[:3]) - - # This returns a different value because gzip's priority was - # 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')]) - if six.PY3: - self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) - else: - self.assertInBody(''.join([chr((ord(x) + 3) % 256) - for x in zbuf.getvalue()])) - - 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.assertBody(content) - - def testHandlerWrapperTool(self): - self.getPage('/tarfile') - self.assertBody('I am a tarfile') - - def testToolWithConfig(self): - if not sys.version_info >= (2, 5): - return self.skip('skipped (Python 2.5+ only)') - - self.getPage('/tooldecs/blah') - self.assertHeader('Content-Type', 'application/data') - - def testWarnToolOn(self): - # get - try: - cherrypy.tools.numerify.on - except AttributeError: - pass - else: - raise AssertionError('Tool.on did not error as it should have.') - - # set - try: - cherrypy.tools.numerify.on = True - except AttributeError: - pass - else: - raise AssertionError('Tool.on did not error as it should have.') - - 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, - ) - def example(): - pass - self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool)) - self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize') - self.assertEqual(cherrypy.tools.renamed._name, 'renamed') - self.assertEqual(cherrypy.tools.renamed._priority, 60) - - -class SessionAuthTest(unittest.TestCase): - - def test_login_screen_returns_bytes(self): - """ - login_screen must return bytes even if unicode parameters are passed. - Issue 1132 revealed that login_screen would return unicode if the - username and password were unicode. - """ - sa = cherrypy.lib.cptools.SessionAuth() - res = sa.login_screen(None, username=six.text_type('nobody'), - password=six.text_type('anypass')) - self.assertTrue(isinstance(res, bytes)) - - -class TestHooks: - def test_priorities(self): - """ - Hooks should sort by priority order. - """ - Hook = cherrypy._cprequest.Hook - hooks = [ - Hook(None, priority=48), - Hook(None), - Hook(None, priority=49), - ] - hooks.sort() - by_priority = operator.attrgetter('priority') - priorities = list(map(by_priority, hooks)) - assert priorities == [48, 49, 50] diff --git a/libraries/cherrypy/test/test_tutorials.py b/libraries/cherrypy/test/test_tutorials.py deleted file mode 100644 index efa35b99..00000000 --- a/libraries/cherrypy/test/test_tutorials.py +++ /dev/null @@ -1,210 +0,0 @@ -import sys -import imp -import types -import importlib - -import six - -import cherrypy -from cherrypy.test import helper - - -class TutorialTest(helper.CPWebCase): - - @classmethod - def setup_server(cls): - """ - Mount something so the engine starts. - """ - class Dummy: - pass - cherrypy.tree.mount(Dummy()) - - @staticmethod - def load_module(name): - """ - Import or reload tutorial module as needed. - """ - target = 'cherrypy.tutorial.' + name - if target in sys.modules: - module = imp.reload(sys.modules[target]) - else: - module = importlib.import_module(target) - return module - - @classmethod - def setup_tutorial(cls, name, root_name, config={}): - cherrypy.config.reset() - module = cls.load_module(name) - root = getattr(module, root_name) - conf = getattr(module, 'tutconf') - class_types = type, - if six.PY2: - class_types += types.ClassType, - if isinstance(root, class_types): - root = root() - cherrypy.tree.mount(root, config=conf) - cherrypy.config.update(config) - - def test01HelloWorld(self): - self.setup_tutorial('tut01_helloworld', 'HelloWorld') - self.getPage('/') - self.assertBody('Hello world!') - - def test02ExposeMethods(self): - self.setup_tutorial('tut02_expose_methods', 'HelloWorld') - self.getPage('/show_msg') - self.assertBody('Hello world!') - - def test03GetAndPost(self): - self.setup_tutorial('tut03_get_and_post', 'WelcomePage') - - # Try different GET queries - self.getPage('/greetUser?name=Bob') - self.assertBody("Hey Bob, what's up?") - - self.getPage('/greetUser') - self.assertBody('Please enter your name <a href="./">here</a>.') - - self.getPage('/greetUser?name=') - self.assertBody('No, really, enter your name <a href="./">here</a>.') - - # Try the same with POST - self.getPage('/greetUser', method='POST', body='name=Bob') - self.assertBody("Hey Bob, what's up?") - - self.getPage('/greetUser', method='POST', body='name=') - self.assertBody('No, really, enter your name <a href="./">here</a>.') - - def test04ComplexSite(self): - self.setup_tutorial('tut04_complex_site', 'root') - - msg = ''' - <p>Here are some extra useful links:</p> - - <ul> - <li><a href="http://del.icio.us">del.icio.us</a></li> - <li><a href="http://www.cherrypy.org">CherryPy</a></li> - </ul> - - <p>[<a href="../">Return to links page</a>]</p>''' - self.getPage('/links/extra/') - self.assertBody(msg) - - def test05DerivedObjects(self): - self.setup_tutorial('tut05_derived_objects', 'HomePage') - msg = ''' - <html> - <head> - <title>Another Page</title> - <head> - <body> - <h2>Another Page</h2> - - <p> - And this is the amazing second page! - </p> - - </body> - </html> - ''' - # the tutorial has some annoying spaces in otherwise blank lines - msg = msg.replace('</h2>\n\n', '</h2>\n \n') - msg = msg.replace('</p>\n\n', '</p>\n \n') - self.getPage('/another/') - self.assertBody(msg) - - def test06DefaultMethod(self): - self.setup_tutorial('tut06_default_method', 'UsersPage') - self.getPage('/hendrik') - self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' - '(<a href="./">back</a>)') - - def test07Sessions(self): - self.setup_tutorial('tut07_sessions', 'HitCounter') - - self.getPage('/') - self.assertBody( - "\n During your current session, you've viewed this" - '\n page 1 times! Your life is a patio of fun!' - '\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 ') - - def test08GeneratorsAndYield(self): - self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo') - self.getPage('/') - self.assertBody('<html><body><h2>Generators rule!</h2>' - '<h3>List of users:</h3>' - 'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>' - '</body></html>') - - 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') - b += 'a' * filesize + '\n' + '--x--\n' - self.getPage('/upload', h, 'POST', b) - self.assertBody('''<html> - <body> - myFile length: %d<br /> - myFile filename: hello.txt<br /> - myFile mime-type: text/plain - </body> - </html>''' % 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.assertEqual(len(self.body), 85698) - - def test10HTTPErrors(self): - self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo') - - @cherrypy.expose - def traceback_setting(): - return repr(cherrypy.request.show_tracebacks) - cherrypy.tree.mount(traceback_setting, '/traceback_setting') - - self.getPage('/') - self.assertInBody("""<a href="toggleTracebacks">""") - self.assertInBody("""<a href="/doesNotExist">""") - self.assertInBody("""<a href="/error?code=403">""") - self.assertInBody("""<a href="/error?code=500">""") - self.assertInBody("""<a href="/messageArg">""") - - self.getPage('/traceback_setting') - setting = self.body - self.getPage('/toggleTracebacks') - self.assertStatus((302, 303)) - self.getPage('/traceback_setting') - self.assertBody(str(not eval(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.getPage('/error?code=403') - self.assertStatus(403) - self.assertInBody("<h2>You can't do that!</h2>") - - self.getPage('/messageArg') - self.assertStatus(500) - self.assertInBody("If you construct an HTTPError with a 'message'") diff --git a/libraries/cherrypy/test/test_virtualhost.py b/libraries/cherrypy/test/test_virtualhost.py deleted file mode 100644 index de88f927..00000000 --- a/libraries/cherrypy/test/test_virtualhost.py +++ /dev/null @@ -1,113 +0,0 @@ -import os - -import cherrypy -from cherrypy.test import helper - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -class VirtualHostTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'Hello, world' - - @cherrypy.expose - def dom4(self): - return 'Under construction' - - @cherrypy.expose - def method(self, value): - return 'You sent %s' % value - - class VHost: - - def __init__(self, sitename): - self.sitename = sitename - - @cherrypy.expose - def index(self): - return 'Welcome to %s' % self.sitename - - @cherrypy.expose - def vmethod(self, value): - return 'You sent %s' % value - - @cherrypy.expose - def url(self): - return cherrypy.url('nextpage') - - # Test static as a handler (section must NOT include vhost prefix) - static = cherrypy.tools.staticdir.handler( - 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', - }, - }) - - def testVirtualHost(self): - self.getPage('/', [('Host', 'www.mydom1.com')]) - self.assertBody('Hello, world') - self.getPage('/mydom2/', [('Host', 'www.mydom1.com')]) - self.assertBody('Welcome to Domain 2') - - self.getPage('/', [('Host', 'www.mydom2.com')]) - self.assertBody('Welcome to Domain 2') - self.getPage('/', [('Host', 'www.mydom3.com')]) - self.assertBody('Welcome to Domain 3') - self.getPage('/', [('Host', 'www.mydom4.com')]) - self.assertBody('Under construction') - - # Test GET, POST, and positional params - self.getPage('/method?value=root') - 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.assertBody('You sent dom3 POST') - self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')]) - self.assertBody('You sent pos') - - # Test that cherrypy.url uses the browser url, not the virtual url - self.getPage('/url', [('Host', 'www.mydom2.com')]) - self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme) - - def test_VHost_plus_Static(self): - # Test static as a handler - self.getPage('/static/style.css', [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css;charset=utf-8') - - # Test static in config - self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg']) - - # Test static config with "index" arg - self.getPage('/static2/', [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertBody('Hello, world\r\n') - # Since tools.trailing_slash is on by default, this should redirect - self.getPage('/static2', [('Host', 'www.mydom2.com')]) - self.assertStatus(301) diff --git a/libraries/cherrypy/test/test_wsgi_ns.py b/libraries/cherrypy/test/test_wsgi_ns.py deleted file mode 100644 index 3545724c..00000000 --- a/libraries/cherrypy/test/test_wsgi_ns.py +++ /dev/null @@ -1,93 +0,0 @@ -import cherrypy -from cherrypy.test import helper - - -class WSGI_Namespace_Test(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class WSGIResponse(object): - - def __init__(self, appresults): - self.appresults = appresults - self.iter = iter(appresults) - - def __iter__(self): - return self - - def next(self): - return self.iter.next() - - def __next__(self): - return next(self.iter) - - def close(self): - if hasattr(self.appresults, 'close'): - self.appresults.close() - - class ChangeCase(object): - - def __init__(self, app, to=None): - self.app = app - self.to = to - - 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 - - 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(): - line = line.replace(k, v) - return line - - def __next__(this): - line = next(this.iter) - 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'}, - } - - app = cherrypy.Application(Root()) - app.wsgiapp.pipeline.append(('changecase', ChangeCase)) - app.wsgiapp.config['changecase'] = {'to': 'upper'} - cherrypy.tree.mount(app, config={'/': root_conf}) - - def test_pipeline(self): - if not cherrypy.server.httpserver: - return self.skip() - - self.getPage('/') - # If body is "HEXXO WORXD!", the middleware was applied out of order. - self.assertBody('HERRO WORRD!') diff --git a/libraries/cherrypy/test/test_wsgi_unix_socket.py b/libraries/cherrypy/test/test_wsgi_unix_socket.py deleted file mode 100644 index 8f1cc00b..00000000 --- a/libraries/cherrypy/test/test_wsgi_unix_socket.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import socket -import atexit -import tempfile - -from six.moves.http_client import HTTPConnection - -import pytest - -import cherrypy -from cherrypy.test import helper - - -def usocket_path(): - fd, path = tempfile.mkstemp('cp_test.sock') - os.close(fd) - os.remove(path) - return path - - -USOCKET_PATH = usocket_path() - - -class USocketHTTPConnection(HTTPConnection): - """ - HTTPConnection over a unix socket. - """ - - def __init__(self, path): - HTTPConnection.__init__(self, 'localhost') - self.path = path - - def __call__(self, *args, **kwargs): - """ - Catch-all method just to present itself as a constructor for the - HTTPConnection. - """ - return self - - def connect(self): - """ - Override the connect method and assign a unix socket as a transport. - """ - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock - atexit.register(lambda: os.remove(self.path)) - - -@pytest.mark.skipif("sys.platform == 'win32'") -class WSGI_UnixSocket_Test(helper.CPWebCase): - """ - Test basic behavior on a cherrypy wsgi server listening - on a unix socket. - - 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' - - @cherrypy.expose - def error(self): - raise Exception('Invalid page') - - config = { - 'server.socket_file': USOCKET_PATH - } - cherrypy.config.update(config) - cherrypy.tree.mount(Root()) - - def tearDown(self): - cherrypy.config.update({'server.socket_file': None}) - - def test_simple_request(self): - self.getPage('/') - self.assertStatus('200 OK') - self.assertInBody('Test OK') - - def test_not_found(self): - self.getPage('/invalid_path') - self.assertStatus('404 Not Found') - - def test_internal_error(self): - self.getPage('/error') - self.assertStatus('500 Internal Server Error') - self.assertInBody('Invalid page') diff --git a/libraries/cherrypy/test/test_wsgi_vhost.py b/libraries/cherrypy/test/test_wsgi_vhost.py deleted file mode 100644 index 2b6e5ba9..00000000 --- a/libraries/cherrypy/test/test_wsgi_vhost.py +++ /dev/null @@ -1,35 +0,0 @@ -import cherrypy -from cherrypy.test import helper - - -class WSGI_VirtualHost_Test(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class ClassOfRoot(object): - - def __init__(self, name): - self.name = name - - @cherrypy.expose - def index(self): - return 'Welcome to the %s website!' % self.name - - default = cherrypy.Application(None) - - domains = {} - for year in range(1997, 2008): - app = cherrypy.Application(ClassOfRoot('Class of %s' % year)) - domains['www.classof%s.example' % year] = app - - cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains)) - - def test_welcome(self): - if not cherrypy.server.using_wsgi: - return self.skip('skipped (not using WSGI)... ') - - for year in range(1997, 2008): - self.getPage( - '/', headers=[('Host', 'www.classof%s.example' % year)]) - self.assertBody('Welcome to the Class of %s website!' % year) diff --git a/libraries/cherrypy/test/test_wsgiapps.py b/libraries/cherrypy/test/test_wsgiapps.py deleted file mode 100644 index 1b3bf28f..00000000 --- a/libraries/cherrypy/test/test_wsgiapps.py +++ /dev/null @@ -1,120 +0,0 @@ -import sys - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.test import helper - - -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'] - keys = list(environ.keys()) - keys.sort() - for k in keys: - output.append('%s: %s\n' % (k, environ[k])) - return [ntob(x, 'utf-8') for x in output] - - def test_empty_string_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type', 'text/plain')] - start_response(status, response_headers) - return [ - b'Hello', b'', b' ', b'', b'world', - ] - - class WSGIResponse(object): - - def __init__(self, appresults): - self.appresults = appresults - self.iter = iter(appresults) - - 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() - - def close(self): - if hasattr(self.appresults, 'close'): - self.appresults.close() - - class ReversingMiddleware(object): - - def __init__(self, app): - self.app = app - - 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() - return ''.join(line) - - return Reverser(results) - - class Root: - - @cherrypy.expose - def index(self): - return ntob("I'm a regular CherryPy page handler!") - - cherrypy.tree.mount(Root()) - - cherrypy.tree.graft(test_app, '/hosted/app1') - cherrypy.tree.graft(test_empty_string_app, '/hosted/app3') - - # Set script_name explicitly to None to signal CP that it should - # be pulled from the WSGI environ each time. - 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!''' - - def test_01_standard_app(self): - self.getPage('/') - self.assertBody("I'm a regular CherryPy page handler!") - - def test_04_pure_wsgi(self): - if not cherrypy.server.using_wsgi: - return self.skip('skipped (not using WSGI)... ') - self.getPage('/hosted/app1') - self.assertHeader('Content-Type', 'text/plain') - self.assertInBody(self.wsgi_output) - - def test_05_wrapped_cp_app(self): - if not cherrypy.server.using_wsgi: - return self.skip('skipped (not using WSGI)... ') - self.getPage('/hosted/app2/') - body = list("I'm a regular CherryPy page handler!") - body.reverse() - body = ''.join(body) - self.assertInBody(body) - - def test_06_empty_string_app(self): - if not cherrypy.server.using_wsgi: - return self.skip('skipped (not using WSGI)... ') - self.getPage('/hosted/app3') - self.assertHeader('Content-Type', 'text/plain') - self.assertInBody('Hello world') diff --git a/libraries/cherrypy/test/test_xmlrpc.py b/libraries/cherrypy/test/test_xmlrpc.py deleted file mode 100644 index ad93b821..00000000 --- a/libraries/cherrypy/test/test_xmlrpc.py +++ /dev/null @@ -1,183 +0,0 @@ -import sys - -import six - -from six.moves.xmlrpc_client import ( - DateTime, Fault, - ProtocolError, ServerProxy, SafeTransport -) - -import cherrypy -from cherrypy import _cptools -from cherrypy.test import helper - -if six.PY3: - HTTPSTransport = SafeTransport - - # Python 3.0's SafeTransport still mistakenly checks for socket.ssl - import socket - if not hasattr(socket, 'ssl'): - socket.ssl = True -else: - class HTTPSTransport(SafeTransport): - - """Subclass of SafeTransport to fix sock.recv errors (by using file). - """ - - def request(self, host, handler, request_body, verbose=0): - # issue XML-RPC request - h = self.make_connection(host) - if verbose: - h.set_debuglevel(1) - - self.send_request(h, handler, request_body) - self.send_host(h, host) - self.send_user_agent(h) - self.send_content(h, request_body) - - errcode, errmsg, headers = h.getreply() - if errcode != 200: - raise ProtocolError(host + handler, errcode, errmsg, headers) - - self.verbose = verbose - - # Here's where we differ from the superclass. It says: - # try: - # sock = h._conn.sock - # except AttributeError: - # sock = None - # return self._parse_response(h.getfile(), sock) - - return self.parse_response(h.getfile()) - - -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!' - - @cherrypy.expose - def return_single_item_list(self): - return [42] - - @cherrypy.expose - def return_string(self): - return 'here is a string' - - @cherrypy.expose - def return_tuple(self): - return ('here', 'is', 1, 'tuple') - - @cherrypy.expose - def return_dict(self): - return dict(a=1, b=2, c=3) - - @cherrypy.expose - def return_composite(self): - return dict(a=1, z=26), 'hi', ['welcome', 'friend'] - - @cherrypy.expose - def return_int(self): - return 42 - - @cherrypy.expose - def return_float(self): - return 3.14 - - @cherrypy.expose - def return_datetime(self): - return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)) - - @cherrypy.expose - def return_boolean(self): - return True - - @cherrypy.expose - def test_argument_passing(self, num): - return num * 2 - - @cherrypy.expose - def test_returning_Fault(self): - return Fault(1, 'custom Fault response') - - root = Root() - root.xmlrpc = XmlRpc() - 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) - proxy = ServerProxy(url, transport=HTTPSTransport()) - else: - url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT) - proxy = ServerProxy(url) - - # begin the tests ... - self.getPage('/xmlrpc/foo') - self.assertBody('Hello world!') - - 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_dict(), {'a': 1, 'c': 3, 'b': 2}) - 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_boolean(), True) - self.assertEqual(proxy.test_argument_passing(22), 22 * 2) - - # Test an error in the page handler (should raise an xmlrpclib.Fault) - try: - proxy.test_argument_passing({}) - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, ('unsupported operand type(s) ' - "for *: 'dict' and 'int'")) - else: - self.fail('Expected xmlrpclib.Fault') - - # https://github.com/cherrypy/cherrypy/issues/533 - # if a method is not found, an xmlrpclib.Fault should be raised - try: - proxy.non_method() - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, - 'method "non_method" is not supported') - else: - self.fail('Expected xmlrpclib.Fault') - - # Test returning a Fault from the page handler. - try: - proxy.test_returning_Fault() - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, ('custom Fault response')) - else: - self.fail('Expected xmlrpclib.Fault') diff --git a/libraries/cherrypy/test/webtest.py b/libraries/cherrypy/test/webtest.py deleted file mode 100644 index 9fb6ce62..00000000 --- a/libraries/cherrypy/test/webtest.py +++ /dev/null @@ -1,11 +0,0 @@ -# for compatibility, expose cheroot webtest here -import warnings - -from cheroot.test.webtest import ( # noqa - interface, - WebCase, cleanHeaders, shb, openURL, - ServerError, server_error, -) - - -warnings.warn('Use cheroot.test.webtest', DeprecationWarning) diff --git a/libraries/cherrypy/tutorial/README.rst b/libraries/cherrypy/tutorial/README.rst deleted file mode 100644 index c47e7d32..00000000 --- a/libraries/cherrypy/tutorial/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -CherryPy Tutorials ------------------- - -This is a series of tutorials explaining how to develop dynamic web -applications using CherryPy. A couple of notes: - - -- Each of these tutorials builds on the ones before it. If you're - new to CherryPy, we recommend you start with 01_helloworld.py and - work your way upwards. :) - -- In most of these tutorials, you will notice that all output is done - by returning normal Python strings, often using simple Python - variable substitution. In most real-world applications, you will - probably want to use a separate template package (like Cheetah, - CherryTemplate or XML/XSL). diff --git a/libraries/cherrypy/tutorial/__init__.py b/libraries/cherrypy/tutorial/__init__.py deleted file mode 100644 index 08c142c5..00000000 --- a/libraries/cherrypy/tutorial/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -# This is used in test_config to test unrepr of "from A import B" -thing2 = object() diff --git a/libraries/cherrypy/tutorial/custom_error.html b/libraries/cherrypy/tutorial/custom_error.html deleted file mode 100644 index d0f30c8a..00000000 --- a/libraries/cherrypy/tutorial/custom_error.html +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html> -<head> - <title>403 Unauthorized</title> -</head> - <body> - <h2>You can't do that!</h2> - <p>%(message)s</p> - <p>This is a custom error page that is read from a file.<p> - <pre>%(traceback)s</pre> - </body> -</html> diff --git a/libraries/cherrypy/tutorial/pdf_file.pdf b/libraries/cherrypy/tutorial/pdf_file.pdf deleted file mode 100644 index 38b4f15eabdd65d4a674cb32034361245aa7b97e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85698 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 diff --git a/libraries/cherrypy/tutorial/tut01_helloworld.py b/libraries/cherrypy/tutorial/tut01_helloworld.py deleted file mode 100644 index e86793c8..00000000 --- a/libraries/cherrypy/tutorial/tut01_helloworld.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Tutorial - Hello World - -The most basic (working) CherryPy application possible. -""" - -import os.path - -# Import CherryPy global namespace -import cherrypy - - -class HelloWorld: - - """ Sample request handler class. """ - - # Expose the index method through the web. CherryPy will never - # publish methods that don't have the exposed attribute set to True. - @cherrypy.expose - def index(self): - # CherryPy will call this method for the root URI ("/") and send - # its return value to the client. Because this is tutorial - # lesson number 01, we'll just send something really simple. - # How about... - return 'Hello world!' - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HelloWorld(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut02_expose_methods.py b/libraries/cherrypy/tutorial/tut02_expose_methods.py deleted file mode 100644 index 8afbf7d8..00000000 --- a/libraries/cherrypy/tutorial/tut02_expose_methods.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Tutorial - Multiple methods - -This tutorial shows you how to link to other methods of your request -handler. -""" - -import os.path - -import cherrypy - - -class HelloWorld: - - @cherrypy.expose - def index(self): - # Let's link to another method here. - return 'We have an <a href="show_msg">important message</a> for you!' - - @cherrypy.expose - def show_msg(self): - # Here's the important message! - return 'Hello world!' - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HelloWorld(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut03_get_and_post.py b/libraries/cherrypy/tutorial/tut03_get_and_post.py deleted file mode 100644 index 0b3d4613..00000000 --- a/libraries/cherrypy/tutorial/tut03_get_and_post.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Tutorial - Passing variables - -This tutorial shows you how to pass GET/POST variables to methods. -""" - -import os.path - -import cherrypy - - -class WelcomePage: - - @cherrypy.expose - def index(self): - # Ask for the user's name. - return ''' - <form action="greetUser" method="GET"> - What is your name? - <input type="text" name="name" /> - <input type="submit" /> - </form>''' - - @cherrypy.expose - def greetUser(self, name=None): - # CherryPy passes all GET and POST variables as method parameters. - # It doesn't make a difference where the variables come from, how - # large their contents are, and so on. - # - # You can define default parameter values as usual. In this - # example, the "name" parameter defaults to None so we can check - # if a name was actually specified. - - if name: - # Greet the user! - return "Hey %s, what's up?" % name - else: - if name is None: - # No name was specified - return 'Please enter your name <a href="./">here</a>.' - else: - return 'No, really, enter your name <a href="./">here</a>.' - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(WelcomePage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut04_complex_site.py b/libraries/cherrypy/tutorial/tut04_complex_site.py deleted file mode 100644 index 3caa1775..00000000 --- a/libraries/cherrypy/tutorial/tut04_complex_site.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Tutorial - Multiple objects - -This tutorial shows you how to create a site structure through multiple -possibly nested request handler objects. -""" - -import os.path - -import cherrypy - - -class HomePage: - - @cherrypy.expose - def index(self): - return ''' - <p>Hi, this is the home page! Check out the other - fun stuff on this site:</p> - - <ul> - <li><a href="/joke/">A silly joke</a></li> - <li><a href="/links/">Useful links</a></li> - </ul>''' - - -class JokePage: - - @cherrypy.expose - def index(self): - return ''' - <p>"In Python, how do you create a string of random - characters?" -- "Read a Perl file!"</p> - <p>[<a href="../">Return</a>]</p>''' - - -class LinksPage: - - def __init__(self): - # Request handler objects can create their own nested request - # handler objects. Simply create them inside their __init__ - # methods! - self.extra = ExtraLinksPage() - - @cherrypy.expose - def index(self): - # Note the way we link to the extra links page (and back). - # 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 ''' - <p>Here are some useful links:</p> - - <ul> - <li> - <a href="http://www.cherrypy.org">The CherryPy Homepage</a> - </li> - <li> - <a href="http://www.python.org">The Python Homepage</a> - </li> - </ul> - - <p>You can check out some extra useful - links <a href="./extra/">here</a>.</p> - - <p>[<a href="../">Return</a>]</p> - ''' - - -class ExtraLinksPage: - - @cherrypy.expose - def index(self): - # Note the relative link back to the Links page! - return ''' - <p>Here are some extra useful links:</p> - - <ul> - <li><a href="http://del.icio.us">del.icio.us</a></li> - <li><a href="http://www.cherrypy.org">CherryPy</a></li> - </ul> - - <p>[<a href="../">Return to links page</a>]</p>''' - - -# Of course we can also mount request handler objects right here! -root = HomePage() -root.joke = JokePage() -root.links = LinksPage() - -# Remember, we don't need to mount ExtraLinksPage here, because -# LinksPage does that itself on initialization. In fact, there is -# no reason why you shouldn't let your root object take care of -# creating all contained request handler objects. - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(root, config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut05_derived_objects.py b/libraries/cherrypy/tutorial/tut05_derived_objects.py deleted file mode 100644 index f626e03f..00000000 --- a/libraries/cherrypy/tutorial/tut05_derived_objects.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Tutorial - Object inheritance - -You are free to derive your request handler classes from any base -class you wish. In most real-world applications, you will probably -want to create a central base class used for all your pages, which takes -care of things like printing a common page header and footer. -""" - -import os.path - -import cherrypy - - -class Page: - # Store the page title in a class attribute - title = 'Untitled Page' - - def header(self): - return ''' - <html> - <head> - <title>%s</title> - <head> - <body> - <h2>%s</h2> - ''' % (self.title, self.title) - - def footer(self): - return ''' - </body> - </html> - ''' - - # Note that header and footer don't get their exposed attributes - # set to True. This isn't necessary since the user isn't supposed - # to call header or footer directly; instead, we'll call them from - # within the actually exposed handler methods defined in this - # class' subclasses. - - -class HomePage(Page): - # Different title for this page - title = 'Tutorial 5' - - def __init__(self): - # create a subpage - self.another = AnotherPage() - - @cherrypy.expose - def index(self): - # Note that we call the header and footer methods inherited - # from the Page class! - return self.header() + ''' - <p> - Isn't this exciting? There's - <a href="./another/">another page</a>, too! - </p> - ''' + self.footer() - - -class AnotherPage(Page): - title = 'Another Page' - - @cherrypy.expose - def index(self): - return self.header() + ''' - <p> - And this is the amazing second page! - </p> - ''' + self.footer() - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HomePage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut06_default_method.py b/libraries/cherrypy/tutorial/tut06_default_method.py deleted file mode 100644 index 0ce4cabe..00000000 --- a/libraries/cherrypy/tutorial/tut06_default_method.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Tutorial - The default method - -Request handler objects can implement a method called "default" that -is called when no other suitable method/object could be found. -Essentially, if CherryPy2 can't find a matching request handler object -for the given request URI, it will use the default method of the object -located deepest on the URI path. - -Using this mechanism you can easily simulate virtual URI structures -by parsing the extra URI string, which you can access through -cherrypy.request.virtualPath. - -The application in this tutorial simulates an URI structure looking -like /users/<username>. Since the <username> bit will not be found (as -there are no matching methods), it is handled by the default method. -""" - -import os.path - -import cherrypy - - -class UsersPage: - - @cherrypy.expose - 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 ''' - <a href="./remi">Remi Delon</a><br/> - <a href="./hendrik">Hendrik Mans</a><br/> - <a href="./lorenzo">Lorenzo Lamas</a><br/> - ''' - - @cherrypy.expose - def default(self, user): - # Here we react depending on the virtualPath -- the part of the - # path that could not be mapped to an object method. In a real - # application, we would probably do some database lookups here - # instead of the silly if/elif/else construct. - if user == 'remi': - out = 'Remi Delon, CherryPy lead developer' - elif user == 'hendrik': - out = 'Hendrik Mans, CherryPy co-developer & crazy German' - elif user == 'lorenzo': - out = 'Lorenzo Lamas, famous actor and singer!' - else: - out = 'Unknown user. :-(' - - return '%s (<a href="./">back</a>)' % out - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(UsersPage(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut07_sessions.py b/libraries/cherrypy/tutorial/tut07_sessions.py deleted file mode 100644 index 204322b5..00000000 --- a/libraries/cherrypy/tutorial/tut07_sessions.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Tutorial - Sessions - -Storing session data in CherryPy applications is very easy: cherrypy -provides a dictionary called "session" that represents the session -data for the current user. If you use RAM based sessions, you can store -any kind of object into that dictionary; otherwise, you are limited to -objects that can be pickled. -""" - -import os.path - -import cherrypy - - -class HitCounter: - - _cp_config = {'tools.sessions.on': True} - - @cherrypy.expose - def index(self): - # Increase the silly hit counter - count = cherrypy.session.get('count', 0) + 1 - - # Store the new value in the session dictionary - cherrypy.session['count'] = count - - # And display a silly hit count message! - return ''' - During your current session, you've viewed this - page %s times! Your life is a patio of fun! - ''' % count - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HitCounter(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py deleted file mode 100644 index 18f42f93..00000000 --- a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Bonus Tutorial: Using generators to return result bodies - -Instead of returning a complete result string, you can use the yield -statement to return one result part after another. This may be convenient -in situations where using a template package like CherryPy or Cheetah -would be overkill, and messy string concatenation too uncool. ;-) -""" - -import os.path - -import cherrypy - - -class GeneratorDemo: - - def header(self): - return '<html><body><h2>Generators rule!</h2>' - - def footer(self): - return '</body></html>' - - @cherrypy.expose - def index(self): - # Let's make up a list of users for presentation purposes - users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas'] - - # Every yield line adds one part to the total result body. - yield self.header() - yield '<h3>List of users:</h3>' - - for user in users: - yield '%s<br/>' % user - - yield self.footer() - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(GeneratorDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut09_files.py b/libraries/cherrypy/tutorial/tut09_files.py deleted file mode 100644 index 48585cbe..00000000 --- a/libraries/cherrypy/tutorial/tut09_files.py +++ /dev/null @@ -1,105 +0,0 @@ -""" - -Tutorial: File upload and download - -Uploads -------- - -When a client uploads a file to a CherryPy application, it's placed -on disk immediately. CherryPy will pass it to your exposed method -as an argument (see "myFile" below); that arg will have a "file" -attribute, which is a handle to the temporary uploaded file. -If you wish to permanently save the file, you need to read() -from myFile.file and write() somewhere else. - -Note the use of 'enctype="multipart/form-data"' and 'input type="file"' -in the HTML which the client uses to upload the file. - - -Downloads ---------- - -If you wish to send a file to the client, you have two options: -First, you can simply return a file-like object from your page handler. -CherryPy will read the file and serve it as the content (HTTP body) -of the response. However, that doesn't tell the client that -the response is a file to be saved, rather than displayed. -Use cherrypy.lib.static.serve_file for that; it takes four -arguments: - -serve_file(path, content_type=None, disposition=None, name=None) - -Set "name" to the filename that you expect clients to use when they save -your file. Note that the "name" argument is ignored if you don't also -provide a "disposition" (usually "attachement"). You can manually set -"content_type", but be aware that if you also use the encoding tool, it -may choke if the file extension is not recognized as belonging to a known -Content-Type. Setting the content_type to "application/x-download" works -in most cases, and should prompt the user with an Open/Save dialog in -popular browsers. - -""" - -import os -import os.path - -import cherrypy -from cherrypy.lib import static - -localDir = os.path.dirname(__file__) -absDir = os.path.join(os.getcwd(), localDir) - - -class FileDemo(object): - - @cherrypy.expose - def index(self): - return """ - <html><body> - <h2>Upload a file</h2> - <form action="upload" method="post" enctype="multipart/form-data"> - filename: <input type="file" name="myFile" /><br /> - <input type="submit" /> - </form> - <h2>Download a file</h2> - <a href='download'>This one</a> - </body></html> - """ - - @cherrypy.expose - def upload(self, myFile): - out = """<html> - <body> - myFile length: %s<br /> - myFile filename: %s<br /> - myFile mime-type: %s - </body> - </html>""" - - # Although this just counts the file length, it demonstrates - # how to read large files in chunks instead of all at once. - # CherryPy reads the uploaded file into a temporary file; - # myFile.file.read reads from that. - size = 0 - while True: - data = myFile.file.read(8192) - if not data: - break - size += len(data) - - return out % (size, myFile.filename, myFile.content_type) - - @cherrypy.expose - def download(self): - path = os.path.join(absDir, 'pdf_file.pdf') - return static.serve_file(path, 'application/x-download', - 'attachment', os.path.basename(path)) - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(FileDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tut10_http_errors.py b/libraries/cherrypy/tutorial/tut10_http_errors.py deleted file mode 100644 index 18f02fd0..00000000 --- a/libraries/cherrypy/tutorial/tut10_http_errors.py +++ /dev/null @@ -1,84 +0,0 @@ -""" - -Tutorial: HTTP errors - -HTTPError is used to return an error response to the client. -CherryPy has lots of options regarding how such errors are -logged, displayed, and formatted. - -""" - -import os -import os.path - -import cherrypy - -localDir = os.path.dirname(__file__) -curpath = os.path.normpath(os.path.join(os.getcwd(), localDir)) - - -class HTTPErrorDemo(object): - - # Set a custom response for 403 errors. - _cp_config = {'error_page.403': - os.path.join(curpath, 'custom_error.html')} - - @cherrypy.expose - def index(self): - # display some links that will result in errors - tracebacks = cherrypy.request.show_tracebacks - if tracebacks: - trace = 'off' - else: - trace = 'on' - - return """ - <html><body> - <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p> - <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p> - <p> - <a href="/error?code=403"> - Use a custom error page from a file. - </a> - </p> - <p>These errors are explicitly raised by the application:</p> - <ul> - <li><a href="/error?code=400">400</a></li> - <li><a href="/error?code=401">401</a></li> - <li><a href="/error?code=402">402</a></li> - <li><a href="/error?code=500">500</a></li> - </ul> - <p><a href="/messageArg">You can also set the response body - when you raise an error.</a></p> - </body></html> - """ % trace - - @cherrypy.expose - def toggleTracebacks(self): - # simple function to toggle tracebacks on and off - tracebacks = cherrypy.request.show_tracebacks - cherrypy.config.update({'request.show_tracebacks': not tracebacks}) - - # redirect back to the index - raise cherrypy.HTTPRedirect('/') - - @cherrypy.expose - def error(self, code): - # raise an error based on the get query - raise cherrypy.HTTPError(status=code) - - @cherrypy.expose - def messageArg(self): - 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) - - -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HTTPErrorDemo(), config=tutconf) diff --git a/libraries/cherrypy/tutorial/tutorial.conf b/libraries/cherrypy/tutorial/tutorial.conf deleted file mode 100644 index 43dfa60f..00000000 --- a/libraries/cherrypy/tutorial/tutorial.conf +++ /dev/null @@ -1,4 +0,0 @@ -[global] -server.socket_host = "127.0.0.1" -server.socket_port = 8080 -server.thread_pool = 10 diff --git a/libraries/contextlib2.py b/libraries/contextlib2.py deleted file mode 100644 index f08df14c..00000000 --- a/libraries/contextlib2.py +++ /dev/null @@ -1,436 +0,0 @@ -"""contextlib2 - backports and enhancements to the contextlib module""" - -import sys -import warnings -from collections import deque -from functools import wraps - -__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", - "redirect_stdout", "redirect_stderr", "suppress"] - -# Backwards compatibility -__all__ += ["ContextStack"] - -class ContextDecorator(object): - "A base class or mixin that enables context managers to work as decorators." - - def refresh_cm(self): - """Returns the context manager used to actually wrap the call to the - decorated function. - - The default implementation just returns *self*. - - Overriding this method allows otherwise one-shot context managers - like _GeneratorContextManager to support use as decorators via - implicit recreation. - - DEPRECATED: refresh_cm was never added to the standard library's - ContextDecorator API - """ - warnings.warn("refresh_cm was never added to the standard library", - DeprecationWarning) - return self._recreate_cm() - - def _recreate_cm(self): - """Return a recreated instance of self. - - Allows an otherwise one-shot context manager like - _GeneratorContextManager to support use as - a decorator via implicit recreation. - - This is a private interface just for _GeneratorContextManager. - See issue #11647 for details. - """ - return self - - def __call__(self, func): - @wraps(func) - def inner(*args, **kwds): - with self._recreate_cm(): - return func(*args, **kwds) - return inner - - -class _GeneratorContextManager(ContextDecorator): - """Helper for @contextmanager decorator.""" - - def __init__(self, func, args, kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - # Issue 19330: ensure context manager instances have good docstrings - doc = getattr(func, "__doc__", None) - if doc is None: - doc = type(self).__doc__ - self.__doc__ = doc - # Unfortunately, this still doesn't provide good help output when - # inspecting the created context manager instances, since pydoc - # currently bypasses the instance docstring and shows the docstring - # for the class instead. - # See http://bugs.python.org/issue19404 for more details. - - def _recreate_cm(self): - # _GCM instances are one-shot context managers, so the - # CM must be recreated each time a decorated function is - # called - return self.__class__(self.func, self.args, self.kwds) - - def __enter__(self): - try: - return next(self.gen) - except StopIteration: - raise RuntimeError("generator didn't yield") - - def __exit__(self, type, value, traceback): - if type is None: - try: - next(self.gen) - except StopIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - # Need to force instantiation so we can reliably - # tell if we get the same exception back - value = type() - try: - self.gen.throw(type, value, traceback) - raise RuntimeError("generator didn't stop after throw()") - except StopIteration as exc: - # Suppress StopIteration *unless* it's the same exception that - # was passed to throw(). This prevents a StopIteration - # raised inside the "with" statement from being suppressed. - return exc is not value - except RuntimeError as exc: - # Don't re-raise the passed in exception - if exc is value: - return False - # Likewise, avoid suppressing if a StopIteration exception - # was passed to throw() and later wrapped into a RuntimeError - # (see PEP 479). - if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: - return False - raise - except: - # only re-raise if it's *not* the exception that was - # passed to throw(), because __exit__() must not raise - # an exception unless __exit__() itself failed. But throw() - # has to raise the exception to signal propagation, so this - # fixes the impedance mismatch between the throw() protocol - # and the __exit__() protocol. - # - if sys.exc_info()[1] is not value: - raise - - -def contextmanager(func): - """@contextmanager decorator. - - Typical usage: - - @contextmanager - def some_generator(<arguments>): - <setup> - try: - yield <value> - finally: - <cleanup> - - This makes this: - - with some_generator(<arguments>) as <variable>: - <body> - - equivalent to this: - - <setup> - try: - <variable> = <value> - <body> - finally: - <cleanup> - - """ - @wraps(func) - def helper(*args, **kwds): - return _GeneratorContextManager(func, args, kwds) - return helper - - -class closing(object): - """Context to automatically close something at the end of a block. - - Code like this: - - with closing(<module>.open(<arguments>)) as f: - <block> - - is equivalent to this: - - f = <module>.open(<arguments>) - try: - <block> - finally: - f.close() - - """ - def __init__(self, thing): - self.thing = thing - def __enter__(self): - return self.thing - def __exit__(self, *exc_info): - self.thing.close() - - -class _RedirectStream(object): - - _stream = None - - def __init__(self, new_target): - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] - - def __enter__(self): - self._old_targets.append(getattr(sys, self._stream)) - setattr(sys, self._stream, self._new_target) - return self._new_target - - def __exit__(self, exctype, excinst, exctb): - setattr(sys, self._stream, self._old_targets.pop()) - - -class redirect_stdout(_RedirectStream): - """Context manager for temporarily redirecting stdout to another file. - - # How to send help() to stderr - with redirect_stdout(sys.stderr): - help(dir) - - # How to write help() to a file - with open('help.txt', 'w') as f: - with redirect_stdout(f): - help(pow) - """ - - _stream = "stdout" - - -class redirect_stderr(_RedirectStream): - """Context manager for temporarily redirecting stderr to another file.""" - - _stream = "stderr" - - -class suppress(object): - """Context manager to suppress specified exceptions - - After the exception is suppressed, execution proceeds with the next - statement following the with statement. - - with suppress(FileNotFoundError): - os.remove(somefile) - # Execution still resumes here if the file was already removed - """ - - def __init__(self, *exceptions): - self._exceptions = exceptions - - def __enter__(self): - pass - - def __exit__(self, exctype, excinst, exctb): - # Unlike isinstance and issubclass, CPython exception handling - # currently only looks at the concrete type hierarchy (ignoring - # the instance and subclass checking hooks). While Guido considers - # that a bug rather than a feature, it's a fairly hard one to fix - # due to various internal implementation details. suppress provides - # the simpler issubclass based semantics, rather than trying to - # exactly reproduce the limitations of the CPython interpreter. - # - # See http://bugs.python.org/issue12029 for more details - return exctype is not None and issubclass(exctype, self._exceptions) - - -# Context manipulation is Python 3 only -_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 -if _HAVE_EXCEPTION_CHAINING: - def _make_context_fixer(frame_exc): - def _fix_exception_context(new_exc, old_exc): - # Context may not be correct, so find the end of the chain - while 1: - exc_context = new_exc.__context__ - if exc_context is old_exc: - # Context is already set correctly (see issue 20317) - return - if exc_context is None or exc_context is frame_exc: - break - new_exc = exc_context - # Change the end of the chain to point to the exception - # we expect it to reference - new_exc.__context__ = old_exc - return _fix_exception_context - - def _reraise_with_existing_context(exc_details): - try: - # bare "raise exc_details[1]" replaces our carefully - # set-up context - fixed_ctx = exc_details[1].__context__ - raise exc_details[1] - except BaseException: - exc_details[1].__context__ = fixed_ctx - raise -else: - # No exception context in Python 2 - def _make_context_fixer(frame_exc): - return lambda new_exc, old_exc: None - - # Use 3 argument raise in Python 2, - # but use exec to avoid SyntaxError in Python 3 - def _reraise_with_existing_context(exc_details): - exc_type, exc_value, exc_tb = exc_details - exec ("raise exc_type, exc_value, exc_tb") - -# Handle old-style classes if they exist -try: - from types import InstanceType -except ImportError: - # Python 3 doesn't have old-style classes - _get_type = type -else: - # Need to handle old-style context managers on Python 2 - def _get_type(obj): - obj_type = type(obj) - if obj_type is InstanceType: - return obj.__class__ # Old-style class - return obj_type # New-style class - -# Inspired by discussions on http://bugs.python.org/issue13585 -class ExitStack(object): - """Context manager for dynamic management of a stack of exit callbacks - - For example: - - with ExitStack() as stack: - files = [stack.enter_context(open(fname)) for fname in filenames] - # All opened files will automatically be closed at the end of - # the with statement, even if attempts to open files later - # in the list raise an exception - - """ - def __init__(self): - self._exit_callbacks = deque() - - def pop_all(self): - """Preserve the context stack by transferring it to a new instance""" - new_stack = type(self)() - new_stack._exit_callbacks = self._exit_callbacks - self._exit_callbacks = deque() - return new_stack - - def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods""" - def _exit_wrapper(*exc_details): - return cm_exit(cm, *exc_details) - _exit_wrapper.__self__ = cm - self.push(_exit_wrapper) - - def push(self, exit): - """Registers a callback with the standard __exit__ method signature - - Can suppress exceptions the same way __exit__ methods can. - - Also accepts any object with an __exit__ method (registering a call - to the method instead of the object itself) - """ - # We use an unbound method rather than a bound method to follow - # the standard lookup behaviour for special methods - _cb_type = _get_type(exit) - try: - exit_method = _cb_type.__exit__ - except AttributeError: - # Not a context manager, so assume its a callable - self._exit_callbacks.append(exit) - else: - self._push_cm_exit(exit, exit_method) - return exit # Allow use as a decorator - - def callback(self, callback, *args, **kwds): - """Registers an arbitrary callback and arguments. - - Cannot suppress exceptions. - """ - def _exit_wrapper(exc_type, exc, tb): - callback(*args, **kwds) - # We changed the signature, so using @wraps is not appropriate, but - # setting __wrapped__ may still help with introspection - _exit_wrapper.__wrapped__ = callback - self.push(_exit_wrapper) - return callback # Allow use as a decorator - - def enter_context(self, cm): - """Enters the supplied context manager - - If successful, also pushes its __exit__ method as a callback and - returns the result of the __enter__ method. - """ - # We look up the special methods on the type to match the with statement - _cm_type = _get_type(cm) - _exit = _cm_type.__exit__ - result = _cm_type.__enter__(cm) - self._push_cm_exit(cm, _exit) - return result - - def close(self): - """Immediately unwind the context stack""" - self.__exit__(None, None, None) - - def __enter__(self): - return self - - def __exit__(self, *exc_details): - received_exc = exc_details[0] is not None - - # We manipulate the exception state so it behaves as though - # we were actually nesting multiple with statements - frame_exc = sys.exc_info()[1] - _fix_exception_context = _make_context_fixer(frame_exc) - - # Callbacks are invoked in LIFO order to match the behaviour of - # nested context managers - suppressed_exc = False - pending_raise = False - while self._exit_callbacks: - cb = self._exit_callbacks.pop() - try: - if cb(*exc_details): - suppressed_exc = True - pending_raise = False - exc_details = (None, None, None) - except: - new_exc_details = sys.exc_info() - # simulate the stack of exceptions by setting the context - _fix_exception_context(new_exc_details[1], exc_details[1]) - pending_raise = True - exc_details = new_exc_details - if pending_raise: - _reraise_with_existing_context(exc_details) - return received_exc and suppressed_exc - -# Preserve backwards compatibility -class ContextStack(ExitStack): - """Backwards compatibility alias for ExitStack""" - - def __init__(self): - warnings.warn("ContextStack has been renamed to ExitStack", - DeprecationWarning) - super(ContextStack, self).__init__() - - def register_exit(self, callback): - return self.push(callback) - - def register(self, callback, *args, **kwds): - return self.callback(callback, *args, **kwds) - - def preserve(self): - return self.pop_all() diff --git a/libraries/more_itertools/__init__.py b/libraries/more_itertools/__init__.py deleted file mode 100644 index bba462c3..00000000 --- a/libraries/more_itertools/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from more_itertools.more import * # noqa -from more_itertools.recipes import * # noqa diff --git a/libraries/more_itertools/more.py b/libraries/more_itertools/more.py deleted file mode 100644 index 05e851ee..00000000 --- a/libraries/more_itertools/more.py +++ /dev/null @@ -1,2211 +0,0 @@ -from __future__ import print_function - -from collections import Counter, defaultdict, deque -from functools import partial, wraps -from heapq import merge -from itertools import ( - chain, - compress, - count, - cycle, - dropwhile, - groupby, - islice, - repeat, - starmap, - takewhile, - tee -) -from operator import itemgetter, lt, gt, sub -from sys import maxsize, version_info -try: - from collections.abc import Sequence -except ImportError: - from collections import Sequence - -from six import binary_type, string_types, text_type -from six.moves import filter, map, range, zip, zip_longest - -from .recipes import consume, flatten, take - -__all__ = [ - 'adjacent', - 'always_iterable', - 'always_reversible', - 'bucket', - 'chunked', - 'circular_shifts', - 'collapse', - 'collate', - 'consecutive_groups', - 'consumer', - 'count_cycle', - 'difference', - 'distinct_permutations', - 'distribute', - 'divide', - 'exactly_n', - 'first', - 'groupby_transform', - 'ilen', - 'interleave_longest', - 'interleave', - 'intersperse', - 'islice_extended', - 'iterate', - 'last', - 'locate', - 'lstrip', - 'make_decorator', - 'map_reduce', - 'numeric_range', - 'one', - 'padded', - 'peekable', - 'replace', - 'rlocate', - 'rstrip', - 'run_length', - 'seekable', - 'SequenceView', - 'side_effect', - 'sliced', - 'sort_together', - 'split_at', - 'split_after', - 'split_before', - 'spy', - 'stagger', - 'strip', - 'unique_to_each', - 'windowed', - 'with_iter', - 'zip_offset', -] - -_marker = object() - - -def chunked(iterable, n): - """Break *iterable* into lists of length *n*: - - >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) - [[1, 2, 3], [4, 5, 6]] - - If the length of *iterable* is not evenly divisible by *n*, the last - returned list will be shorter: - - >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) - [[1, 2, 3], [4, 5, 6], [7, 8]] - - To use a fill-in value instead, see the :func:`grouper` recipe. - - :func:`chunked` is useful for splitting up a computation on a large number - of keys into batches, to be pickled and sent off to worker processes. One - example is operations on rows in MySQL, which does not implement - server-side cursors properly and would otherwise load the entire dataset - into RAM on the client. - - """ - return iter(partial(take, n, iter(iterable)), []) - - -def first(iterable, default=_marker): - """Return the first item of *iterable*, or *default* if *iterable* is - empty. - - >>> first([0, 1, 2, 3]) - 0 - >>> first([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - - :func:`first` is useful when you have a generator of expensive-to-retrieve - values and want any arbitrary one. It is marginally shorter than - ``next(iter(iterable), default)``. - - """ - try: - return next(iter(iterable)) - except StopIteration: - # I'm on the edge about raising ValueError instead of StopIteration. At - # the moment, ValueError wins, because the caller could conceivably - # want to do something different with flow control when I raise the - # exception, and it's weird to explicitly catch StopIteration. - if default is _marker: - raise ValueError('first() was called on an empty iterable, and no ' - 'default value was provided.') - return default - - -def last(iterable, default=_marker): - """Return the last item of *iterable*, or *default* if *iterable* is - empty. - - >>> last([0, 1, 2, 3]) - 3 - >>> last([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - """ - try: - try: - # Try to access the last item directly - return iterable[-1] - except (TypeError, AttributeError, KeyError): - # If not slice-able, iterate entirely using length-1 deque - return deque(iterable, maxlen=1)[0] - except IndexError: # If the iterable was empty - if default is _marker: - raise ValueError('last() was called on an empty iterable, and no ' - 'default value was provided.') - return default - - -class peekable(object): - """Wrap an iterator to allow lookahead and prepending elements. - - Call :meth:`peek` on the result to get the value that will be returned - by :func:`next`. This won't advance the iterator: - - >>> p = peekable(['a', 'b']) - >>> p.peek() - 'a' - >>> next(p) - 'a' - - Pass :meth:`peek` a default value to return that instead of raising - ``StopIteration`` when the iterator is exhausted. - - >>> p = peekable([]) - >>> p.peek('hi') - 'hi' - - peekables also offer a :meth:`prepend` method, which "inserts" items - at the head of the iterable: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> p.peek() - 11 - >>> list(p) - [11, 12, 1, 2, 3] - - peekables can be indexed. Index 0 is the item that will be returned by - :func:`next`, index 1 is the item after that, and so on: - The values up to the given index will be cached. - - >>> p = peekable(['a', 'b', 'c', 'd']) - >>> p[0] - 'a' - >>> p[1] - 'b' - >>> next(p) - 'a' - - Negative indexes are supported, but be aware that they will cache the - remaining items in the source iterator, which may require significant - storage. - - To check whether a peekable is exhausted, check its truth value: - - >>> p = peekable(['a', 'b']) - >>> if p: # peekable has items - ... list(p) - ['a', 'b'] - >>> if not p: # peekable is exhaused - ... list(p) - [] - - """ - def __init__(self, iterable): - self._it = iter(iterable) - self._cache = deque() - - def __iter__(self): - return self - - def __bool__(self): - try: - self.peek() - except StopIteration: - return False - return True - - def __nonzero__(self): - # For Python 2 compatibility - return self.__bool__() - - def peek(self, default=_marker): - """Return the item that will be next returned from ``next()``. - - Return ``default`` if there are no items left. If ``default`` is not - provided, raise ``StopIteration``. - - """ - if not self._cache: - try: - self._cache.append(next(self._it)) - except StopIteration: - if default is _marker: - raise - return default - return self._cache[0] - - def prepend(self, *items): - """Stack up items to be the next ones returned from ``next()`` or - ``self.peek()``. The items will be returned in - first in, first out order:: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> list(p) - [11, 12, 1, 2, 3] - - It is possible, by prepending items, to "resurrect" a peekable that - previously raised ``StopIteration``. - - >>> p = peekable([]) - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - >>> p.prepend(1) - >>> next(p) - 1 - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - - """ - self._cache.extendleft(reversed(items)) - - def __next__(self): - if self._cache: - return self._cache.popleft() - - return next(self._it) - - next = __next__ # For Python 2 compatibility - - def _get_slice(self, index): - # Normalize the slice's arguments - step = 1 if (index.step is None) else index.step - if step > 0: - start = 0 if (index.start is None) else index.start - stop = maxsize if (index.stop is None) else index.stop - elif step < 0: - start = -1 if (index.start is None) else index.start - stop = (-maxsize - 1) if (index.stop is None) else index.stop - else: - raise ValueError('slice step cannot be zero') - - # If either the start or stop index is negative, we'll need to cache - # the rest of the iterable in order to slice from the right side. - if (start < 0) or (stop < 0): - self._cache.extend(self._it) - # Otherwise we'll need to find the rightmost index and cache to that - # point. - else: - n = min(max(start, stop) + 1, maxsize) - cache_len = len(self._cache) - if n >= cache_len: - self._cache.extend(islice(self._it, n - cache_len)) - - return list(self._cache)[index] - - def __getitem__(self, index): - if isinstance(index, slice): - return self._get_slice(index) - - cache_len = len(self._cache) - if index < 0: - self._cache.extend(self._it) - elif index >= cache_len: - self._cache.extend(islice(self._it, index + 1 - cache_len)) - - return self._cache[index] - - -def _collate(*iterables, **kwargs): - """Helper for ``collate()``, called when the user is using the ``reverse`` - or ``key`` keyword arguments on Python versions below 3.5. - - """ - key = kwargs.pop('key', lambda a: a) - reverse = kwargs.pop('reverse', False) - - min_or_max = partial(max if reverse else min, key=itemgetter(0)) - peekables = [peekable(it) for it in iterables] - peekables = [p for p in peekables if p] # Kill empties. - while peekables: - _, p = min_or_max((key(p.peek()), p) for p in peekables) - yield next(p) - peekables = [x for x in peekables if x] - - -def collate(*iterables, **kwargs): - """Return a sorted merge of the items from each of several already-sorted - *iterables*. - - >>> list(collate('ACDZ', 'AZ', 'JKL')) - ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] - - Works lazily, keeping only the next value from each iterable in memory. Use - :func:`collate` to, for example, perform a n-way mergesort of items that - don't fit in memory. - - If a *key* function is specified, the iterables will be sorted according - to its result: - - >>> key = lambda s: int(s) # Sort by numeric value, not by string - >>> list(collate(['1', '10'], ['2', '11'], key=key)) - ['1', '2', '10', '11'] - - - If the *iterables* are sorted in descending order, set *reverse* to - ``True``: - - >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) - [5, 4, 3, 2, 1, 0] - - If the elements of the passed-in iterables are out of order, you might get - unexpected results. - - On Python 2.7, this function delegates to :func:`heapq.merge` if neither - of the keyword arguments are specified. On Python 3.5+, this function - is an alias for :func:`heapq.merge`. - - """ - if not kwargs: - return merge(*iterables) - - return _collate(*iterables, **kwargs) - - -# If using Python version 3.5 or greater, heapq.merge() will be faster than -# collate - use that instead. -if version_info >= (3, 5, 0): - _collate_docstring = collate.__doc__ - collate = partial(merge) - collate.__doc__ = _collate_docstring - - -def consumer(func): - """Decorator that automatically advances a PEP-342-style "reverse iterator" - to its first yield point so you don't have to call ``next()`` on it - manually. - - >>> @consumer - ... def tally(): - ... i = 0 - ... while True: - ... print('Thing number %s is %s.' % (i, (yield))) - ... i += 1 - ... - >>> t = tally() - >>> t.send('red') - Thing number 0 is red. - >>> t.send('fish') - Thing number 1 is fish. - - Without the decorator, you would have to call ``next(t)`` before - ``t.send()`` could be used. - - """ - @wraps(func) - def wrapper(*args, **kwargs): - gen = func(*args, **kwargs) - next(gen) - return gen - return wrapper - - -def ilen(iterable): - """Return the number of items in *iterable*. - - >>> ilen(x for x in range(1000000) if x % 3 == 0) - 333334 - - This consumes the iterable, so handle with care. - - """ - # maxlen=1 only stores the last item in the deque - d = deque(enumerate(iterable, 1), maxlen=1) - # since we started enumerate at 1, - # the first item of the last pair will be the length of the iterable - # (assuming there were items) - return d[0][0] if d else 0 - - -def iterate(func, start): - """Return ``start``, ``func(start)``, ``func(func(start))``, ... - - >>> from itertools import islice - >>> list(islice(iterate(lambda x: 2*x, 1), 10)) - [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] - - """ - while True: - yield start - start = func(start) - - -def with_iter(context_manager): - """Wrap an iterable in a ``with`` statement, so it closes once exhausted. - - For example, this will close the file when the iterator is exhausted:: - - upper_lines = (line.upper() for line in with_iter(open('foo'))) - - Any context manager which returns an iterable is a candidate for - ``with_iter``. - - """ - with context_manager as iterable: - for item in iterable: - yield item - - -def one(iterable, too_short=None, too_long=None): - """Return the first item from *iterable*, which is expected to contain only - that item. Raise an exception if *iterable* is empty or has more than one - item. - - :func:`one` is useful for ensuring that an iterable contains only one item. - For example, it can be used to retrieve the result of a database query - that is expected to return a single row. - - If *iterable* is empty, ``ValueError`` will be raised. You may specify a - different exception with the *too_short* keyword: - - >>> it = [] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too many items in iterable (expected 1)' - >>> too_short = IndexError('too few items') - >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - IndexError: too few items - - Similarly, if *iterable* contains more than one item, ``ValueError`` will - be raised. You may specify a different exception with the *too_long* - keyword: - - >>> it = ['too', 'many'] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too many items in iterable (expected 1)' - >>> too_long = RuntimeError - >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - RuntimeError - - Note that :func:`one` attempts to advance *iterable* twice to ensure there - is only one item. If there is more than one, both items will be discarded. - See :func:`spy` or :func:`peekable` to check iterable contents less - destructively. - - """ - it = iter(iterable) - - try: - value = next(it) - except StopIteration: - raise too_short or ValueError('too few items in iterable (expected 1)') - - try: - next(it) - except StopIteration: - pass - else: - raise too_long or ValueError('too many items in iterable (expected 1)') - - return value - - -def distinct_permutations(iterable): - """Yield successive distinct permutations of the elements in *iterable*. - - >>> sorted(distinct_permutations([1, 0, 1])) - [(0, 1, 1), (1, 0, 1), (1, 1, 0)] - - Equivalent to ``set(permutations(iterable))``, except duplicates are not - generated and thrown away. For larger input sequences this is much more - efficient. - - Duplicate permutations arise when there are duplicated elements in the - input iterable. The number of items returned is - `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of - items input, and each `x_i` is the count of a distinct item in the input - sequence. - - """ - def perm_unique_helper(item_counts, perm, i): - """Internal helper function - - :arg item_counts: Stores the unique items in ``iterable`` and how many - times they are repeated - :arg perm: The permutation that is being built for output - :arg i: The index of the permutation being modified - - The output permutations are built up recursively; the distinct items - are placed until their repetitions are exhausted. - """ - if i < 0: - yield tuple(perm) - else: - for item in item_counts: - if item_counts[item] <= 0: - continue - perm[i] = item - item_counts[item] -= 1 - for x in perm_unique_helper(item_counts, perm, i - 1): - yield x - item_counts[item] += 1 - - item_counts = Counter(iterable) - length = sum(item_counts.values()) - - return perm_unique_helper(item_counts, [None] * length, length - 1) - - -def intersperse(e, iterable, n=1): - """Intersperse filler element *e* among the items in *iterable*, leaving - *n* items between each filler element. - - >>> list(intersperse('!', [1, 2, 3, 4, 5])) - [1, '!', 2, '!', 3, '!', 4, '!', 5] - - >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) - [1, 2, None, 3, 4, None, 5] - - """ - if n == 0: - raise ValueError('n must be > 0') - elif n == 1: - # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2... - # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2... - return islice(interleave(repeat(e), iterable), 1, None) - else: - # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... - # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... - # flatten(...) -> x_0, x_1, e, x_2, x_3... - filler = repeat([e]) - chunks = chunked(iterable, n) - return flatten(islice(interleave(filler, chunks), 1, None)) - - -def unique_to_each(*iterables): - """Return the elements from each of the input iterables that aren't in the - other input iterables. - - For example, suppose you have a set of packages, each with a set of - dependencies:: - - {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} - - If you remove one package, which dependencies can also be removed? - - If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not - associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for - ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: - - >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) - [['A'], ['C'], ['D']] - - If there are duplicates in one input iterable that aren't in the others - they will be duplicated in the output. Input order is preserved:: - - >>> unique_to_each("mississippi", "missouri") - [['p', 'p'], ['o', 'u', 'r']] - - It is assumed that the elements of each iterable are hashable. - - """ - pool = [list(it) for it in iterables] - counts = Counter(chain.from_iterable(map(set, pool))) - uniques = {element for element in counts if counts[element] == 1} - return [list(filter(uniques.__contains__, it)) for it in pool] - - -def windowed(seq, n, fillvalue=None, step=1): - """Return a sliding window of width *n* over the given iterable. - - >>> all_windows = windowed([1, 2, 3, 4, 5], 3) - >>> list(all_windows) - [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - - When the window is larger than the iterable, *fillvalue* is used in place - of missing values:: - - >>> list(windowed([1, 2, 3], 4)) - [(1, 2, 3, None)] - - Each window will advance in increments of *step*: - - >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) - [(1, 2, 3), (3, 4, 5), (5, 6, '!')] - - """ - if n < 0: - raise ValueError('n must be >= 0') - if n == 0: - yield tuple() - return - if step < 1: - raise ValueError('step must be >= 1') - - it = iter(seq) - window = deque([], n) - append = window.append - - # Initial deque fill - for _ in range(n): - append(next(it, fillvalue)) - yield tuple(window) - - # Appending new items to the right causes old items to fall off the left - i = 0 - for item in it: - append(item) - i = (i + 1) % step - if i % step == 0: - yield tuple(window) - - # If there are items from the iterable in the window, pad with the given - # value and emit them. - if (i % step) and (step - i < n): - for _ in range(step - i): - append(fillvalue) - yield tuple(window) - - -class bucket(object): - """Wrap *iterable* and return an object that buckets it iterable into - child iterables based on a *key* function. - - >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] - >>> s = bucket(iterable, key=lambda x: x[0]) - >>> a_iterable = s['a'] - >>> next(a_iterable) - 'a1' - >>> next(a_iterable) - 'a2' - >>> list(s['b']) - ['b1', 'b2', 'b3'] - - The original iterable will be advanced and its items will be cached until - they are used by the child iterables. This may require significant storage. - - By default, attempting to select a bucket to which no items belong will - exhaust the iterable and cache all values. - If you specify a *validator* function, selected buckets will instead be - checked against it. - - >>> from itertools import count - >>> it = count(1, 2) # Infinite sequence of odd numbers - >>> key = lambda x: x % 10 # Bucket by last digit - >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only - >>> s = bucket(it, key=key, validator=validator) - >>> 2 in s - False - >>> list(s[2]) - [] - - """ - def __init__(self, iterable, key, validator=None): - self._it = iter(iterable) - self._key = key - self._cache = defaultdict(deque) - self._validator = validator or (lambda x: True) - - def __contains__(self, value): - if not self._validator(value): - return False - - try: - item = next(self[value]) - except StopIteration: - return False - else: - self._cache[value].appendleft(item) - - return True - - def _get_values(self, value): - """ - Helper to yield items from the parent iterator that match *value*. - Items that don't match are stored in the local cache as they - are encountered. - """ - while True: - # If we've cached some items that match the target value, emit - # the first one and evict it from the cache. - if self._cache[value]: - yield self._cache[value].popleft() - # Otherwise we need to advance the parent iterator to search for - # a matching item, caching the rest. - else: - while True: - try: - item = next(self._it) - except StopIteration: - return - item_value = self._key(item) - if item_value == value: - yield item - break - elif self._validator(item_value): - self._cache[item_value].append(item) - - def __getitem__(self, value): - if not self._validator(value): - return iter(()) - - return self._get_values(value) - - -def spy(iterable, n=1): - """Return a 2-tuple with a list containing the first *n* elements of - *iterable*, and an iterator with the same items as *iterable*. - This allows you to "look ahead" at the items in the iterable without - advancing it. - - There is one item in the list by default: - - >>> iterable = 'abcdefg' - >>> head, iterable = spy(iterable) - >>> head - ['a'] - >>> list(iterable) - ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - - You may use unpacking to retrieve items instead of lists: - - >>> (head,), iterable = spy('abcdefg') - >>> head - 'a' - >>> (first, second), iterable = spy('abcdefg', 2) - >>> first - 'a' - >>> second - 'b' - - The number of items requested can be larger than the number of items in - the iterable: - - >>> iterable = [1, 2, 3, 4, 5] - >>> head, iterable = spy(iterable, 10) - >>> head - [1, 2, 3, 4, 5] - >>> list(iterable) - [1, 2, 3, 4, 5] - - """ - it = iter(iterable) - head = take(n, it) - - return head, chain(head, it) - - -def interleave(*iterables): - """Return a new iterable yielding from each iterable in turn, - until the shortest is exhausted. - - >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7] - - For a version that doesn't terminate after the shortest iterable is - exhausted, see :func:`interleave_longest`. - - """ - return chain.from_iterable(zip(*iterables)) - - -def interleave_longest(*iterables): - """Return a new iterable yielding from each iterable in turn, - skipping any that are exhausted. - - >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7, 3, 8] - - This function produces the same output as :func:`roundrobin`, but may - perform better for some inputs (in particular when the number of iterables - is large). - - """ - i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) - return (x for x in i if x is not _marker) - - -def collapse(iterable, base_type=None, levels=None): - """Flatten an iterable with multiple levels of nesting (e.g., a list of - lists of tuples) into non-iterable types. - - >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] - >>> list(collapse(iterable)) - [1, 2, 3, 4, 5, 6] - - String types are not considered iterable and will not be collapsed. - To avoid collapsing other types, specify *base_type*: - - >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] - >>> list(collapse(iterable, base_type=tuple)) - ['ab', ('cd', 'ef'), 'gh', 'ij'] - - Specify *levels* to stop flattening after a certain level: - - >>> iterable = [('a', ['b']), ('c', ['d'])] - >>> list(collapse(iterable)) # Fully flattened - ['a', 'b', 'c', 'd'] - >>> list(collapse(iterable, levels=1)) # Only one level flattened - ['a', ['b'], 'c', ['d']] - - """ - def walk(node, level): - if ( - ((levels is not None) and (level > levels)) or - isinstance(node, string_types) or - ((base_type is not None) and isinstance(node, base_type)) - ): - yield node - return - - try: - tree = iter(node) - except TypeError: - yield node - return - else: - for child in tree: - for x in walk(child, level + 1): - yield x - - for x in walk(iterable, 0): - yield x - - -def side_effect(func, iterable, chunk_size=None, before=None, after=None): - """Invoke *func* on each item in *iterable* (or on each *chunk_size* group - of items) before yielding the item. - - `func` must be a function that takes a single argument. Its return value - will be discarded. - - *before* and *after* are optional functions that take no arguments. They - will be executed before iteration starts and after it ends, respectively. - - `side_effect` can be used for logging, updating progress bars, or anything - that is not functionally "pure." - - Emitting a status message: - - >>> from more_itertools import consume - >>> func = lambda item: print('Received {}'.format(item)) - >>> consume(side_effect(func, range(2))) - Received 0 - Received 1 - - Operating on chunks of items: - - >>> pair_sums = [] - >>> func = lambda chunk: pair_sums.append(sum(chunk)) - >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) - [0, 1, 2, 3, 4, 5] - >>> list(pair_sums) - [1, 5, 9] - - Writing to a file-like object: - - >>> from io import StringIO - >>> from more_itertools import consume - >>> f = StringIO() - >>> func = lambda x: print(x, file=f) - >>> before = lambda: print(u'HEADER', file=f) - >>> after = f.close - >>> it = [u'a', u'b', u'c'] - >>> consume(side_effect(func, it, before=before, after=after)) - >>> f.closed - True - - """ - try: - if before is not None: - before() - - if chunk_size is None: - for item in iterable: - func(item) - yield item - else: - for chunk in chunked(iterable, chunk_size): - func(chunk) - for item in chunk: - yield item - finally: - if after is not None: - after() - - -def sliced(seq, n): - """Yield slices of length *n* from the sequence *seq*. - - >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) - [(1, 2, 3), (4, 5, 6)] - - If the length of the sequence is not divisible by the requested slice - length, the last slice will be shorter. - - >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) - [(1, 2, 3), (4, 5, 6), (7, 8)] - - This function will only work for iterables that support slicing. - For non-sliceable iterables, see :func:`chunked`. - - """ - return takewhile(bool, (seq[i: i + n] for i in count(0, n))) - - -def split_at(iterable, pred): - """Yield lists of items from *iterable*, where each list is delimited by - an item where callable *pred* returns ``True``. The lists do not include - the delimiting items. - - >>> list(split_at('abcdcba', lambda x: x == 'b')) - [['a'], ['c', 'd', 'c'], ['a']] - - >>> list(split_at(range(10), lambda n: n % 2 == 1)) - [[0], [2], [4], [6], [8], []] - """ - buf = [] - for item in iterable: - if pred(item): - yield buf - buf = [] - else: - buf.append(item) - yield buf - - -def split_before(iterable, pred): - """Yield lists of items from *iterable*, where each list starts with an - item where callable *pred* returns ``True``: - - >>> list(split_before('OneTwo', lambda s: s.isupper())) - [['O', 'n', 'e'], ['T', 'w', 'o']] - - >>> list(split_before(range(10), lambda n: n % 3 == 0)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] - - """ - buf = [] - for item in iterable: - if pred(item) and buf: - yield buf - buf = [] - buf.append(item) - yield buf - - -def split_after(iterable, pred): - """Yield lists of items from *iterable*, where each list ends with an - item where callable *pred* returns ``True``: - - >>> list(split_after('one1two2', lambda s: s.isdigit())) - [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] - - >>> list(split_after(range(10), lambda n: n % 3 == 0)) - [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] - - """ - buf = [] - for item in iterable: - buf.append(item) - if pred(item) and buf: - yield buf - buf = [] - if buf: - yield buf - - -def padded(iterable, fillvalue=None, n=None, next_multiple=False): - """Yield the elements from *iterable*, followed by *fillvalue*, such that - at least *n* items are emitted. - - >>> list(padded([1, 2, 3], '?', 5)) - [1, 2, 3, '?', '?'] - - If *next_multiple* is ``True``, *fillvalue* will be emitted until the - number of items emitted is a multiple of *n*:: - - >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) - [1, 2, 3, 4, None, None] - - If *n* is ``None``, *fillvalue* will be emitted indefinitely. - - """ - it = iter(iterable) - if n is None: - for item in chain(it, repeat(fillvalue)): - yield item - elif n < 1: - raise ValueError('n must be at least 1') - else: - item_count = 0 - for item in it: - yield item - item_count += 1 - - remaining = (n - item_count) % n if next_multiple else n - item_count - for _ in range(remaining): - yield fillvalue - - -def distribute(n, iterable): - """Distribute the items from *iterable* among *n* smaller iterables. - - >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 3, 5] - >>> list(group_2) - [2, 4, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 4, 7], [2, 5], [3, 6]] - - If the length of *iterable* is smaller than *n*, then the last returned - iterables will be empty: - - >>> children = distribute(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function uses :func:`itertools.tee` and may require significant - storage. If you need the order items in the smaller iterables to match the - original iterable, see :func:`divide`. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - children = tee(iterable, n) - return [islice(it, index, None, n) for index, it in enumerate(children)] - - -def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): - """Yield tuples whose elements are offset from *iterable*. - The amount by which the `i`-th item in each tuple is offset is given by - the `i`-th item in *offsets*. - - >>> list(stagger([0, 1, 2, 3])) - [(None, 0, 1), (0, 1, 2), (1, 2, 3)] - >>> list(stagger(range(8), offsets=(0, 2, 4))) - [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] - - By default, the sequence will end when the final element of a tuple is the - last item in the iterable. To continue until the first element of a tuple - is the last item in the iterable, set *longest* to ``True``:: - - >>> list(stagger([0, 1, 2, 3], longest=True)) - [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - children = tee(iterable, len(offsets)) - - return zip_offset( - *children, offsets=offsets, longest=longest, fillvalue=fillvalue - ) - - -def zip_offset(*iterables, **kwargs): - """``zip`` the input *iterables* together, but offset the `i`-th iterable - by the `i`-th item in *offsets*. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] - - This can be used as a lightweight alternative to SciPy or pandas to analyze - data sets in which somes series have a lead or lag relationship. - - By default, the sequence will end when the shortest iterable is exhausted. - To continue until the longest iterable is exhausted, set *longest* to - ``True``. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - offsets = kwargs['offsets'] - longest = kwargs.get('longest', False) - fillvalue = kwargs.get('fillvalue', None) - - if len(iterables) != len(offsets): - raise ValueError("Number of iterables and offsets didn't match") - - staggered = [] - for it, n in zip(iterables, offsets): - if n < 0: - staggered.append(chain(repeat(fillvalue, -n), it)) - elif n > 0: - staggered.append(islice(it, n, None)) - else: - staggered.append(it) - - if longest: - return zip_longest(*staggered, fillvalue=fillvalue) - - return zip(*staggered) - - -def sort_together(iterables, key_list=(0,), reverse=False): - """Return the input iterables sorted together, with *key_list* as the - priority for sorting. All iterables are trimmed to the length of the - shortest one. - - This can be used like the sorting function in a spreadsheet. If each - iterable represents a column of data, the key list determines which - columns are used for sorting. - - By default, all iterables are sorted using the ``0``-th iterable:: - - >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] - >>> sort_together(iterables) - [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] - - Set a different key list to sort according to another iterable. - Specifying mutliple keys dictates how ties are broken:: - - >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] - >>> sort_together(iterables, key_list=(1, 2)) - [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] - - Set *reverse* to ``True`` to sort in descending order. - - >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) - [(3, 2, 1), ('a', 'b', 'c')] - - """ - return list(zip(*sorted(zip(*iterables), - key=itemgetter(*key_list), - reverse=reverse))) - - -def divide(n, iterable): - """Divide the elements from *iterable* into *n* parts, maintaining - order. - - >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 2, 3] - >>> list(group_2) - [4, 5, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 2, 3], [4, 5], [6, 7]] - - If the length of the iterable is smaller than n, then the last returned - iterables will be empty: - - >>> children = divide(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function will exhaust the iterable before returning and may require - significant storage. If order is not important, see :func:`distribute`, - which does not first pull the iterable into memory. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - seq = tuple(iterable) - q, r = divmod(len(seq), n) - - ret = [] - for i in range(n): - start = (i * q) + (i if i < r else r) - stop = ((i + 1) * q) + (i + 1 if i + 1 < r else r) - ret.append(iter(seq[start:stop])) - - return ret - - -def always_iterable(obj, base_type=(text_type, binary_type)): - """If *obj* is iterable, return an iterator over its items:: - - >>> obj = (1, 2, 3) - >>> list(always_iterable(obj)) - [1, 2, 3] - - If *obj* is not iterable, return a one-item iterable containing *obj*:: - - >>> obj = 1 - >>> list(always_iterable(obj)) - [1] - - If *obj* is ``None``, return an empty iterable: - - >>> obj = None - >>> list(always_iterable(None)) - [] - - By default, binary and text strings are not considered iterable:: - - >>> obj = 'foo' - >>> list(always_iterable(obj)) - ['foo'] - - If *base_type* is set, objects for which ``isinstance(obj, base_type)`` - returns ``True`` won't be considered iterable. - - >>> obj = {'a': 1} - >>> list(always_iterable(obj)) # Iterate over the dict's keys - ['a'] - >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit - [{'a': 1}] - - Set *base_type* to ``None`` to avoid any special handling and treat objects - Python considers iterable as iterable: - - >>> obj = 'foo' - >>> list(always_iterable(obj, base_type=None)) - ['f', 'o', 'o'] - """ - if obj is None: - return iter(()) - - if (base_type is not None) and isinstance(obj, base_type): - return iter((obj,)) - - try: - return iter(obj) - except TypeError: - return iter((obj,)) - - -def adjacent(predicate, iterable, distance=1): - """Return an iterable over `(bool, item)` tuples where the `item` is - drawn from *iterable* and the `bool` indicates whether - that item satisfies the *predicate* or is adjacent to an item that does. - - For example, to find whether items are adjacent to a ``3``:: - - >>> list(adjacent(lambda x: x == 3, range(6))) - [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] - - Set *distance* to change what counts as adjacent. For example, to find - whether items are two places away from a ``3``: - - >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) - [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] - - This is useful for contextualizing the results of a search function. - For example, a code comparison tool might want to identify lines that - have changed, but also surrounding lines to give the viewer of the diff - context. - - The predicate function will only be called once for each item in the - iterable. - - See also :func:`groupby_transform`, which can be used with this function - to group ranges of items with the same `bool` value. - - """ - # Allow distance=0 mainly for testing that it reproduces results with map() - if distance < 0: - raise ValueError('distance must be at least 0') - - i1, i2 = tee(iterable) - padding = [False] * distance - selected = chain(padding, map(predicate, i1), padding) - adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) - return zip(adjacent_to_selected, i2) - - -def groupby_transform(iterable, keyfunc=None, valuefunc=None): - """An extension of :func:`itertools.groupby` that transforms the values of - *iterable* after grouping them. - *keyfunc* is a function used to compute a grouping key for each item. - *valuefunc* is a function for transforming the items after grouping. - - >>> iterable = 'AaaABbBCcA' - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: x.lower() - >>> grouper = groupby_transform(iterable, keyfunc, valuefunc) - >>> [(k, ''.join(g)) for k, g in grouper] - [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')] - - *keyfunc* and *valuefunc* default to identity functions if they are not - specified. - - :func:`groupby_transform` is useful when grouping elements of an iterable - using a separate iterable as the key. To do this, :func:`zip` the iterables - and pass a *keyfunc* that extracts the first element and a *valuefunc* - that extracts the second element:: - - >>> from operator import itemgetter - >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] - >>> values = 'abcdefghi' - >>> iterable = zip(keys, values) - >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) - >>> [(k, ''.join(g)) for k, g in grouper] - [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] - - Note that the order of items in the iterable is significant. - Only adjacent items are grouped together, so if you don't want any - duplicate groups, you should sort the iterable by the key function. - - """ - valuefunc = (lambda x: x) if valuefunc is None else valuefunc - return ((k, map(valuefunc, g)) for k, g in groupby(iterable, keyfunc)) - - -def numeric_range(*args): - """An extension of the built-in ``range()`` function whose arguments can - be any orderable numeric type. - - With only *stop* specified, *start* defaults to ``0`` and *step* - defaults to ``1``. The output items will match the type of *stop*: - - >>> list(numeric_range(3.5)) - [0.0, 1.0, 2.0, 3.0] - - With only *start* and *stop* specified, *step* defaults to ``1``. The - output items will match the type of *start*: - - >>> from decimal import Decimal - >>> start = Decimal('2.1') - >>> stop = Decimal('5.1') - >>> list(numeric_range(start, stop)) - [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] - - With *start*, *stop*, and *step* specified the output items will match - the type of ``start + step``: - - >>> from fractions import Fraction - >>> start = Fraction(1, 2) # Start at 1/2 - >>> stop = Fraction(5, 2) # End at 5/2 - >>> step = Fraction(1, 2) # Count by 1/2 - >>> list(numeric_range(start, stop, step)) - [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] - - If *step* is zero, ``ValueError`` is raised. Negative steps are supported: - - >>> list(numeric_range(3, -1, -1.0)) - [3.0, 2.0, 1.0, 0.0] - - Be aware of the limitations of floating point numbers; the representation - of the yielded numbers may be surprising. - - """ - argc = len(args) - if argc == 1: - stop, = args - start = type(stop)(0) - step = 1 - elif argc == 2: - start, stop = args - step = 1 - elif argc == 3: - start, stop, step = args - else: - err_msg = 'numeric_range takes at most 3 arguments, got {}' - raise TypeError(err_msg.format(argc)) - - values = (start + (step * n) for n in count()) - if step > 0: - return takewhile(partial(gt, stop), values) - elif step < 0: - return takewhile(partial(lt, stop), values) - else: - raise ValueError('numeric_range arg 3 must not be zero') - - -def count_cycle(iterable, n=None): - """Cycle through the items from *iterable* up to *n* times, yielding - the number of completed cycles along with each item. If *n* is omitted the - process repeats indefinitely. - - >>> list(count_cycle('AB', 3)) - [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] - - """ - iterable = tuple(iterable) - if not iterable: - return iter(()) - counter = count() if n is None else range(n) - return ((i, item) for i in counter for item in iterable) - - -def locate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(locate([0, 1, 1, 0, 1, 0, 0])) - [1, 2, 4] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item. - - >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) - [1, 3] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(locate(iterable, pred=pred, window_size=3)) - [1, 5, 9] - - Use with :func:`seekable` to find indexes and then retrieve the associated - items: - - >>> from itertools import count - >>> from more_itertools import seekable - >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) - >>> it = seekable(source) - >>> pred = lambda x: x > 100 - >>> indexes = locate(it, pred=pred) - >>> i = next(indexes) - >>> it.seek(i) - >>> next(it) - 106 - - """ - if window_size is None: - return compress(count(), map(pred, iterable)) - - if window_size < 1: - raise ValueError('window size must be at least 1') - - it = windowed(iterable, window_size, fillvalue=_marker) - return compress(count(), starmap(pred, it)) - - -def lstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the beginning - for which *pred* returns ``True``. - - For example, to remove a set of items from the start of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(lstrip(iterable, pred)) - [1, 2, None, 3, False, None] - - This function is analogous to to :func:`str.lstrip`, and is essentially - an wrapper for :func:`itertools.dropwhile`. - - """ - return dropwhile(pred, iterable) - - -def rstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the end - for which *pred* returns ``True``. - - For example, to remove a set of items from the end of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(rstrip(iterable, pred)) - [None, False, None, 1, 2, None, 3] - - This function is analogous to :func:`str.rstrip`. - - """ - cache = [] - cache_append = cache.append - for x in iterable: - if pred(x): - cache_append(x) - else: - for y in cache: - yield y - del cache[:] - yield x - - -def strip(iterable, pred): - """Yield the items from *iterable*, but strip any from the - beginning and end for which *pred* returns ``True``. - - For example, to remove a set of items from both ends of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(strip(iterable, pred)) - [1, 2, None, 3] - - This function is analogous to :func:`str.strip`. - - """ - return rstrip(lstrip(iterable, pred), pred) - - -def islice_extended(iterable, *args): - """An extension of :func:`itertools.islice` that supports negative values - for *stop*, *start*, and *step*. - - >>> iterable = iter('abcdefgh') - >>> list(islice_extended(iterable, -4, -1)) - ['e', 'f', 'g'] - - Slices with negative values require some caching of *iterable*, but this - function takes care to minimize the amount of memory required. - - For example, you can use a negative step with an infinite iterator: - - >>> from itertools import count - >>> list(islice_extended(count(), 110, 99, -2)) - [110, 108, 106, 104, 102, 100] - - """ - s = slice(*args) - start = s.start - stop = s.stop - if s.step == 0: - raise ValueError('step argument must be a non-zero integer or None.') - step = s.step or 1 - - it = iter(iterable) - - if step > 0: - start = 0 if (start is None) else start - - if (start < 0): - # Consume all but the last -start items - cache = deque(enumerate(it, 1), maxlen=-start) - len_iter = cache[-1][0] if cache else 0 - - # Adjust start to be positive - i = max(len_iter + start, 0) - - # Adjust stop to be positive - if stop is None: - j = len_iter - elif stop >= 0: - j = min(stop, len_iter) - else: - j = max(len_iter + stop, 0) - - # Slice the cache - n = j - i - if n <= 0: - return - - for index, item in islice(cache, 0, n, step): - yield item - elif (stop is not None) and (stop < 0): - # Advance to the start position - next(islice(it, start, start), None) - - # When stop is negative, we have to carry -stop items while - # iterating - cache = deque(islice(it, -stop), maxlen=-stop) - - for index, item in enumerate(it): - cached_item = cache.popleft() - if index % step == 0: - yield cached_item - cache.append(item) - else: - # When both start and stop are positive we have the normal case - for item in islice(it, start, stop, step): - yield item - else: - start = -1 if (start is None) else start - - if (stop is not None) and (stop < 0): - # Consume all but the last items - n = -stop - 1 - cache = deque(enumerate(it, 1), maxlen=n) - len_iter = cache[-1][0] if cache else 0 - - # If start and stop are both negative they are comparable and - # we can just slice. Otherwise we can adjust start to be negative - # and then slice. - if start < 0: - i, j = start, stop - else: - i, j = min(start - len_iter, -1), None - - for index, item in list(cache)[i:j:step]: - yield item - else: - # Advance to the stop position - if stop is not None: - m = stop + 1 - next(islice(it, m, m), None) - - # stop is positive, so if start is negative they are not comparable - # and we need the rest of the items. - if start < 0: - i = start - n = None - # stop is None and start is positive, so we just need items up to - # the start index. - elif stop is None: - i = None - n = start + 1 - # Both stop and start are positive, so they are comparable. - else: - i = None - n = start - stop - if n <= 0: - return - - cache = list(islice(it, n)) - - for item in cache[i::step]: - yield item - - -def always_reversible(iterable): - """An extension of :func:`reversed` that supports all iterables, not - just those which implement the ``Reversible`` or ``Sequence`` protocols. - - >>> print(*always_reversible(x for x in range(3))) - 2 1 0 - - If the iterable is already reversible, this function returns the - result of :func:`reversed()`. If the iterable is not reversible, - this function will cache the remaining items in the iterable and - yield them in reverse order, which may require significant storage. - """ - try: - return reversed(iterable) - except TypeError: - return reversed(list(iterable)) - - -def consecutive_groups(iterable, ordering=lambda x: x): - """Yield groups of consecutive items using :func:`itertools.groupby`. - The *ordering* function determines whether two items are adjacent by - returning their position. - - By default, the ordering function is the identity function. This is - suitable for finding runs of numbers: - - >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] - >>> for group in consecutive_groups(iterable): - ... print(list(group)) - [1] - [10, 11, 12] - [20] - [30, 31, 32, 33] - [40] - - For finding runs of adjacent letters, try using the :meth:`index` method - of a string of letters: - - >>> from string import ascii_lowercase - >>> iterable = 'abcdfgilmnop' - >>> ordering = ascii_lowercase.index - >>> for group in consecutive_groups(iterable, ordering): - ... print(list(group)) - ['a', 'b', 'c', 'd'] - ['f', 'g'] - ['i'] - ['l', 'm', 'n', 'o', 'p'] - - """ - for k, g in groupby( - enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) - ): - yield map(itemgetter(1), g) - - -def difference(iterable, func=sub): - """By default, compute the first difference of *iterable* using - :func:`operator.sub`. - - >>> iterable = [0, 1, 3, 6, 10] - >>> list(difference(iterable)) - [0, 1, 2, 3, 4] - - This is the opposite of :func:`accumulate`'s default behavior: - - >>> from more_itertools import accumulate - >>> iterable = [0, 1, 2, 3, 4] - >>> list(accumulate(iterable)) - [0, 1, 3, 6, 10] - >>> list(difference(accumulate(iterable))) - [0, 1, 2, 3, 4] - - By default *func* is :func:`operator.sub`, but other functions can be - specified. They will be applied as follows:: - - A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... - - For example, to do progressive division: - - >>> iterable = [1, 2, 6, 24, 120] # Factorial sequence - >>> func = lambda x, y: x // y - >>> list(difference(iterable, func)) - [1, 2, 3, 4, 5] - - """ - a, b = tee(iterable) - try: - item = next(b) - except StopIteration: - return iter([]) - return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b))) - - -class SequenceView(Sequence): - """Return a read-only view of the sequence object *target*. - - :class:`SequenceView` objects are analagous to Python's built-in - "dictionary view" types. They provide a dynamic view of a sequence's items, - meaning that when the sequence updates, so does the view. - - >>> seq = ['0', '1', '2'] - >>> view = SequenceView(seq) - >>> view - SequenceView(['0', '1', '2']) - >>> seq.append('3') - >>> view - SequenceView(['0', '1', '2', '3']) - - Sequence views support indexing, slicing, and length queries. They act - like the underlying sequence, except they don't allow assignment: - - >>> view[1] - '1' - >>> view[1:-1] - ['1', '2'] - >>> len(view) - 4 - - Sequence views are useful as an alternative to copying, as they don't - require (much) extra storage. - - """ - def __init__(self, target): - if not isinstance(target, Sequence): - raise TypeError - self._target = target - - def __getitem__(self, index): - return self._target[index] - - def __len__(self): - return len(self._target) - - def __repr__(self): - return '{}({})'.format(self.__class__.__name__, repr(self._target)) - - -class seekable(object): - """Wrap an iterator to allow for seeking backward and forward. This - progressively caches the items in the source iterable so they can be - re-visited. - - Call :meth:`seek` with an index to seek to that position in the source - iterable. - - To "reset" an iterator, seek to ``0``: - - >>> from itertools import count - >>> it = seekable((str(n) for n in count())) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> it.seek(0) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> next(it) - '3' - - You can also seek forward: - - >>> it = seekable((str(n) for n in range(20))) - >>> it.seek(10) - >>> next(it) - '10' - >>> it.seek(20) # Seeking past the end of the source isn't a problem - >>> list(it) - [] - >>> it.seek(0) # Resetting works even after hitting the end - >>> next(it), next(it), next(it) - ('0', '1', '2') - - The cache grows as the source iterable progresses, so beware of wrapping - very large or infinite iterables. - - You may view the contents of the cache with the :meth:`elements` method. - That returns a :class:`SequenceView`, a view that updates automatically: - - >>> it = seekable((str(n) for n in range(10))) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> elements = it.elements() - >>> elements - SequenceView(['0', '1', '2']) - >>> next(it) - '3' - >>> elements - SequenceView(['0', '1', '2', '3']) - - """ - - def __init__(self, iterable): - self._source = iter(iterable) - self._cache = [] - self._index = None - - def __iter__(self): - return self - - def __next__(self): - if self._index is not None: - try: - item = self._cache[self._index] - except IndexError: - self._index = None - else: - self._index += 1 - return item - - item = next(self._source) - self._cache.append(item) - return item - - next = __next__ - - def elements(self): - return SequenceView(self._cache) - - def seek(self, index): - self._index = index - remainder = index - len(self._cache) - if remainder > 0: - consume(self, remainder) - - -class run_length(object): - """ - :func:`run_length.encode` compresses an iterable with run-length encoding. - It yields groups of repeated items with the count of how many times they - were repeated: - - >>> uncompressed = 'abbcccdddd' - >>> list(run_length.encode(uncompressed)) - [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - - :func:`run_length.decode` decompresses an iterable that was previously - compressed with run-length encoding. It yields the items of the - decompressed iterable: - - >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - >>> list(run_length.decode(compressed)) - ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] - - """ - - @staticmethod - def encode(iterable): - return ((k, ilen(g)) for k, g in groupby(iterable)) - - @staticmethod - def decode(iterable): - return chain.from_iterable(repeat(k, n) for k, n in iterable) - - -def exactly_n(iterable, n, predicate=bool): - """Return ``True`` if exactly ``n`` items in the iterable are ``True`` - according to the *predicate* function. - - >>> exactly_n([True, True, False], 2) - True - >>> exactly_n([True, True, False], 1) - False - >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) - True - - The iterable will be advanced until ``n + 1`` truthy items are encountered, - so avoid calling it on infinite iterables. - - """ - return len(take(n + 1, filter(predicate, iterable))) == n - - -def circular_shifts(iterable): - """Return a list of circular shifts of *iterable*. - - >>> circular_shifts(range(4)) - [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] - """ - lst = list(iterable) - return take(len(lst), windowed(cycle(lst), len(lst))) - - -def make_decorator(wrapping_func, result_index=0): - """Return a decorator version of *wrapping_func*, which is a function that - modifies an iterable. *result_index* is the position in that function's - signature where the iterable goes. - - This lets you use itertools on the "production end," i.e. at function - definition. This can augment what the function returns without changing the - function's code. - - For example, to produce a decorator version of :func:`chunked`: - - >>> from more_itertools import chunked - >>> chunker = make_decorator(chunked, result_index=0) - >>> @chunker(3) - ... def iter_range(n): - ... return iter(range(n)) - ... - >>> list(iter_range(9)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8]] - - To only allow truthy items to be returned: - - >>> truth_serum = make_decorator(filter, result_index=1) - >>> @truth_serum(bool) - ... def boolean_test(): - ... return [0, 1, '', ' ', False, True] - ... - >>> list(boolean_test()) - [1, ' ', True] - - The :func:`peekable` and :func:`seekable` wrappers make for practical - decorators: - - >>> from more_itertools import peekable - >>> peekable_function = make_decorator(peekable) - >>> @peekable_function() - ... def str_range(*args): - ... return (str(x) for x in range(*args)) - ... - >>> it = str_range(1, 20, 2) - >>> next(it), next(it), next(it) - ('1', '3', '5') - >>> it.peek() - '7' - >>> next(it) - '7' - - """ - # See https://sites.google.com/site/bbayles/index/decorator_factory for - # notes on how this works. - def decorator(*wrapping_args, **wrapping_kwargs): - def outer_wrapper(f): - def inner_wrapper(*args, **kwargs): - result = f(*args, **kwargs) - wrapping_args_ = list(wrapping_args) - wrapping_args_.insert(result_index, result) - return wrapping_func(*wrapping_args_, **wrapping_kwargs) - - return inner_wrapper - - return outer_wrapper - - return decorator - - -def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): - """Return a dictionary that maps the items in *iterable* to categories - defined by *keyfunc*, transforms them with *valuefunc*, and - then summarizes them by category with *reducefunc*. - - *valuefunc* defaults to the identity function if it is unspecified. - If *reducefunc* is unspecified, no summarization takes place: - - >>> keyfunc = lambda x: x.upper() - >>> result = map_reduce('abbccc', keyfunc) - >>> sorted(result.items()) - [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] - - Specifying *valuefunc* transforms the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> result = map_reduce('abbccc', keyfunc, valuefunc) - >>> sorted(result.items()) - [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] - - Specifying *reducefunc* summarizes the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> reducefunc = sum - >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) - >>> sorted(result.items()) - [('A', 1), ('B', 2), ('C', 3)] - - You may want to filter the input iterable before applying the map/reduce - procedure: - - >>> all_items = range(30) - >>> items = [x for x in all_items if 10 <= x <= 20] # Filter - >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 - >>> categories = map_reduce(items, keyfunc=keyfunc) - >>> sorted(categories.items()) - [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] - >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) - >>> sorted(summaries.items()) - [(0, 90), (1, 75)] - - Note that all items in the iterable are gathered into a list before the - summarization step, which may require significant storage. - - The returned object is a :obj:`collections.defaultdict` with the - ``default_factory`` set to ``None``, such that it behaves like a normal - dictionary. - - """ - valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc - - ret = defaultdict(list) - for item in iterable: - key = keyfunc(item) - value = valuefunc(item) - ret[key].append(value) - - if reducefunc is not None: - for key, value_list in ret.items(): - ret[key] = reducefunc(value_list) - - ret.default_factory = None - return ret - - -def rlocate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``, starting from the right and moving left. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 - [4, 2, 1] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item: - - >>> iterable = iter('abcb') - >>> pred = lambda x: x == 'b' - >>> list(rlocate(iterable, pred)) - [3, 1] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(rlocate(iterable, pred=pred, window_size=3)) - [9, 5, 1] - - Beware, this function won't return anything for infinite iterables. - If *iterable* is reversible, ``rlocate`` will reverse it and search from - the right. Otherwise, it will search from the left and return the results - in reverse order. - - See :func:`locate` to for other example applications. - - """ - if window_size is None: - try: - len_iter = len(iterable) - return ( - len_iter - i - 1 for i in locate(reversed(iterable), pred) - ) - except TypeError: - pass - - return reversed(list(locate(iterable, pred, window_size))) - - -def replace(iterable, pred, substitutes, count=None, window_size=1): - """Yield the items from *iterable*, replacing the items for which *pred* - returns ``True`` with the items from the iterable *substitutes*. - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] - >>> pred = lambda x: x == 0 - >>> substitutes = (2, 3) - >>> list(replace(iterable, pred, substitutes)) - [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] - - If *count* is given, the number of replacements will be limited: - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] - >>> pred = lambda x: x == 0 - >>> substitutes = [None] - >>> list(replace(iterable, pred, substitutes, count=2)) - [1, 1, None, 1, 1, None, 1, 1, 0] - - Use *window_size* to control the number of items passed as arguments to - *pred*. This allows for locating and replacing subsequences. - - >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] - >>> window_size = 3 - >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred - >>> substitutes = [3, 4] # Splice in these items - >>> list(replace(iterable, pred, substitutes, window_size=window_size)) - [3, 4, 5, 3, 4, 5] - - """ - if window_size < 1: - raise ValueError('window_size must be at least 1') - - # Save the substitutes iterable, since it's used more than once - substitutes = tuple(substitutes) - - # Add padding such that the number of windows matches the length of the - # iterable - it = chain(iterable, [_marker] * (window_size - 1)) - windows = windowed(it, window_size) - - n = 0 - for w in windows: - # If the current window matches our predicate (and we haven't hit - # our maximum number of replacements), splice in the substitutes - # and then consume the following windows that overlap with this one. - # For example, if the iterable is (0, 1, 2, 3, 4...) - # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... - # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) - if pred(*w): - if (count is None) or (n < count): - n += 1 - for s in substitutes: - yield s - consume(windows, window_size - 1) - continue - - # If there was no match (or we've reached the replacement limit), - # yield the first item from the window. - if w and (w[0] is not _marker): - yield w[0] diff --git a/libraries/more_itertools/recipes.py b/libraries/more_itertools/recipes.py deleted file mode 100644 index 3a7706cb..00000000 --- a/libraries/more_itertools/recipes.py +++ /dev/null @@ -1,565 +0,0 @@ -"""Imported from the recipes section of the itertools documentation. - -All functions taken from the recipes section of the itertools library docs -[1]_. -Some backward-compatible usability improvements have been made. - -.. [1] http://docs.python.org/library/itertools.html#recipes - -""" -from collections import deque -from itertools import ( - chain, combinations, count, cycle, groupby, islice, repeat, starmap, tee -) -import operator -from random import randrange, sample, choice - -from six import PY2 -from six.moves import filter, filterfalse, map, range, zip, zip_longest - -__all__ = [ - 'accumulate', - 'all_equal', - 'consume', - 'dotproduct', - 'first_true', - 'flatten', - 'grouper', - 'iter_except', - 'ncycles', - 'nth', - 'nth_combination', - 'padnone', - 'pairwise', - 'partition', - 'powerset', - 'prepend', - 'quantify', - 'random_combination_with_replacement', - 'random_combination', - 'random_permutation', - 'random_product', - 'repeatfunc', - 'roundrobin', - 'tabulate', - 'tail', - 'take', - 'unique_everseen', - 'unique_justseen', -] - - -def accumulate(iterable, func=operator.add): - """ - Return an iterator whose items are the accumulated results of a function - (specified by the optional *func* argument) that takes two arguments. - By default, returns accumulated sums with :func:`operator.add`. - - >>> list(accumulate([1, 2, 3, 4, 5])) # Running sum - [1, 3, 6, 10, 15] - >>> list(accumulate([1, 2, 3], func=operator.mul)) # Running product - [1, 2, 6] - >>> list(accumulate([0, 1, -1, 2, 3, 2], func=max)) # Running maximum - [0, 1, 1, 2, 3, 3] - - This function is available in the ``itertools`` module for Python 3.2 and - greater. - - """ - it = iter(iterable) - try: - total = next(it) - except StopIteration: - return - else: - yield total - - for element in it: - total = func(total, element) - yield total - - -def take(n, iterable): - """Return first *n* items of the iterable as a list. - - >>> take(3, range(10)) - [0, 1, 2] - >>> take(5, range(3)) - [0, 1, 2] - - Effectively a short replacement for ``next`` based iterator consumption - when you want more than one item, but less than the whole iterator. - - """ - return list(islice(iterable, n)) - - -def tabulate(function, start=0): - """Return an iterator over the results of ``func(start)``, - ``func(start + 1)``, ``func(start + 2)``... - - *func* should be a function that accepts one integer argument. - - If *start* is not specified it defaults to 0. It will be incremented each - time the iterator is advanced. - - >>> square = lambda x: x ** 2 - >>> iterator = tabulate(square, -3) - >>> take(4, iterator) - [9, 4, 1, 0] - - """ - return map(function, count(start)) - - -def tail(n, iterable): - """Return an iterator over the last *n* items of *iterable*. - - >>> t = tail(3, 'ABCDEFG') - >>> list(t) - ['E', 'F', 'G'] - - """ - return iter(deque(iterable, maxlen=n)) - - -def consume(iterator, n=None): - """Advance *iterable* by *n* steps. If *n* is ``None``, consume it - entirely. - - Efficiently exhausts an iterator without returning values. Defaults to - consuming the whole iterator, but an optional second argument may be - provided to limit consumption. - - >>> i = (x for x in range(10)) - >>> next(i) - 0 - >>> consume(i, 3) - >>> next(i) - 4 - >>> consume(i) - >>> next(i) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - StopIteration - - If the iterator has fewer items remaining than the provided limit, the - whole iterator will be consumed. - - >>> i = (x for x in range(3)) - >>> consume(i, 5) - >>> next(i) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - StopIteration - - """ - # Use functions that consume iterators at C speed. - if n is None: - # feed the entire iterator into a zero-length deque - deque(iterator, maxlen=0) - else: - # advance to the empty slice starting at position n - next(islice(iterator, n, n), None) - - -def nth(iterable, n, default=None): - """Returns the nth item or a default value. - - >>> l = range(10) - >>> nth(l, 3) - 3 - >>> nth(l, 20, "zebra") - 'zebra' - - """ - return next(islice(iterable, n, None), default) - - -def all_equal(iterable): - """ - Returns ``True`` if all the elements are equal to each other. - - >>> all_equal('aaaa') - True - >>> all_equal('aaab') - False - - """ - g = groupby(iterable) - return next(g, True) and not next(g, False) - - -def quantify(iterable, pred=bool): - """Return the how many times the predicate is true. - - >>> quantify([True, False, True]) - 2 - - """ - return sum(map(pred, iterable)) - - -def padnone(iterable): - """Returns the sequence of elements and then returns ``None`` indefinitely. - - >>> take(5, padnone(range(3))) - [0, 1, 2, None, None] - - Useful for emulating the behavior of the built-in :func:`map` function. - - See also :func:`padded`. - - """ - return chain(iterable, repeat(None)) - - -def ncycles(iterable, n): - """Returns the sequence elements *n* times - - >>> list(ncycles(["a", "b"], 3)) - ['a', 'b', 'a', 'b', 'a', 'b'] - - """ - return chain.from_iterable(repeat(tuple(iterable), n)) - - -def dotproduct(vec1, vec2): - """Returns the dot product of the two iterables. - - >>> dotproduct([10, 10], [20, 20]) - 400 - - """ - return sum(map(operator.mul, vec1, vec2)) - - -def flatten(listOfLists): - """Return an iterator flattening one level of nesting in a list of lists. - - >>> list(flatten([[0, 1], [2, 3]])) - [0, 1, 2, 3] - - See also :func:`collapse`, which can flatten multiple levels of nesting. - - """ - return chain.from_iterable(listOfLists) - - -def repeatfunc(func, times=None, *args): - """Call *func* with *args* repeatedly, returning an iterable over the - results. - - If *times* is specified, the iterable will terminate after that many - repetitions: - - >>> from operator import add - >>> times = 4 - >>> args = 3, 5 - >>> list(repeatfunc(add, times, *args)) - [8, 8, 8, 8] - - If *times* is ``None`` the iterable will not terminate: - - >>> from random import randrange - >>> times = None - >>> args = 1, 11 - >>> take(6, repeatfunc(randrange, times, *args)) # doctest:+SKIP - [2, 4, 8, 1, 8, 4] - - """ - if times is None: - return starmap(func, repeat(args)) - return starmap(func, repeat(args, times)) - - -def pairwise(iterable): - """Returns an iterator of paired items, overlapping, from the original - - >>> take(4, pairwise(count())) - [(0, 1), (1, 2), (2, 3), (3, 4)] - - """ - a, b = tee(iterable) - next(b, None) - return zip(a, b) - - -def grouper(n, iterable, fillvalue=None): - """Collect data into fixed-length chunks or blocks. - - >>> list(grouper(3, 'ABCDEFG', 'x')) - [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] - - """ - args = [iter(iterable)] * n - return zip_longest(fillvalue=fillvalue, *args) - - -def roundrobin(*iterables): - """Yields an item from each iterable, alternating between them. - - >>> list(roundrobin('ABC', 'D', 'EF')) - ['A', 'D', 'E', 'B', 'F', 'C'] - - This function produces the same output as :func:`interleave_longest`, but - may perform better for some inputs (in particular when the number of - iterables is small). - - """ - # Recipe credited to George Sakkis - pending = len(iterables) - if PY2: - nexts = cycle(iter(it).next for it in iterables) - else: - nexts = cycle(iter(it).__next__ for it in iterables) - while pending: - try: - for next in nexts: - yield next() - except StopIteration: - pending -= 1 - nexts = cycle(islice(nexts, pending)) - - -def partition(pred, iterable): - """ - Returns a 2-tuple of iterables derived from the input iterable. - The first yields the items that have ``pred(item) == False``. - The second yields the items that have ``pred(item) == True``. - - >>> is_odd = lambda x: x % 2 != 0 - >>> iterable = range(10) - >>> even_items, odd_items = partition(is_odd, iterable) - >>> list(even_items), list(odd_items) - ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) - - """ - # partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 - t1, t2 = tee(iterable) - return filterfalse(pred, t1), filter(pred, t2) - - -def powerset(iterable): - """Yields all possible subsets of the iterable. - - >>> list(powerset([1,2,3])) - [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] - - """ - s = list(iterable) - return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) - - -def unique_everseen(iterable, key=None): - """ - Yield unique elements, preserving order. - - >>> list(unique_everseen('AAAABBBCCDAABBB')) - ['A', 'B', 'C', 'D'] - >>> list(unique_everseen('ABBCcAD', str.lower)) - ['A', 'B', 'C', 'D'] - - Sequences with a mix of hashable and unhashable items can be used. - The function will be slower (i.e., `O(n^2)`) for unhashable items. - - """ - seenset = set() - seenset_add = seenset.add - seenlist = [] - seenlist_add = seenlist.append - if key is None: - for element in iterable: - try: - if element not in seenset: - seenset_add(element) - yield element - except TypeError: - if element not in seenlist: - seenlist_add(element) - yield element - else: - for element in iterable: - k = key(element) - try: - if k not in seenset: - seenset_add(k) - yield element - except TypeError: - if k not in seenlist: - seenlist_add(k) - yield element - - -def unique_justseen(iterable, key=None): - """Yields elements in order, ignoring serial duplicates - - >>> list(unique_justseen('AAAABBBCCDAABBB')) - ['A', 'B', 'C', 'D', 'A', 'B'] - >>> list(unique_justseen('ABBCcAD', str.lower)) - ['A', 'B', 'C', 'A', 'D'] - - """ - return map(next, map(operator.itemgetter(1), groupby(iterable, key))) - - -def iter_except(func, exception, first=None): - """Yields results from a function repeatedly until an exception is raised. - - Converts a call-until-exception interface to an iterator interface. - Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel - to end the loop. - - >>> l = [0, 1, 2] - >>> list(iter_except(l.pop, IndexError)) - [2, 1, 0] - - """ - try: - if first is not None: - yield first() - while 1: - yield func() - except exception: - pass - - -def first_true(iterable, default=False, pred=None): - """ - Returns the first true value in the iterable. - - If no true value is found, returns *default* - - If *pred* is not None, returns the first item for which - ``pred(item) == True`` . - - >>> first_true(range(10)) - 1 - >>> first_true(range(10), pred=lambda x: x > 5) - 6 - >>> first_true(range(10), default='missing', pred=lambda x: x > 9) - 'missing' - - """ - return next(filter(pred, iterable), default) - - -def random_product(*args, **kwds): - """Draw an item at random from each of the input iterables. - - >>> random_product('abc', range(4), 'XYZ') # doctest:+SKIP - ('c', 3, 'Z') - - If *repeat* is provided as a keyword argument, that many items will be - drawn from each iterable. - - >>> random_product('abcd', range(4), repeat=2) # doctest:+SKIP - ('a', 2, 'd', 3) - - This equivalent to taking a random selection from - ``itertools.product(*args, **kwarg)``. - - """ - pools = [tuple(pool) for pool in args] * kwds.get('repeat', 1) - return tuple(choice(pool) for pool in pools) - - -def random_permutation(iterable, r=None): - """Return a random *r* length permutation of the elements in *iterable*. - - If *r* is not specified or is ``None``, then *r* defaults to the length of - *iterable*. - - >>> random_permutation(range(5)) # doctest:+SKIP - (3, 4, 0, 1, 2) - - This equivalent to taking a random selection from - ``itertools.permutations(iterable, r)``. - - """ - pool = tuple(iterable) - r = len(pool) if r is None else r - return tuple(sample(pool, r)) - - -def random_combination(iterable, r): - """Return a random *r* length subsequence of the elements in *iterable*. - - >>> random_combination(range(5), 3) # doctest:+SKIP - (2, 3, 4) - - This equivalent to taking a random selection from - ``itertools.combinations(iterable, r)``. - - """ - pool = tuple(iterable) - n = len(pool) - indices = sorted(sample(range(n), r)) - return tuple(pool[i] for i in indices) - - -def random_combination_with_replacement(iterable, r): - """Return a random *r* length subsequence of elements in *iterable*, - allowing individual elements to be repeated. - - >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP - (0, 0, 1, 2, 2) - - This equivalent to taking a random selection from - ``itertools.combinations_with_replacement(iterable, r)``. - - """ - pool = tuple(iterable) - n = len(pool) - indices = sorted(randrange(n) for i in range(r)) - return tuple(pool[i] for i in indices) - - -def nth_combination(iterable, r, index): - """Equivalent to ``list(combinations(iterable, r))[index]``. - - The subsequences of *iterable* that are of length *r* can be ordered - lexicographically. :func:`nth_combination` computes the subsequence at - sort position *index* directly, without computing the previous - subsequences. - - """ - pool = tuple(iterable) - n = len(pool) - if (r < 0) or (r > n): - raise ValueError - - c = 1 - k = min(r, n - r) - for i in range(1, k + 1): - c = c * (n - k + i) // i - - if index < 0: - index += c - - if (index < 0) or (index >= c): - raise IndexError - - result = [] - while r: - c, n, r = c * r // n, n - 1, r - 1 - while index >= c: - index -= c - c, n = c * (n - r) // n, n - 1 - result.append(pool[-1 - n]) - - return tuple(result) - - -def prepend(value, iterator): - """Yield *value*, followed by the elements in *iterator*. - - >>> value = '0' - >>> iterator = ['1', '2', '3'] - >>> list(prepend(value, iterator)) - ['0', '1', '2', '3'] - - To prepend multiple values, see :func:`itertools.chain`. - - """ - return chain([value], iterator) diff --git a/libraries/more_itertools/tests/__init__.py b/libraries/more_itertools/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libraries/more_itertools/tests/test_more.py b/libraries/more_itertools/tests/test_more.py deleted file mode 100644 index a1b1e431..00000000 --- a/libraries/more_itertools/tests/test_more.py +++ /dev/null @@ -1,2074 +0,0 @@ -from __future__ import division, print_function, unicode_literals - -from collections import OrderedDict -from decimal import Decimal -from doctest import DocTestSuite -from fractions import Fraction -from functools import partial, reduce -from heapq import merge -from io import StringIO -from itertools import ( - chain, - count, - groupby, - islice, - permutations, - product, - repeat, -) -from operator import add, mul, itemgetter -from unittest import TestCase - -from six.moves import filter, map, range, zip - -import more_itertools as mi - - -def load_tests(loader, tests, ignore): - # Add the doctests - tests.addTests(DocTestSuite('more_itertools.more')) - return tests - - -class CollateTests(TestCase): - """Unit tests for ``collate()``""" - # Also accidentally tests peekable, though that could use its own tests - - def test_default(self): - """Test with the default `key` function.""" - iterables = [range(4), range(7), range(3, 6)] - self.assertEqual( - sorted(reduce(list.__add__, [list(it) for it in iterables])), - list(mi.collate(*iterables)) - ) - - def test_key(self): - """Test using a custom `key` function.""" - iterables = [range(5, 0, -1), range(4, 0, -1)] - actual = sorted( - reduce(list.__add__, [list(it) for it in iterables]), reverse=True - ) - expected = list(mi.collate(*iterables, key=lambda x: -x)) - self.assertEqual(actual, expected) - - def test_empty(self): - """Be nice if passed an empty list of iterables.""" - self.assertEqual([], list(mi.collate())) - - def test_one(self): - """Work when only 1 iterable is passed.""" - self.assertEqual([0, 1], list(mi.collate(range(2)))) - - def test_reverse(self): - """Test the `reverse` kwarg.""" - iterables = [range(4, 0, -1), range(7, 0, -1), range(3, 6, -1)] - - actual = sorted( - reduce(list.__add__, [list(it) for it in iterables]), reverse=True - ) - expected = list(mi.collate(*iterables, reverse=True)) - self.assertEqual(actual, expected) - - def test_alias(self): - self.assertNotEqual(merge.__doc__, mi.collate.__doc__) - self.assertNotEqual(partial.__doc__, mi.collate.__doc__) - - -class ChunkedTests(TestCase): - """Tests for ``chunked()``""" - - def test_even(self): - """Test when ``n`` divides evenly into the length of the iterable.""" - self.assertEqual( - list(mi.chunked('ABCDEF', 3)), [['A', 'B', 'C'], ['D', 'E', 'F']] - ) - - def test_odd(self): - """Test when ``n`` does not divide evenly into the length of the - iterable. - - """ - self.assertEqual( - list(mi.chunked('ABCDE', 3)), [['A', 'B', 'C'], ['D', 'E']] - ) - - -class FirstTests(TestCase): - """Tests for ``first()``""" - - def test_many(self): - """Test that it works on many-item iterables.""" - # Also try it on a generator expression to make sure it works on - # whatever those return, across Python versions. - self.assertEqual(mi.first(x for x in range(4)), 0) - - def test_one(self): - """Test that it doesn't raise StopIteration prematurely.""" - self.assertEqual(mi.first([3]), 3) - - def test_empty_stop_iteration(self): - """It should raise StopIteration for empty iterables.""" - self.assertRaises(ValueError, lambda: mi.first([])) - - def test_default(self): - """It should return the provided default arg for empty iterables.""" - self.assertEqual(mi.first([], 'boo'), 'boo') - - -class IterOnlyRange: - """User-defined iterable class which only support __iter__. - - It is not specified to inherit ``object``, so indexing on a instance will - raise an ``AttributeError`` rather than ``TypeError`` in Python 2. - - >>> r = IterOnlyRange(5) - >>> r[0] - AttributeError: IterOnlyRange instance has no attribute '__getitem__' - - Note: In Python 3, ``TypeError`` will be raised because ``object`` is - inherited implicitly by default. - - >>> r[0] - TypeError: 'IterOnlyRange' object does not support indexing - """ - def __init__(self, n): - """Set the length of the range.""" - self.n = n - - def __iter__(self): - """Works same as range().""" - return iter(range(self.n)) - - -class LastTests(TestCase): - """Tests for ``last()``""" - - def test_many_nonsliceable(self): - """Test that it works on many-item non-slice-able iterables.""" - # Also try it on a generator expression to make sure it works on - # whatever those return, across Python versions. - self.assertEqual(mi.last(x for x in range(4)), 3) - - def test_one_nonsliceable(self): - """Test that it doesn't raise StopIteration prematurely.""" - self.assertEqual(mi.last(x for x in range(1)), 0) - - def test_empty_stop_iteration_nonsliceable(self): - """It should raise ValueError for empty non-slice-able iterables.""" - self.assertRaises(ValueError, lambda: mi.last(x for x in range(0))) - - def test_default_nonsliceable(self): - """It should return the provided default arg for empty non-slice-able - iterables. - """ - self.assertEqual(mi.last((x for x in range(0)), 'boo'), 'boo') - - def test_many_sliceable(self): - """Test that it works on many-item slice-able iterables.""" - self.assertEqual(mi.last([0, 1, 2, 3]), 3) - - def test_one_sliceable(self): - """Test that it doesn't raise StopIteration prematurely.""" - self.assertEqual(mi.last([3]), 3) - - def test_empty_stop_iteration_sliceable(self): - """It should raise ValueError for empty slice-able iterables.""" - self.assertRaises(ValueError, lambda: mi.last([])) - - def test_default_sliceable(self): - """It should return the provided default arg for empty slice-able - iterables. - """ - self.assertEqual(mi.last([], 'boo'), 'boo') - - def test_dict(self): - """last(dic) and last(dic.keys()) should return same result.""" - dic = {'a': 1, 'b': 2, 'c': 3} - self.assertEqual(mi.last(dic), mi.last(dic.keys())) - - def test_ordereddict(self): - """last(dic) should return the last key.""" - od = OrderedDict() - od['a'] = 1 - od['b'] = 2 - od['c'] = 3 - self.assertEqual(mi.last(od), 'c') - - def test_customrange(self): - """It should work on custom class where [] raises AttributeError.""" - self.assertEqual(mi.last(IterOnlyRange(5)), 4) - - -class PeekableTests(TestCase): - """Tests for ``peekable()`` behavor not incidentally covered by testing - ``collate()`` - - """ - def test_peek_default(self): - """Make sure passing a default into ``peek()`` works.""" - p = mi.peekable([]) - self.assertEqual(p.peek(7), 7) - - def test_truthiness(self): - """Make sure a ``peekable`` tests true iff there are items remaining in - the iterable. - - """ - p = mi.peekable([]) - self.assertFalse(p) - - p = mi.peekable(range(3)) - self.assertTrue(p) - - def test_simple_peeking(self): - """Make sure ``next`` and ``peek`` advance and don't advance the - iterator, respectively. - - """ - p = mi.peekable(range(10)) - self.assertEqual(next(p), 0) - self.assertEqual(p.peek(), 1) - self.assertEqual(next(p), 1) - - def test_indexing(self): - """ - Indexing into the peekable shouldn't advance the iterator. - """ - p = mi.peekable('abcdefghijkl') - - # The 0th index is what ``next()`` will return - self.assertEqual(p[0], 'a') - self.assertEqual(next(p), 'a') - - # Indexing further into the peekable shouldn't advance the itertor - self.assertEqual(p[2], 'd') - self.assertEqual(next(p), 'b') - - # The 0th index moves up with the iterator; the last index follows - self.assertEqual(p[0], 'c') - self.assertEqual(p[9], 'l') - - self.assertEqual(next(p), 'c') - self.assertEqual(p[8], 'l') - - # Negative indexing should work too - self.assertEqual(p[-2], 'k') - self.assertEqual(p[-9], 'd') - self.assertRaises(IndexError, lambda: p[-10]) - - def test_slicing(self): - """Slicing the peekable shouldn't advance the iterator.""" - seq = list('abcdefghijkl') - p = mi.peekable(seq) - - # Slicing the peekable should just be like slicing a re-iterable - self.assertEqual(p[1:4], seq[1:4]) - - # Advancing the iterator moves the slices up also - self.assertEqual(next(p), 'a') - self.assertEqual(p[1:4], seq[1:][1:4]) - - # Implicit starts and stop should work - self.assertEqual(p[:5], seq[1:][:5]) - self.assertEqual(p[:], seq[1:][:]) - - # Indexing past the end should work - self.assertEqual(p[:100], seq[1:][:100]) - - # Steps should work, including negative - self.assertEqual(p[::2], seq[1:][::2]) - self.assertEqual(p[::-1], seq[1:][::-1]) - - def test_slicing_reset(self): - """Test slicing on a fresh iterable each time""" - iterable = ['0', '1', '2', '3', '4', '5'] - indexes = list(range(-4, len(iterable) + 4)) + [None] - steps = [1, 2, 3, 4, -1, -2, -3, 4] - for slice_args in product(indexes, indexes, steps): - it = iter(iterable) - p = mi.peekable(it) - next(p) - index = slice(*slice_args) - actual = p[index] - expected = iterable[1:][index] - self.assertEqual(actual, expected, slice_args) - - def test_slicing_error(self): - iterable = '01234567' - p = mi.peekable(iter(iterable)) - - # Prime the cache - p.peek() - old_cache = list(p._cache) - - # Illegal slice - with self.assertRaises(ValueError): - p[1:-1:0] - - # Neither the cache nor the iteration should be affected - self.assertEqual(old_cache, list(p._cache)) - self.assertEqual(list(p), list(iterable)) - - def test_passthrough(self): - """Iterating a peekable without using ``peek()`` or ``prepend()`` - should just give the underlying iterable's elements (a trivial test but - useful to set a baseline in case something goes wrong)""" - expected = [1, 2, 3, 4, 5] - actual = list(mi.peekable(expected)) - self.assertEqual(actual, expected) - - # prepend() behavior tests - - def test_prepend(self): - """Tests intersperesed ``prepend()`` and ``next()`` calls""" - it = mi.peekable(range(2)) - actual = [] - - # Test prepend() before next() - it.prepend(10) - actual += [next(it), next(it)] - - # Test prepend() between next()s - it.prepend(11) - actual += [next(it), next(it)] - - # Test prepend() after source iterable is consumed - it.prepend(12) - actual += [next(it)] - - expected = [10, 0, 11, 1, 12] - self.assertEqual(actual, expected) - - def test_multi_prepend(self): - """Tests prepending multiple items and getting them in proper order""" - it = mi.peekable(range(5)) - actual = [next(it), next(it)] - it.prepend(10, 11, 12) - it.prepend(20, 21) - actual += list(it) - expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4] - self.assertEqual(actual, expected) - - def test_empty(self): - """Tests prepending in front of an empty iterable""" - it = mi.peekable([]) - it.prepend(10) - actual = list(it) - expected = [10] - self.assertEqual(actual, expected) - - def test_prepend_truthiness(self): - """Tests that ``__bool__()`` or ``__nonzero__()`` works properly - with ``prepend()``""" - it = mi.peekable(range(5)) - self.assertTrue(it) - actual = list(it) - self.assertFalse(it) - it.prepend(10) - self.assertTrue(it) - actual += [next(it)] - self.assertFalse(it) - expected = [0, 1, 2, 3, 4, 10] - self.assertEqual(actual, expected) - - def test_multi_prepend_peek(self): - """Tests prepending multiple elements and getting them in reverse order - while peeking""" - it = mi.peekable(range(5)) - actual = [next(it), next(it)] - self.assertEqual(it.peek(), 2) - it.prepend(10, 11, 12) - self.assertEqual(it.peek(), 10) - it.prepend(20, 21) - self.assertEqual(it.peek(), 20) - actual += list(it) - self.assertFalse(it) - expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4] - self.assertEqual(actual, expected) - - def test_prepend_after_stop(self): - """Test resuming iteration after a previous exhaustion""" - it = mi.peekable(range(3)) - self.assertEqual(list(it), [0, 1, 2]) - self.assertRaises(StopIteration, lambda: next(it)) - it.prepend(10) - self.assertEqual(next(it), 10) - self.assertRaises(StopIteration, lambda: next(it)) - - def test_prepend_slicing(self): - """Tests interaction between prepending and slicing""" - seq = list(range(20)) - p = mi.peekable(seq) - - p.prepend(30, 40, 50) - pseq = [30, 40, 50] + seq # pseq for prepended_seq - - # adapt the specific tests from test_slicing - self.assertEqual(p[0], 30) - self.assertEqual(p[1:8], pseq[1:8]) - self.assertEqual(p[1:], pseq[1:]) - self.assertEqual(p[:5], pseq[:5]) - self.assertEqual(p[:], pseq[:]) - self.assertEqual(p[:100], pseq[:100]) - self.assertEqual(p[::2], pseq[::2]) - self.assertEqual(p[::-1], pseq[::-1]) - - def test_prepend_indexing(self): - """Tests interaction between prepending and indexing""" - seq = list(range(20)) - p = mi.peekable(seq) - - p.prepend(30, 40, 50) - - self.assertEqual(p[0], 30) - self.assertEqual(next(p), 30) - self.assertEqual(p[2], 0) - self.assertEqual(next(p), 40) - self.assertEqual(p[0], 50) - self.assertEqual(p[9], 8) - self.assertEqual(next(p), 50) - self.assertEqual(p[8], 8) - self.assertEqual(p[-2], 18) - self.assertEqual(p[-9], 11) - self.assertRaises(IndexError, lambda: p[-21]) - - def test_prepend_iterable(self): - """Tests prepending from an iterable""" - it = mi.peekable(range(5)) - # Don't directly use the range() object to avoid any range-specific - # optimizations - it.prepend(*(x for x in range(5))) - actual = list(it) - expected = list(chain(range(5), range(5))) - self.assertEqual(actual, expected) - - def test_prepend_many(self): - """Tests that prepending a huge number of elements works""" - it = mi.peekable(range(5)) - # Don't directly use the range() object to avoid any range-specific - # optimizations - it.prepend(*(x for x in range(20000))) - actual = list(it) - expected = list(chain(range(20000), range(5))) - self.assertEqual(actual, expected) - - def test_prepend_reversed(self): - """Tests prepending from a reversed iterable""" - it = mi.peekable(range(3)) - it.prepend(*reversed((10, 11, 12))) - actual = list(it) - expected = [12, 11, 10, 0, 1, 2] - self.assertEqual(actual, expected) - - -class ConsumerTests(TestCase): - """Tests for ``consumer()``""" - - def test_consumer(self): - @mi.consumer - def eater(): - while True: - x = yield # noqa - - e = eater() - e.send('hi') # without @consumer, would raise TypeError - - -class DistinctPermutationsTests(TestCase): - def test_distinct_permutations(self): - """Make sure the output for ``distinct_permutations()`` is the same as - set(permutations(it)). - - """ - iterable = ['z', 'a', 'a', 'q', 'q', 'q', 'y'] - test_output = sorted(mi.distinct_permutations(iterable)) - ref_output = sorted(set(permutations(iterable))) - self.assertEqual(test_output, ref_output) - - def test_other_iterables(self): - """Make sure ``distinct_permutations()`` accepts a different type of - iterables. - - """ - # a generator - iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y']) - test_output = sorted(mi.distinct_permutations(iterable)) - # "reload" it - iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y']) - ref_output = sorted(set(permutations(iterable))) - self.assertEqual(test_output, ref_output) - - # an iterator - iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y']) - test_output = sorted(mi.distinct_permutations(iterable)) - # "reload" it - iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y']) - ref_output = sorted(set(permutations(iterable))) - self.assertEqual(test_output, ref_output) - - -class IlenTests(TestCase): - def test_ilen(self): - """Sanity-checks for ``ilen()``.""" - # Non-empty - self.assertEqual( - mi.ilen(filter(lambda x: x % 10 == 0, range(101))), 11 - ) - - # Empty - self.assertEqual(mi.ilen((x for x in range(0))), 0) - - # Iterable with __len__ - self.assertEqual(mi.ilen(list(range(6))), 6) - - -class WithIterTests(TestCase): - def test_with_iter(self): - s = StringIO('One fish\nTwo fish') - initial_words = [line.split()[0] for line in mi.with_iter(s)] - - # Iterable's items should be faithfully represented - self.assertEqual(initial_words, ['One', 'Two']) - # The file object should be closed - self.assertEqual(s.closed, True) - - -class OneTests(TestCase): - def test_basic(self): - it = iter(['item']) - self.assertEqual(mi.one(it), 'item') - - def test_too_short(self): - it = iter([]) - self.assertRaises(ValueError, lambda: mi.one(it)) - self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError)) - - def test_too_long(self): - it = count() - self.assertRaises(ValueError, lambda: mi.one(it)) # burn 0 and 1 - self.assertEqual(next(it), 2) - self.assertRaises( - OverflowError, lambda: mi.one(it, too_long=OverflowError) - ) - - -class IntersperseTest(TestCase): - """ Tests for intersperse() """ - - def test_even(self): - iterable = (x for x in '01') - self.assertEqual( - list(mi.intersperse(None, iterable)), ['0', None, '1'] - ) - - def test_odd(self): - iterable = (x for x in '012') - self.assertEqual( - list(mi.intersperse(None, iterable)), ['0', None, '1', None, '2'] - ) - - def test_nested(self): - element = ('a', 'b') - iterable = (x for x in '012') - actual = list(mi.intersperse(element, iterable)) - expected = ['0', ('a', 'b'), '1', ('a', 'b'), '2'] - self.assertEqual(actual, expected) - - def test_not_iterable(self): - self.assertRaises(TypeError, lambda: mi.intersperse('x', 1)) - - def test_n(self): - for n, element, expected in [ - (1, '_', ['0', '_', '1', '_', '2', '_', '3', '_', '4', '_', '5']), - (2, '_', ['0', '1', '_', '2', '3', '_', '4', '5']), - (3, '_', ['0', '1', '2', '_', '3', '4', '5']), - (4, '_', ['0', '1', '2', '3', '_', '4', '5']), - (5, '_', ['0', '1', '2', '3', '4', '_', '5']), - (6, '_', ['0', '1', '2', '3', '4', '5']), - (7, '_', ['0', '1', '2', '3', '4', '5']), - (3, ['a', 'b'], ['0', '1', '2', ['a', 'b'], '3', '4', '5']), - ]: - iterable = (x for x in '012345') - actual = list(mi.intersperse(element, iterable, n=n)) - self.assertEqual(actual, expected) - - def test_n_zero(self): - self.assertRaises( - ValueError, lambda: list(mi.intersperse('x', '012', n=0)) - ) - - -class UniqueToEachTests(TestCase): - """Tests for ``unique_to_each()``""" - - def test_all_unique(self): - """When all the input iterables are unique the output should match - the input.""" - iterables = [[1, 2], [3, 4, 5], [6, 7, 8]] - self.assertEqual(mi.unique_to_each(*iterables), iterables) - - def test_duplicates(self): - """When there are duplicates in any of the input iterables that aren't - in the rest, those duplicates should be emitted.""" - iterables = ["mississippi", "missouri"] - self.assertEqual( - mi.unique_to_each(*iterables), [['p', 'p'], ['o', 'u', 'r']] - ) - - def test_mixed(self): - """When the input iterables contain different types the function should - still behave properly""" - iterables = ['x', (i for i in range(3)), [1, 2, 3], tuple()] - self.assertEqual(mi.unique_to_each(*iterables), [['x'], [0], [3], []]) - - -class WindowedTests(TestCase): - """Tests for ``windowed()``""" - - def test_basic(self): - actual = list(mi.windowed([1, 2, 3, 4, 5], 3)) - expected = [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - self.assertEqual(actual, expected) - - def test_large_size(self): - """ - When the window size is larger than the iterable, and no fill value is - given,``None`` should be filled in. - """ - actual = list(mi.windowed([1, 2, 3, 4, 5], 6)) - expected = [(1, 2, 3, 4, 5, None)] - self.assertEqual(actual, expected) - - def test_fillvalue(self): - """ - When sizes don't match evenly, the given fill value should be used. - """ - iterable = [1, 2, 3, 4, 5] - - for n, kwargs, expected in [ - (6, {}, [(1, 2, 3, 4, 5, '!')]), # n > len(iterable) - (3, {'step': 3}, [(1, 2, 3), (4, 5, '!')]), # using ``step`` - ]: - actual = list(mi.windowed(iterable, n, fillvalue='!', **kwargs)) - self.assertEqual(actual, expected) - - def test_zero(self): - """When the window size is zero, an empty tuple should be emitted.""" - actual = list(mi.windowed([1, 2, 3, 4, 5], 0)) - expected = [tuple()] - self.assertEqual(actual, expected) - - def test_negative(self): - """When the window size is negative, ValueError should be raised.""" - with self.assertRaises(ValueError): - list(mi.windowed([1, 2, 3, 4, 5], -1)) - - def test_step(self): - """The window should advance by the number of steps provided""" - iterable = [1, 2, 3, 4, 5, 6, 7] - for n, step, expected in [ - (3, 2, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]), # n > step - (3, 3, [(1, 2, 3), (4, 5, 6), (7, None, None)]), # n == step - (3, 4, [(1, 2, 3), (5, 6, 7)]), # line up nicely - (3, 5, [(1, 2, 3), (6, 7, None)]), # off by one - (3, 6, [(1, 2, 3), (7, None, None)]), # off by two - (3, 7, [(1, 2, 3)]), # step past the end - (7, 8, [(1, 2, 3, 4, 5, 6, 7)]), # step > len(iterable) - ]: - actual = list(mi.windowed(iterable, n, step=step)) - self.assertEqual(actual, expected) - - # Step must be greater than or equal to 1 - with self.assertRaises(ValueError): - list(mi.windowed(iterable, 3, step=0)) - - -class BucketTests(TestCase): - """Tests for ``bucket()``""" - - def test_basic(self): - iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33] - D = mi.bucket(iterable, key=lambda x: 10 * (x // 10)) - - # In-order access - self.assertEqual(list(D[10]), [10, 11, 12]) - - # Out of order access - self.assertEqual(list(D[30]), [30, 31, 33]) - self.assertEqual(list(D[20]), [20, 21, 22, 23]) - - self.assertEqual(list(D[40]), []) # Nothing in here! - - def test_in(self): - iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33] - D = mi.bucket(iterable, key=lambda x: 10 * (x // 10)) - - self.assertTrue(10 in D) - self.assertFalse(40 in D) - self.assertTrue(20 in D) - self.assertFalse(21 in D) - - # Checking in-ness shouldn't advance the iterator - self.assertEqual(next(D[10]), 10) - - def test_validator(self): - iterable = count(0) - key = lambda x: int(str(x)[0]) # First digit of each number - validator = lambda x: 0 < x < 10 # No leading zeros - D = mi.bucket(iterable, key, validator=validator) - self.assertEqual(mi.take(3, D[1]), [1, 10, 11]) - self.assertNotIn(0, D) # Non-valid entries don't return True - self.assertNotIn(0, D._cache) # Don't store non-valid entries - self.assertEqual(list(D[0]), []) - - -class SpyTests(TestCase): - """Tests for ``spy()``""" - - def test_basic(self): - original_iterable = iter('abcdefg') - head, new_iterable = mi.spy(original_iterable) - self.assertEqual(head, ['a']) - self.assertEqual( - list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - ) - - def test_unpacking(self): - original_iterable = iter('abcdefg') - (first, second, third), new_iterable = mi.spy(original_iterable, 3) - self.assertEqual(first, 'a') - self.assertEqual(second, 'b') - self.assertEqual(third, 'c') - self.assertEqual( - list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - ) - - def test_too_many(self): - original_iterable = iter('abc') - head, new_iterable = mi.spy(original_iterable, 4) - self.assertEqual(head, ['a', 'b', 'c']) - self.assertEqual(list(new_iterable), ['a', 'b', 'c']) - - def test_zero(self): - original_iterable = iter('abc') - head, new_iterable = mi.spy(original_iterable, 0) - self.assertEqual(head, []) - self.assertEqual(list(new_iterable), ['a', 'b', 'c']) - - -class InterleaveTests(TestCase): - def test_even(self): - actual = list(mi.interleave([1, 4, 7], [2, 5, 8], [3, 6, 9])) - expected = [1, 2, 3, 4, 5, 6, 7, 8, 9] - self.assertEqual(actual, expected) - - def test_short(self): - actual = list(mi.interleave([1, 4], [2, 5, 7], [3, 6, 8])) - expected = [1, 2, 3, 4, 5, 6] - self.assertEqual(actual, expected) - - def test_mixed_types(self): - it_list = ['a', 'b', 'c', 'd'] - it_str = '12345' - it_inf = count() - actual = list(mi.interleave(it_list, it_str, it_inf)) - expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', 3] - self.assertEqual(actual, expected) - - -class InterleaveLongestTests(TestCase): - def test_even(self): - actual = list(mi.interleave_longest([1, 4, 7], [2, 5, 8], [3, 6, 9])) - expected = [1, 2, 3, 4, 5, 6, 7, 8, 9] - self.assertEqual(actual, expected) - - def test_short(self): - actual = list(mi.interleave_longest([1, 4], [2, 5, 7], [3, 6, 8])) - expected = [1, 2, 3, 4, 5, 6, 7, 8] - self.assertEqual(actual, expected) - - def test_mixed_types(self): - it_list = ['a', 'b', 'c', 'd'] - it_str = '12345' - it_gen = (x for x in range(3)) - actual = list(mi.interleave_longest(it_list, it_str, it_gen)) - expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', '5'] - self.assertEqual(actual, expected) - - -class TestCollapse(TestCase): - """Tests for ``collapse()``""" - - def test_collapse(self): - l = [[1], 2, [[3], 4], [[[5]]]] - self.assertEqual(list(mi.collapse(l)), [1, 2, 3, 4, 5]) - - def test_collapse_to_string(self): - l = [["s1"], "s2", [["s3"], "s4"], [[["s5"]]]] - self.assertEqual(list(mi.collapse(l)), ["s1", "s2", "s3", "s4", "s5"]) - - def test_collapse_flatten(self): - l = [[1], [2], [[3], 4], [[[5]]]] - self.assertEqual(list(mi.collapse(l, levels=1)), list(mi.flatten(l))) - - def test_collapse_to_level(self): - l = [[1], 2, [[3], 4], [[[5]]]] - self.assertEqual(list(mi.collapse(l, levels=2)), [1, 2, 3, 4, [5]]) - self.assertEqual( - list(mi.collapse(mi.collapse(l, levels=1), levels=1)), - list(mi.collapse(l, levels=2)) - ) - - def test_collapse_to_list(self): - l = (1, [2], (3, [4, (5,)], 'ab')) - actual = list(mi.collapse(l, base_type=list)) - expected = [1, [2], 3, [4, (5,)], 'ab'] - self.assertEqual(actual, expected) - - -class SideEffectTests(TestCase): - """Tests for ``side_effect()``""" - - def test_individual(self): - # The function increments the counter for each call - counter = [0] - - def func(arg): - counter[0] += 1 - - result = list(mi.side_effect(func, range(10))) - self.assertEqual(result, list(range(10))) - self.assertEqual(counter[0], 10) - - def test_chunked(self): - # The function increments the counter for each call - counter = [0] - - def func(arg): - counter[0] += 1 - - result = list(mi.side_effect(func, range(10), 2)) - self.assertEqual(result, list(range(10))) - self.assertEqual(counter[0], 5) - - def test_before_after(self): - f = StringIO() - collector = [] - - def func(item): - print(item, file=f) - collector.append(f.getvalue()) - - def it(): - yield u'a' - yield u'b' - raise RuntimeError('kaboom') - - before = lambda: print('HEADER', file=f) - after = f.close - - try: - mi.consume(mi.side_effect(func, it(), before=before, after=after)) - except RuntimeError: - pass - - # The iterable should have been written to the file - self.assertEqual(collector, [u'HEADER\na\n', u'HEADER\na\nb\n']) - - # The file should be closed even though something bad happened - self.assertTrue(f.closed) - - def test_before_fails(self): - f = StringIO() - func = lambda x: print(x, file=f) - - def before(): - raise RuntimeError('ouch') - - try: - mi.consume( - mi.side_effect(func, u'abc', before=before, after=f.close) - ) - except RuntimeError: - pass - - # The file should be closed even though something bad happened in the - # before function - self.assertTrue(f.closed) - - -class SlicedTests(TestCase): - """Tests for ``sliced()``""" - - def test_even(self): - """Test when the length of the sequence is divisible by *n*""" - seq = 'ABCDEFGHI' - self.assertEqual(list(mi.sliced(seq, 3)), ['ABC', 'DEF', 'GHI']) - - def test_odd(self): - """Test when the length of the sequence is not divisible by *n*""" - seq = 'ABCDEFGHI' - self.assertEqual(list(mi.sliced(seq, 4)), ['ABCD', 'EFGH', 'I']) - - def test_not_sliceable(self): - seq = (x for x in 'ABCDEFGHI') - - with self.assertRaises(TypeError): - list(mi.sliced(seq, 3)) - - -class SplitAtTests(TestCase): - """Tests for ``split()``""" - - def comp_with_str_split(self, str_to_split, delim): - pred = lambda c: c == delim - actual = list(map(''.join, mi.split_at(str_to_split, pred))) - expected = str_to_split.split(delim) - self.assertEqual(actual, expected) - - def test_seperators(self): - test_strs = ['', 'abcba', 'aaabbbcccddd', 'e'] - for s, delim in product(test_strs, 'abcd'): - self.comp_with_str_split(s, delim) - - -class SplitBeforeTest(TestCase): - """Tests for ``split_before()``""" - - def test_starts_with_sep(self): - actual = list(mi.split_before('xooxoo', lambda c: c == 'x')) - expected = [['x', 'o', 'o'], ['x', 'o', 'o']] - self.assertEqual(actual, expected) - - def test_ends_with_sep(self): - actual = list(mi.split_before('ooxoox', lambda c: c == 'x')) - expected = [['o', 'o'], ['x', 'o', 'o'], ['x']] - self.assertEqual(actual, expected) - - def test_no_sep(self): - actual = list(mi.split_before('ooo', lambda c: c == 'x')) - expected = [['o', 'o', 'o']] - self.assertEqual(actual, expected) - - -class SplitAfterTest(TestCase): - """Tests for ``split_after()``""" - - def test_starts_with_sep(self): - actual = list(mi.split_after('xooxoo', lambda c: c == 'x')) - expected = [['x'], ['o', 'o', 'x'], ['o', 'o']] - self.assertEqual(actual, expected) - - def test_ends_with_sep(self): - actual = list(mi.split_after('ooxoox', lambda c: c == 'x')) - expected = [['o', 'o', 'x'], ['o', 'o', 'x']] - self.assertEqual(actual, expected) - - def test_no_sep(self): - actual = list(mi.split_after('ooo', lambda c: c == 'x')) - expected = [['o', 'o', 'o']] - self.assertEqual(actual, expected) - - -class PaddedTest(TestCase): - """Tests for ``padded()``""" - - def test_no_n(self): - seq = [1, 2, 3] - - # No fillvalue - self.assertEqual(mi.take(5, mi.padded(seq)), [1, 2, 3, None, None]) - - # With fillvalue - self.assertEqual( - mi.take(5, mi.padded(seq, fillvalue='')), [1, 2, 3, '', ''] - ) - - def test_invalid_n(self): - self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=-1))) - self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=0))) - - def test_valid_n(self): - seq = [1, 2, 3, 4, 5] - - # No need for padding: len(seq) <= n - self.assertEqual(list(mi.padded(seq, n=4)), [1, 2, 3, 4, 5]) - self.assertEqual(list(mi.padded(seq, n=5)), [1, 2, 3, 4, 5]) - - # No fillvalue - self.assertEqual( - list(mi.padded(seq, n=7)), [1, 2, 3, 4, 5, None, None] - ) - - # With fillvalue - self.assertEqual( - list(mi.padded(seq, fillvalue='', n=7)), [1, 2, 3, 4, 5, '', ''] - ) - - def test_next_multiple(self): - seq = [1, 2, 3, 4, 5, 6] - - # No need for padding: len(seq) % n == 0 - self.assertEqual( - list(mi.padded(seq, n=3, next_multiple=True)), [1, 2, 3, 4, 5, 6] - ) - - # Padding needed: len(seq) < n - self.assertEqual( - list(mi.padded(seq, n=8, next_multiple=True)), - [1, 2, 3, 4, 5, 6, None, None] - ) - - # No padding needed: len(seq) == n - self.assertEqual( - list(mi.padded(seq, n=6, next_multiple=True)), [1, 2, 3, 4, 5, 6] - ) - - # Padding needed: len(seq) > n - self.assertEqual( - list(mi.padded(seq, n=4, next_multiple=True)), - [1, 2, 3, 4, 5, 6, None, None] - ) - - # With fillvalue - self.assertEqual( - list(mi.padded(seq, fillvalue='', n=4, next_multiple=True)), - [1, 2, 3, 4, 5, 6, '', ''] - ) - - -class DistributeTest(TestCase): - """Tests for distribute()""" - - def test_invalid_n(self): - self.assertRaises(ValueError, lambda: mi.distribute(-1, [1, 2, 3])) - self.assertRaises(ValueError, lambda: mi.distribute(0, [1, 2, 3])) - - def test_basic(self): - iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - for n, expected in [ - (1, [iterable]), - (2, [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]), - (3, [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]), - (10, [[n] for n in range(1, 10 + 1)]), - ]: - self.assertEqual( - [list(x) for x in mi.distribute(n, iterable)], expected - ) - - def test_large_n(self): - iterable = [1, 2, 3, 4] - self.assertEqual( - [list(x) for x in mi.distribute(6, iterable)], - [[1], [2], [3], [4], [], []] - ) - - -class StaggerTest(TestCase): - """Tests for ``stagger()``""" - - def test_default(self): - iterable = [0, 1, 2, 3] - actual = list(mi.stagger(iterable)) - expected = [(None, 0, 1), (0, 1, 2), (1, 2, 3)] - self.assertEqual(actual, expected) - - def test_offsets(self): - iterable = [0, 1, 2, 3] - for offsets, expected in [ - ((-2, 0, 2), [('', 0, 2), ('', 1, 3)]), - ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3)]), - ((1, 2), [(1, 2), (2, 3)]), - ]: - all_groups = mi.stagger(iterable, offsets=offsets, fillvalue='') - self.assertEqual(list(all_groups), expected) - - def test_longest(self): - iterable = [0, 1, 2, 3] - for offsets, expected in [ - ( - (-1, 0, 1), - [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, ''), (3, '', '')] - ), - ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3), (3, '')]), - ((1, 2), [(1, 2), (2, 3), (3, '')]), - ]: - all_groups = mi.stagger( - iterable, offsets=offsets, fillvalue='', longest=True - ) - self.assertEqual(list(all_groups), expected) - - -class ZipOffsetTest(TestCase): - """Tests for ``zip_offset()``""" - - def test_shortest(self): - a_1 = [0, 1, 2, 3] - a_2 = [0, 1, 2, 3, 4, 5] - a_3 = [0, 1, 2, 3, 4, 5, 6, 7] - actual = list( - mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), fillvalue='') - ) - expected = [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)] - self.assertEqual(actual, expected) - - def test_longest(self): - a_1 = [0, 1, 2, 3] - a_2 = [0, 1, 2, 3, 4, 5] - a_3 = [0, 1, 2, 3, 4, 5, 6, 7] - actual = list( - mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), longest=True) - ) - expected = [ - (None, 0, 1), - (0, 1, 2), - (1, 2, 3), - (2, 3, 4), - (3, 4, 5), - (None, 5, 6), - (None, None, 7), - ] - self.assertEqual(actual, expected) - - def test_mismatch(self): - iterables = [0, 1, 2], [2, 3, 4] - offsets = (-1, 0, 1) - self.assertRaises( - ValueError, - lambda: list(mi.zip_offset(*iterables, offsets=offsets)) - ) - - -class SortTogetherTest(TestCase): - """Tests for sort_together()""" - - def test_key_list(self): - """tests `key_list` including default, iterables include duplicates""" - iterables = [ - ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], - ['May', 'Aug.', 'May', 'June', 'July', 'July'], - [97, 20, 100, 70, 100, 20] - ] - - self.assertEqual( - mi.sort_together(iterables), - [ - ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), - ('June', 'July', 'July', 'May', 'Aug.', 'May'), - (70, 100, 20, 97, 20, 100) - ] - ) - - self.assertEqual( - mi.sort_together(iterables, key_list=(0, 1)), - [ - ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), - ('July', 'July', 'June', 'Aug.', 'May', 'May'), - (100, 20, 70, 20, 97, 100) - ] - ) - - self.assertEqual( - mi.sort_together(iterables, key_list=(0, 1, 2)), - [ - ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), - ('July', 'July', 'June', 'Aug.', 'May', 'May'), - (20, 100, 70, 20, 97, 100) - ] - ) - - self.assertEqual( - mi.sort_together(iterables, key_list=(2,)), - [ - ('GA', 'CT', 'CT', 'GA', 'GA', 'CT'), - ('Aug.', 'July', 'June', 'May', 'May', 'July'), - (20, 20, 70, 97, 100, 100) - ] - ) - - def test_invalid_key_list(self): - """tests `key_list` for indexes not available in `iterables`""" - iterables = [ - ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], - ['May', 'Aug.', 'May', 'June', 'July', 'July'], - [97, 20, 100, 70, 100, 20] - ] - - self.assertRaises( - IndexError, lambda: mi.sort_together(iterables, key_list=(5,)) - ) - - def test_reverse(self): - """tests `reverse` to ensure a reverse sort for `key_list` iterables""" - iterables = [ - ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], - ['May', 'Aug.', 'May', 'June', 'July', 'July'], - [97, 20, 100, 70, 100, 20] - ] - - self.assertEqual( - mi.sort_together(iterables, key_list=(0, 1, 2), reverse=True), - [('GA', 'GA', 'GA', 'CT', 'CT', 'CT'), - ('May', 'May', 'Aug.', 'June', 'July', 'July'), - (100, 97, 20, 70, 100, 20)] - ) - - def test_uneven_iterables(self): - """tests trimming of iterables to the shortest length before sorting""" - iterables = [['GA', 'GA', 'GA', 'CT', 'CT', 'CT', 'MA'], - ['May', 'Aug.', 'May', 'June', 'July', 'July'], - [97, 20, 100, 70, 100, 20, 0]] - - self.assertEqual( - mi.sort_together(iterables), - [ - ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), - ('June', 'July', 'July', 'May', 'Aug.', 'May'), - (70, 100, 20, 97, 20, 100) - ] - ) - - -class DivideTest(TestCase): - """Tests for divide()""" - - def test_invalid_n(self): - self.assertRaises(ValueError, lambda: mi.divide(-1, [1, 2, 3])) - self.assertRaises(ValueError, lambda: mi.divide(0, [1, 2, 3])) - - def test_basic(self): - iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - for n, expected in [ - (1, [iterable]), - (2, [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]), - (3, [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]), - (10, [[n] for n in range(1, 10 + 1)]), - ]: - self.assertEqual( - [list(x) for x in mi.divide(n, iterable)], expected - ) - - def test_large_n(self): - iterable = [1, 2, 3, 4] - self.assertEqual( - [list(x) for x in mi.divide(6, iterable)], - [[1], [2], [3], [4], [], []] - ) - - -class TestAlwaysIterable(TestCase): - """Tests for always_iterable()""" - def test_single(self): - self.assertEqual(list(mi.always_iterable(1)), [1]) - - def test_strings(self): - for obj in ['foo', b'bar', u'baz']: - actual = list(mi.always_iterable(obj)) - expected = [obj] - self.assertEqual(actual, expected) - - def test_base_type(self): - dict_obj = {'a': 1, 'b': 2} - str_obj = '123' - - # Default: dicts are iterable like they normally are - default_actual = list(mi.always_iterable(dict_obj)) - default_expected = list(dict_obj) - self.assertEqual(default_actual, default_expected) - - # Unitary types set: dicts are not iterable - custom_actual = list(mi.always_iterable(dict_obj, base_type=dict)) - custom_expected = [dict_obj] - self.assertEqual(custom_actual, custom_expected) - - # With unitary types set, strings are iterable - str_actual = list(mi.always_iterable(str_obj, base_type=None)) - str_expected = list(str_obj) - self.assertEqual(str_actual, str_expected) - - def test_iterables(self): - self.assertEqual(list(mi.always_iterable([0, 1])), [0, 1]) - self.assertEqual( - list(mi.always_iterable([0, 1], base_type=list)), [[0, 1]] - ) - self.assertEqual( - list(mi.always_iterable(iter('foo'))), ['f', 'o', 'o'] - ) - self.assertEqual(list(mi.always_iterable([])), []) - - def test_none(self): - self.assertEqual(list(mi.always_iterable(None)), []) - - def test_generator(self): - def _gen(): - yield 0 - yield 1 - - self.assertEqual(list(mi.always_iterable(_gen())), [0, 1]) - - -class AdjacentTests(TestCase): - def test_typical(self): - actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10))) - expected = [(True, 0), (True, 1), (False, 2), (False, 3), (True, 4), - (True, 5), (True, 6), (False, 7), (False, 8), (False, 9)] - self.assertEqual(actual, expected) - - def test_empty_iterable(self): - actual = list(mi.adjacent(lambda x: x % 5 == 0, [])) - expected = [] - self.assertEqual(actual, expected) - - def test_length_one(self): - actual = list(mi.adjacent(lambda x: x % 5 == 0, [0])) - expected = [(True, 0)] - self.assertEqual(actual, expected) - - actual = list(mi.adjacent(lambda x: x % 5 == 0, [1])) - expected = [(False, 1)] - self.assertEqual(actual, expected) - - def test_consecutive_true(self): - """Test that when the predicate matches multiple consecutive elements - it doesn't repeat elements in the output""" - actual = list(mi.adjacent(lambda x: x % 5 < 2, range(10))) - expected = [(True, 0), (True, 1), (True, 2), (False, 3), (True, 4), - (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)] - self.assertEqual(actual, expected) - - def test_distance(self): - actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=2)) - expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4), - (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)] - self.assertEqual(actual, expected) - - actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=3)) - expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4), - (True, 5), (True, 6), (True, 7), (True, 8), (False, 9)] - self.assertEqual(actual, expected) - - def test_large_distance(self): - """Test distance larger than the length of the iterable""" - iterable = range(10) - actual = list(mi.adjacent(lambda x: x % 5 == 4, iterable, distance=20)) - expected = list(zip(repeat(True), iterable)) - self.assertEqual(actual, expected) - - actual = list(mi.adjacent(lambda x: False, iterable, distance=20)) - expected = list(zip(repeat(False), iterable)) - self.assertEqual(actual, expected) - - def test_zero_distance(self): - """Test that adjacent() reduces to zip+map when distance is 0""" - iterable = range(1000) - predicate = lambda x: x % 4 == 2 - actual = mi.adjacent(predicate, iterable, 0) - expected = zip(map(predicate, iterable), iterable) - self.assertTrue(all(a == e for a, e in zip(actual, expected))) - - def test_negative_distance(self): - """Test that adjacent() raises an error with negative distance""" - pred = lambda x: x - self.assertRaises( - ValueError, lambda: mi.adjacent(pred, range(1000), -1) - ) - self.assertRaises( - ValueError, lambda: mi.adjacent(pred, range(10), -10) - ) - - def test_grouping(self): - """Test interaction of adjacent() with groupby_transform()""" - iterable = mi.adjacent(lambda x: x % 5 == 0, range(10)) - grouper = mi.groupby_transform(iterable, itemgetter(0), itemgetter(1)) - actual = [(k, list(g)) for k, g in grouper] - expected = [ - (True, [0, 1]), - (False, [2, 3]), - (True, [4, 5, 6]), - (False, [7, 8, 9]), - ] - self.assertEqual(actual, expected) - - def test_call_once(self): - """Test that the predicate is only called once per item.""" - already_seen = set() - iterable = range(10) - - def predicate(item): - self.assertNotIn(item, already_seen) - already_seen.add(item) - return True - - actual = list(mi.adjacent(predicate, iterable)) - expected = [(True, x) for x in iterable] - self.assertEqual(actual, expected) - - -class GroupByTransformTests(TestCase): - def assertAllGroupsEqual(self, groupby1, groupby2): - """Compare two groupby objects for equality, both keys and groups.""" - for a, b in zip(groupby1, groupby2): - key1, group1 = a - key2, group2 = b - self.assertEqual(key1, key2) - self.assertListEqual(list(group1), list(group2)) - self.assertRaises(StopIteration, lambda: next(groupby1)) - self.assertRaises(StopIteration, lambda: next(groupby2)) - - def test_default_funcs(self): - """Test that groupby_transform() with default args mimics groupby()""" - iterable = [(x // 5, x) for x in range(1000)] - actual = mi.groupby_transform(iterable) - expected = groupby(iterable) - self.assertAllGroupsEqual(actual, expected) - - def test_valuefunc(self): - iterable = [(int(x / 5), int(x / 3), x) for x in range(10)] - - # Test the standard usage of grouping one iterable using another's keys - grouper = mi.groupby_transform( - iterable, keyfunc=itemgetter(0), valuefunc=itemgetter(-1) - ) - actual = [(k, list(g)) for k, g in grouper] - expected = [(0, [0, 1, 2, 3, 4]), (1, [5, 6, 7, 8, 9])] - self.assertEqual(actual, expected) - - grouper = mi.groupby_transform( - iterable, keyfunc=itemgetter(1), valuefunc=itemgetter(-1) - ) - actual = [(k, list(g)) for k, g in grouper] - expected = [(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])] - self.assertEqual(actual, expected) - - # and now for something a little different - d = dict(zip(range(10), 'abcdefghij')) - grouper = mi.groupby_transform( - range(10), keyfunc=lambda x: x // 5, valuefunc=d.get - ) - actual = [(k, ''.join(g)) for k, g in grouper] - expected = [(0, 'abcde'), (1, 'fghij')] - self.assertEqual(actual, expected) - - def test_no_valuefunc(self): - iterable = range(1000) - - def key(x): - return x // 5 - - actual = mi.groupby_transform(iterable, key, valuefunc=None) - expected = groupby(iterable, key) - self.assertAllGroupsEqual(actual, expected) - - actual = mi.groupby_transform(iterable, key) # default valuefunc - expected = groupby(iterable, key) - self.assertAllGroupsEqual(actual, expected) - - -class NumericRangeTests(TestCase): - def test_basic(self): - for args, expected in [ - ((4,), [0, 1, 2, 3]), - ((4.0,), [0.0, 1.0, 2.0, 3.0]), - ((1.0, 4), [1.0, 2.0, 3.0]), - ((1, 4.0), [1, 2, 3]), - ((1.0, 5), [1.0, 2.0, 3.0, 4.0]), - ((0, 20, 5), [0, 5, 10, 15]), - ((0, 20, 5.0), [0.0, 5.0, 10.0, 15.0]), - ((0, 10, 3), [0, 3, 6, 9]), - ((0, 10, 3.0), [0.0, 3.0, 6.0, 9.0]), - ((0, -5, -1), [0, -1, -2, -3, -4]), - ((0.0, -5, -1), [0.0, -1.0, -2.0, -3.0, -4.0]), - ((1, 2, Fraction(1, 2)), [Fraction(1, 1), Fraction(3, 2)]), - ((0,), []), - ((0.0,), []), - ((1, 0), []), - ((1.0, 0.0), []), - ((Fraction(2, 1),), [Fraction(0, 1), Fraction(1, 1)]), - ((Decimal('2.0'),), [Decimal('0.0'), Decimal('1.0')]), - ]: - actual = list(mi.numeric_range(*args)) - self.assertEqual(actual, expected) - self.assertTrue( - all(type(a) == type(e) for a, e in zip(actual, expected)) - ) - - def test_arg_count(self): - self.assertRaises(TypeError, lambda: list(mi.numeric_range())) - self.assertRaises( - TypeError, lambda: list(mi.numeric_range(0, 1, 2, 3)) - ) - - def test_zero_step(self): - self.assertRaises( - ValueError, lambda: list(mi.numeric_range(1, 2, 0)) - ) - - -class CountCycleTests(TestCase): - def test_basic(self): - expected = [ - (0, 'a'), (0, 'b'), (0, 'c'), - (1, 'a'), (1, 'b'), (1, 'c'), - (2, 'a'), (2, 'b'), (2, 'c'), - ] - for actual in [ - mi.take(9, mi.count_cycle('abc')), # n=None - list(mi.count_cycle('abc', 3)), # n=3 - ]: - self.assertEqual(actual, expected) - - def test_empty(self): - self.assertEqual(list(mi.count_cycle('')), []) - self.assertEqual(list(mi.count_cycle('', 2)), []) - - def test_negative(self): - self.assertEqual(list(mi.count_cycle('abc', -3)), []) - - -class LocateTests(TestCase): - def test_default_pred(self): - iterable = [0, 1, 1, 0, 1, 0, 0] - actual = list(mi.locate(iterable)) - expected = [1, 2, 4] - self.assertEqual(actual, expected) - - def test_no_matches(self): - iterable = [0, 0, 0] - actual = list(mi.locate(iterable)) - expected = [] - self.assertEqual(actual, expected) - - def test_custom_pred(self): - iterable = ['0', 1, 1, '0', 1, '0', '0'] - pred = lambda x: x == '0' - actual = list(mi.locate(iterable, pred)) - expected = [0, 3, 5, 6] - self.assertEqual(actual, expected) - - def test_window_size(self): - iterable = ['0', 1, 1, '0', 1, '0', '0'] - pred = lambda *args: args == ('0', 1) - actual = list(mi.locate(iterable, pred, window_size=2)) - expected = [0, 3] - self.assertEqual(actual, expected) - - def test_window_size_large(self): - iterable = [1, 2, 3, 4] - pred = lambda a, b, c, d, e: True - actual = list(mi.locate(iterable, pred, window_size=5)) - expected = [0] - self.assertEqual(actual, expected) - - def test_window_size_zero(self): - iterable = [1, 2, 3, 4] - pred = lambda: True - with self.assertRaises(ValueError): - list(mi.locate(iterable, pred, window_size=0)) - - -class StripFunctionTests(TestCase): - def test_hashable(self): - iterable = list('www.example.com') - pred = lambda x: x in set('cmowz.') - - self.assertEqual(list(mi.lstrip(iterable, pred)), list('example.com')) - self.assertEqual(list(mi.rstrip(iterable, pred)), list('www.example')) - self.assertEqual(list(mi.strip(iterable, pred)), list('example')) - - def test_not_hashable(self): - iterable = [ - list('http://'), list('www'), list('.example'), list('.com') - ] - pred = lambda x: x in [list('http://'), list('www'), list('.com')] - - self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[2:]) - self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:3]) - self.assertEqual(list(mi.strip(iterable, pred)), iterable[2: 3]) - - def test_math(self): - iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2] - pred = lambda x: x <= 2 - - self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[3:]) - self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:-3]) - self.assertEqual(list(mi.strip(iterable, pred)), iterable[3:-3]) - - -class IsliceExtendedTests(TestCase): - def test_all(self): - iterable = ['0', '1', '2', '3', '4', '5'] - indexes = list(range(-4, len(iterable) + 4)) + [None] - steps = [1, 2, 3, 4, -1, -2, -3, 4] - for slice_args in product(indexes, indexes, steps): - try: - actual = list(mi.islice_extended(iterable, *slice_args)) - except Exception as e: - self.fail((slice_args, e)) - - expected = iterable[slice(*slice_args)] - self.assertEqual(actual, expected, slice_args) - - def test_zero_step(self): - with self.assertRaises(ValueError): - list(mi.islice_extended([1, 2, 3], 0, 1, 0)) - - -class ConsecutiveGroupsTest(TestCase): - def test_numbers(self): - iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7] - actual = [list(g) for g in mi.consecutive_groups(iterable)] - expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]] - self.assertEqual(actual, expected) - - def test_custom_ordering(self): - iterable = ['1', '10', '11', '20', '21', '22', '30', '31'] - ordering = lambda x: int(x) - actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] - expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']] - self.assertEqual(actual, expected) - - def test_exotic_ordering(self): - iterable = [ - ('a', 'b', 'c', 'd'), - ('a', 'c', 'b', 'd'), - ('a', 'c', 'd', 'b'), - ('a', 'd', 'b', 'c'), - ('d', 'b', 'c', 'a'), - ('d', 'c', 'a', 'b'), - ] - ordering = list(permutations('abcd')).index - actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] - expected = [ - [('a', 'b', 'c', 'd')], - [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')], - [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')], - ] - self.assertEqual(actual, expected) - - -class DifferenceTest(TestCase): - def test_normal(self): - iterable = [10, 20, 30, 40, 50] - actual = list(mi.difference(iterable)) - expected = [10, 10, 10, 10, 10] - self.assertEqual(actual, expected) - - def test_custom(self): - iterable = [10, 20, 30, 40, 50] - actual = list(mi.difference(iterable, add)) - expected = [10, 30, 50, 70, 90] - self.assertEqual(actual, expected) - - def test_roundtrip(self): - original = list(range(100)) - accumulated = mi.accumulate(original) - actual = list(mi.difference(accumulated)) - self.assertEqual(actual, original) - - def test_one(self): - self.assertEqual(list(mi.difference([0])), [0]) - - def test_empty(self): - self.assertEqual(list(mi.difference([])), []) - - -class SeekableTest(TestCase): - def test_exhaustion_reset(self): - iterable = [str(n) for n in range(10)] - - s = mi.seekable(iterable) - self.assertEqual(list(s), iterable) # Normal iteration - self.assertEqual(list(s), []) # Iterable is exhausted - - s.seek(0) - self.assertEqual(list(s), iterable) # Back in action - - def test_partial_reset(self): - iterable = [str(n) for n in range(10)] - - s = mi.seekable(iterable) - self.assertEqual(mi.take(5, s), iterable[:5]) # Normal iteration - - s.seek(1) - self.assertEqual(list(s), iterable[1:]) # Get the rest of the iterable - - def test_forward(self): - iterable = [str(n) for n in range(10)] - - s = mi.seekable(iterable) - self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration - - s.seek(3) # Skip over index 2 - self.assertEqual(list(s), iterable[3:]) # Result is similar to slicing - - s.seek(0) # Back to 0 - self.assertEqual(list(s), iterable) # No difference in result - - def test_past_end(self): - iterable = [str(n) for n in range(10)] - - s = mi.seekable(iterable) - self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration - - s.seek(20) - self.assertEqual(list(s), []) # Iterable is exhausted - - s.seek(0) # Back to 0 - self.assertEqual(list(s), iterable) # No difference in result - - def test_elements(self): - iterable = map(str, count()) - - s = mi.seekable(iterable) - mi.take(10, s) - - elements = s.elements() - self.assertEqual( - [elements[i] for i in range(10)], [str(n) for n in range(10)] - ) - self.assertEqual(len(elements), 10) - - mi.take(10, s) - self.assertEqual(list(elements), [str(n) for n in range(20)]) - - -class SequenceViewTests(TestCase): - def test_init(self): - view = mi.SequenceView((1, 2, 3)) - self.assertEqual(repr(view), "SequenceView((1, 2, 3))") - self.assertRaises(TypeError, lambda: mi.SequenceView({})) - - def test_update(self): - seq = [1, 2, 3] - view = mi.SequenceView(seq) - self.assertEqual(len(view), 3) - self.assertEqual(repr(view), "SequenceView([1, 2, 3])") - - seq.pop() - self.assertEqual(len(view), 2) - self.assertEqual(repr(view), "SequenceView([1, 2])") - - def test_indexing(self): - seq = ('a', 'b', 'c', 'd', 'e', 'f') - view = mi.SequenceView(seq) - for i in range(-len(seq), len(seq)): - self.assertEqual(view[i], seq[i]) - - def test_slicing(self): - seq = ('a', 'b', 'c', 'd', 'e', 'f') - view = mi.SequenceView(seq) - n = len(seq) - indexes = list(range(-n - 1, n + 1)) + [None] - steps = list(range(-n, n + 1)) - steps.remove(0) - for slice_args in product(indexes, indexes, steps): - i = slice(*slice_args) - self.assertEqual(view[i], seq[i]) - - def test_abc_methods(self): - # collections.Sequence should provide all of this functionality - seq = ('a', 'b', 'c', 'd', 'e', 'f', 'f') - view = mi.SequenceView(seq) - - # __contains__ - self.assertIn('b', view) - self.assertNotIn('g', view) - - # __iter__ - self.assertEqual(list(iter(view)), list(seq)) - - # __reversed__ - self.assertEqual(list(reversed(view)), list(reversed(seq))) - - # index - self.assertEqual(view.index('b'), 1) - - # count - self.assertEqual(seq.count('f'), 2) - - -class RunLengthTest(TestCase): - def test_encode(self): - iterable = (int(str(n)[0]) for n in count(800)) - actual = mi.take(4, mi.run_length.encode(iterable)) - expected = [(8, 100), (9, 100), (1, 1000), (2, 1000)] - self.assertEqual(actual, expected) - - def test_decode(self): - iterable = [('d', 4), ('c', 3), ('b', 2), ('a', 1)] - actual = ''.join(mi.run_length.decode(iterable)) - expected = 'ddddcccbba' - self.assertEqual(actual, expected) - - -class ExactlyNTests(TestCase): - """Tests for ``exactly_n()``""" - - def test_true(self): - """Iterable has ``n`` ``True`` elements""" - self.assertTrue(mi.exactly_n([True, False, True], 2)) - self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3)) - self.assertTrue(mi.exactly_n([False, False], 0)) - self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10)) - - def test_false(self): - """Iterable does not have ``n`` ``True`` elements""" - self.assertFalse(mi.exactly_n([True, False, False], 2)) - self.assertFalse(mi.exactly_n([True, True, False], 1)) - self.assertFalse(mi.exactly_n([False], 1)) - self.assertFalse(mi.exactly_n([True], -1)) - self.assertFalse(mi.exactly_n(repeat(True), 100)) - - def test_empty(self): - """Return ``True`` if the iterable is empty and ``n`` is 0""" - self.assertTrue(mi.exactly_n([], 0)) - self.assertFalse(mi.exactly_n([], 1)) - - -class AlwaysReversibleTests(TestCase): - """Tests for ``always_reversible()``""" - - def test_regular_reversed(self): - self.assertEqual(list(reversed(range(10))), - list(mi.always_reversible(range(10)))) - self.assertEqual(list(reversed([1, 2, 3])), - list(mi.always_reversible([1, 2, 3]))) - self.assertEqual(reversed([1, 2, 3]).__class__, - mi.always_reversible([1, 2, 3]).__class__) - - def test_nonseq_reversed(self): - # Create a non-reversible generator from a sequence - with self.assertRaises(TypeError): - reversed(x for x in range(10)) - - self.assertEqual(list(reversed(range(10))), - list(mi.always_reversible(x for x in range(10)))) - self.assertEqual(list(reversed([1, 2, 3])), - list(mi.always_reversible(x for x in [1, 2, 3]))) - self.assertNotEqual(reversed((1, 2)).__class__, - mi.always_reversible(x for x in (1, 2)).__class__) - - -class CircularShiftsTests(TestCase): - def test_empty(self): - # empty iterable -> empty list - self.assertEqual(list(mi.circular_shifts([])), []) - - def test_simple_circular_shifts(self): - # test the a simple iterator case - self.assertEqual( - mi.circular_shifts(range(4)), - [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] - ) - - def test_duplicates(self): - # test non-distinct entries - self.assertEqual( - mi.circular_shifts([0, 1, 0, 1]), - [(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)] - ) - - -class MakeDecoratorTests(TestCase): - def test_basic(self): - slicer = mi.make_decorator(islice) - - @slicer(1, 10, 2) - def user_function(arg_1, arg_2, kwarg_1=None): - self.assertEqual(arg_1, 'arg_1') - self.assertEqual(arg_2, 'arg_2') - self.assertEqual(kwarg_1, 'kwarg_1') - return map(str, count()) - - it = user_function('arg_1', 'arg_2', kwarg_1='kwarg_1') - actual = list(it) - expected = ['1', '3', '5', '7', '9'] - self.assertEqual(actual, expected) - - def test_result_index(self): - def stringify(*args, **kwargs): - self.assertEqual(args[0], 'arg_0') - iterable = args[1] - self.assertEqual(args[2], 'arg_2') - self.assertEqual(kwargs['kwarg_1'], 'kwarg_1') - return map(str, iterable) - - stringifier = mi.make_decorator(stringify, result_index=1) - - @stringifier('arg_0', 'arg_2', kwarg_1='kwarg_1') - def user_function(n): - return count(n) - - it = user_function(1) - actual = mi.take(5, it) - expected = ['1', '2', '3', '4', '5'] - self.assertEqual(actual, expected) - - def test_wrap_class(self): - seeker = mi.make_decorator(mi.seekable) - - @seeker() - def user_function(n): - return map(str, range(n)) - - it = user_function(5) - self.assertEqual(list(it), ['0', '1', '2', '3', '4']) - - it.seek(0) - self.assertEqual(list(it), ['0', '1', '2', '3', '4']) - - -class MapReduceTests(TestCase): - def test_default(self): - iterable = (str(x) for x in range(5)) - keyfunc = lambda x: int(x) // 2 - actual = sorted(mi.map_reduce(iterable, keyfunc).items()) - expected = [(0, ['0', '1']), (1, ['2', '3']), (2, ['4'])] - self.assertEqual(actual, expected) - - def test_valuefunc(self): - iterable = (str(x) for x in range(5)) - keyfunc = lambda x: int(x) // 2 - valuefunc = int - actual = sorted(mi.map_reduce(iterable, keyfunc, valuefunc).items()) - expected = [(0, [0, 1]), (1, [2, 3]), (2, [4])] - self.assertEqual(actual, expected) - - def test_reducefunc(self): - iterable = (str(x) for x in range(5)) - keyfunc = lambda x: int(x) // 2 - valuefunc = int - reducefunc = lambda value_list: reduce(mul, value_list, 1) - actual = sorted( - mi.map_reduce(iterable, keyfunc, valuefunc, reducefunc).items() - ) - expected = [(0, 0), (1, 6), (2, 4)] - self.assertEqual(actual, expected) - - def test_ret(self): - d = mi.map_reduce([1, 0, 2, 0, 1, 0], bool) - self.assertEqual(d, {False: [0, 0, 0], True: [1, 2, 1]}) - self.assertRaises(KeyError, lambda: d[None].append(1)) - - -class RlocateTests(TestCase): - def test_default_pred(self): - iterable = [0, 1, 1, 0, 1, 0, 0] - for it in (iterable[:], iter(iterable)): - actual = list(mi.rlocate(it)) - expected = [4, 2, 1] - self.assertEqual(actual, expected) - - def test_no_matches(self): - iterable = [0, 0, 0] - for it in (iterable[:], iter(iterable)): - actual = list(mi.rlocate(it)) - expected = [] - self.assertEqual(actual, expected) - - def test_custom_pred(self): - iterable = ['0', 1, 1, '0', 1, '0', '0'] - pred = lambda x: x == '0' - for it in (iterable[:], iter(iterable)): - actual = list(mi.rlocate(it, pred)) - expected = [6, 5, 3, 0] - self.assertEqual(actual, expected) - - def test_efficient_reversal(self): - iterable = range(10 ** 10) # Is efficiently reversible - target = 10 ** 10 - 2 - pred = lambda x: x == target # Find-able from the right - actual = next(mi.rlocate(iterable, pred)) - self.assertEqual(actual, target) - - def test_window_size(self): - iterable = ['0', 1, 1, '0', 1, '0', '0'] - pred = lambda *args: args == ('0', 1) - for it in (iterable, iter(iterable)): - actual = list(mi.rlocate(it, pred, window_size=2)) - expected = [3, 0] - self.assertEqual(actual, expected) - - def test_window_size_large(self): - iterable = [1, 2, 3, 4] - pred = lambda a, b, c, d, e: True - for it in (iterable, iter(iterable)): - actual = list(mi.rlocate(iterable, pred, window_size=5)) - expected = [0] - self.assertEqual(actual, expected) - - def test_window_size_zero(self): - iterable = [1, 2, 3, 4] - pred = lambda: True - for it in (iterable, iter(iterable)): - with self.assertRaises(ValueError): - list(mi.locate(iterable, pred, window_size=0)) - - -class ReplaceTests(TestCase): - def test_basic(self): - iterable = range(10) - pred = lambda x: x % 2 == 0 - substitutes = [] - actual = list(mi.replace(iterable, pred, substitutes)) - expected = [1, 3, 5, 7, 9] - self.assertEqual(actual, expected) - - def test_count(self): - iterable = range(10) - pred = lambda x: x % 2 == 0 - substitutes = [] - actual = list(mi.replace(iterable, pred, substitutes, count=4)) - expected = [1, 3, 5, 7, 8, 9] - self.assertEqual(actual, expected) - - def test_window_size(self): - iterable = range(10) - pred = lambda *args: args == (0, 1, 2) - substitutes = [] - actual = list(mi.replace(iterable, pred, substitutes, window_size=3)) - expected = [3, 4, 5, 6, 7, 8, 9] - self.assertEqual(actual, expected) - - def test_window_size_end(self): - iterable = range(10) - pred = lambda *args: args == (7, 8, 9) - substitutes = [] - actual = list(mi.replace(iterable, pred, substitutes, window_size=3)) - expected = [0, 1, 2, 3, 4, 5, 6] - self.assertEqual(actual, expected) - - def test_window_size_count(self): - iterable = range(10) - pred = lambda *args: (args == (0, 1, 2)) or (args == (7, 8, 9)) - substitutes = [] - actual = list( - mi.replace(iterable, pred, substitutes, count=1, window_size=3) - ) - expected = [3, 4, 5, 6, 7, 8, 9] - self.assertEqual(actual, expected) - - def test_window_size_large(self): - iterable = range(4) - pred = lambda a, b, c, d, e: True - substitutes = [5, 6, 7] - actual = list(mi.replace(iterable, pred, substitutes, window_size=5)) - expected = [5, 6, 7] - self.assertEqual(actual, expected) - - def test_window_size_zero(self): - iterable = range(10) - pred = lambda *args: True - substitutes = [] - with self.assertRaises(ValueError): - list(mi.replace(iterable, pred, substitutes, window_size=0)) - - def test_iterable_substitutes(self): - iterable = range(5) - pred = lambda x: x % 2 == 0 - substitutes = iter('__') - actual = list(mi.replace(iterable, pred, substitutes)) - expected = ['_', '_', 1, '_', '_', 3, '_', '_'] - self.assertEqual(actual, expected) diff --git a/libraries/more_itertools/tests/test_recipes.py b/libraries/more_itertools/tests/test_recipes.py deleted file mode 100644 index 98981fe8..00000000 --- a/libraries/more_itertools/tests/test_recipes.py +++ /dev/null @@ -1,616 +0,0 @@ -from doctest import DocTestSuite -from unittest import TestCase - -from itertools import combinations -from six.moves import range - -import more_itertools as mi - - -def load_tests(loader, tests, ignore): - # Add the doctests - tests.addTests(DocTestSuite('more_itertools.recipes')) - return tests - - -class AccumulateTests(TestCase): - """Tests for ``accumulate()``""" - - def test_empty(self): - """Test that an empty input returns an empty output""" - self.assertEqual(list(mi.accumulate([])), []) - - def test_default(self): - """Test accumulate with the default function (addition)""" - self.assertEqual(list(mi.accumulate([1, 2, 3])), [1, 3, 6]) - - def test_bogus_function(self): - """Test accumulate with an invalid function""" - with self.assertRaises(TypeError): - list(mi.accumulate([1, 2, 3], func=lambda x: x)) - - def test_custom_function(self): - """Test accumulate with a custom function""" - self.assertEqual( - list(mi.accumulate((1, 2, 3, 2, 1), func=max)), [1, 2, 3, 3, 3] - ) - - -class TakeTests(TestCase): - """Tests for ``take()``""" - - def test_simple_take(self): - """Test basic usage""" - t = mi.take(5, range(10)) - self.assertEqual(t, [0, 1, 2, 3, 4]) - - def test_null_take(self): - """Check the null case""" - t = mi.take(0, range(10)) - self.assertEqual(t, []) - - def test_negative_take(self): - """Make sure taking negative items results in a ValueError""" - self.assertRaises(ValueError, lambda: mi.take(-3, range(10))) - - def test_take_too_much(self): - """Taking more than an iterator has remaining should return what the - iterator has remaining. - - """ - t = mi.take(10, range(5)) - self.assertEqual(t, [0, 1, 2, 3, 4]) - - -class TabulateTests(TestCase): - """Tests for ``tabulate()``""" - - def test_simple_tabulate(self): - """Test the happy path""" - t = mi.tabulate(lambda x: x) - f = tuple([next(t) for _ in range(3)]) - self.assertEqual(f, (0, 1, 2)) - - def test_count(self): - """Ensure tabulate accepts specific count""" - t = mi.tabulate(lambda x: 2 * x, -1) - f = (next(t), next(t), next(t)) - self.assertEqual(f, (-2, 0, 2)) - - -class TailTests(TestCase): - """Tests for ``tail()``""" - - def test_greater(self): - """Length of iterable is greather than requested tail""" - self.assertEqual(list(mi.tail(3, 'ABCDEFG')), ['E', 'F', 'G']) - - def test_equal(self): - """Length of iterable is equal to the requested tail""" - self.assertEqual( - list(mi.tail(7, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G'] - ) - - def test_less(self): - """Length of iterable is less than requested tail""" - self.assertEqual( - list(mi.tail(8, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G'] - ) - - -class ConsumeTests(TestCase): - """Tests for ``consume()``""" - - def test_sanity(self): - """Test basic functionality""" - r = (x for x in range(10)) - mi.consume(r, 3) - self.assertEqual(3, next(r)) - - def test_null_consume(self): - """Check the null case""" - r = (x for x in range(10)) - mi.consume(r, 0) - self.assertEqual(0, next(r)) - - def test_negative_consume(self): - """Check that negative consumsion throws an error""" - r = (x for x in range(10)) - self.assertRaises(ValueError, lambda: mi.consume(r, -1)) - - def test_total_consume(self): - """Check that iterator is totally consumed by default""" - r = (x for x in range(10)) - mi.consume(r) - self.assertRaises(StopIteration, lambda: next(r)) - - -class NthTests(TestCase): - """Tests for ``nth()``""" - - def test_basic(self): - """Make sure the nth item is returned""" - l = range(10) - for i, v in enumerate(l): - self.assertEqual(mi.nth(l, i), v) - - def test_default(self): - """Ensure a default value is returned when nth item not found""" - l = range(3) - self.assertEqual(mi.nth(l, 100, "zebra"), "zebra") - - def test_negative_item_raises(self): - """Ensure asking for a negative item raises an exception""" - self.assertRaises(ValueError, lambda: mi.nth(range(10), -3)) - - -class AllEqualTests(TestCase): - """Tests for ``all_equal()``""" - - def test_true(self): - """Everything is equal""" - self.assertTrue(mi.all_equal('aaaaaa')) - self.assertTrue(mi.all_equal([0, 0, 0, 0])) - - def test_false(self): - """Not everything is equal""" - self.assertFalse(mi.all_equal('aaaaab')) - self.assertFalse(mi.all_equal([0, 0, 0, 1])) - - def test_tricky(self): - """Not everything is identical, but everything is equal""" - items = [1, complex(1, 0), 1.0] - self.assertTrue(mi.all_equal(items)) - - def test_empty(self): - """Return True if the iterable is empty""" - self.assertTrue(mi.all_equal('')) - self.assertTrue(mi.all_equal([])) - - def test_one(self): - """Return True if the iterable is singular""" - self.assertTrue(mi.all_equal('0')) - self.assertTrue(mi.all_equal([0])) - - -class QuantifyTests(TestCase): - """Tests for ``quantify()``""" - - def test_happy_path(self): - """Make sure True count is returned""" - q = [True, False, True] - self.assertEqual(mi.quantify(q), 2) - - def test_custom_predicate(self): - """Ensure non-default predicates return as expected""" - q = range(10) - self.assertEqual(mi.quantify(q, lambda x: x % 2 == 0), 5) - - -class PadnoneTests(TestCase): - """Tests for ``padnone()``""" - - def test_happy_path(self): - """wrapper iterator should return None indefinitely""" - r = range(2) - p = mi.padnone(r) - self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)]) - - -class NcyclesTests(TestCase): - """Tests for ``nyclces()``""" - - def test_happy_path(self): - """cycle a sequence three times""" - r = ["a", "b", "c"] - n = mi.ncycles(r, 3) - self.assertEqual( - ["a", "b", "c", "a", "b", "c", "a", "b", "c"], - list(n) - ) - - def test_null_case(self): - """asking for 0 cycles should return an empty iterator""" - n = mi.ncycles(range(100), 0) - self.assertRaises(StopIteration, lambda: next(n)) - - def test_pathalogical_case(self): - """asking for negative cycles should return an empty iterator""" - n = mi.ncycles(range(100), -10) - self.assertRaises(StopIteration, lambda: next(n)) - - -class DotproductTests(TestCase): - """Tests for ``dotproduct()``'""" - - def test_happy_path(self): - """simple dotproduct example""" - self.assertEqual(400, mi.dotproduct([10, 10], [20, 20])) - - -class FlattenTests(TestCase): - """Tests for ``flatten()``""" - - def test_basic_usage(self): - """ensure list of lists is flattened one level""" - f = [[0, 1, 2], [3, 4, 5]] - self.assertEqual(list(range(6)), list(mi.flatten(f))) - - def test_single_level(self): - """ensure list of lists is flattened only one level""" - f = [[0, [1, 2]], [[3, 4], 5]] - self.assertEqual([0, [1, 2], [3, 4], 5], list(mi.flatten(f))) - - -class RepeatfuncTests(TestCase): - """Tests for ``repeatfunc()``""" - - def test_simple_repeat(self): - """test simple repeated functions""" - r = mi.repeatfunc(lambda: 5) - self.assertEqual([5, 5, 5, 5, 5], [next(r) for _ in range(5)]) - - def test_finite_repeat(self): - """ensure limited repeat when times is provided""" - r = mi.repeatfunc(lambda: 5, times=5) - self.assertEqual([5, 5, 5, 5, 5], list(r)) - - def test_added_arguments(self): - """ensure arguments are applied to the function""" - r = mi.repeatfunc(lambda x: x, 2, 3) - self.assertEqual([3, 3], list(r)) - - def test_null_times(self): - """repeat 0 should return an empty iterator""" - r = mi.repeatfunc(range, 0, 3) - self.assertRaises(StopIteration, lambda: next(r)) - - -class PairwiseTests(TestCase): - """Tests for ``pairwise()``""" - - def test_base_case(self): - """ensure an iterable will return pairwise""" - p = mi.pairwise([1, 2, 3]) - self.assertEqual([(1, 2), (2, 3)], list(p)) - - def test_short_case(self): - """ensure an empty iterator if there's not enough values to pair""" - p = mi.pairwise("a") - self.assertRaises(StopIteration, lambda: next(p)) - - -class GrouperTests(TestCase): - """Tests for ``grouper()``""" - - def test_even(self): - """Test when group size divides evenly into the length of - the iterable. - - """ - self.assertEqual( - list(mi.grouper(3, 'ABCDEF')), [('A', 'B', 'C'), ('D', 'E', 'F')] - ) - - def test_odd(self): - """Test when group size does not divide evenly into the length of the - iterable. - - """ - self.assertEqual( - list(mi.grouper(3, 'ABCDE')), [('A', 'B', 'C'), ('D', 'E', None)] - ) - - def test_fill_value(self): - """Test that the fill value is used to pad the final group""" - self.assertEqual( - list(mi.grouper(3, 'ABCDE', 'x')), - [('A', 'B', 'C'), ('D', 'E', 'x')] - ) - - -class RoundrobinTests(TestCase): - """Tests for ``roundrobin()``""" - - def test_even_groups(self): - """Ensure ordered output from evenly populated iterables""" - self.assertEqual( - list(mi.roundrobin('ABC', [1, 2, 3], range(3))), - ['A', 1, 0, 'B', 2, 1, 'C', 3, 2] - ) - - def test_uneven_groups(self): - """Ensure ordered output from unevenly populated iterables""" - self.assertEqual( - list(mi.roundrobin('ABCD', [1, 2], range(0))), - ['A', 1, 'B', 2, 'C', 'D'] - ) - - -class PartitionTests(TestCase): - """Tests for ``partition()``""" - - def test_bool(self): - """Test when pred() returns a boolean""" - lesser, greater = mi.partition(lambda x: x > 5, range(10)) - self.assertEqual(list(lesser), [0, 1, 2, 3, 4, 5]) - self.assertEqual(list(greater), [6, 7, 8, 9]) - - def test_arbitrary(self): - """Test when pred() returns an integer""" - divisibles, remainders = mi.partition(lambda x: x % 3, range(10)) - self.assertEqual(list(divisibles), [0, 3, 6, 9]) - self.assertEqual(list(remainders), [1, 2, 4, 5, 7, 8]) - - -class PowersetTests(TestCase): - """Tests for ``powerset()``""" - - def test_combinatorics(self): - """Ensure a proper enumeration""" - p = mi.powerset([1, 2, 3]) - self.assertEqual( - list(p), - [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] - ) - - -class UniqueEverseenTests(TestCase): - """Tests for ``unique_everseen()``""" - - def test_everseen(self): - """ensure duplicate elements are ignored""" - u = mi.unique_everseen('AAAABBBBCCDAABBB') - self.assertEqual( - ['A', 'B', 'C', 'D'], - list(u) - ) - - def test_custom_key(self): - """ensure the custom key comparison works""" - u = mi.unique_everseen('aAbACCc', key=str.lower) - self.assertEqual(list('abC'), list(u)) - - def test_unhashable(self): - """ensure things work for unhashable items""" - iterable = ['a', [1, 2, 3], [1, 2, 3], 'a'] - u = mi.unique_everseen(iterable) - self.assertEqual(list(u), ['a', [1, 2, 3]]) - - def test_unhashable_key(self): - """ensure things work for unhashable items with a custom key""" - iterable = ['a', [1, 2, 3], [1, 2, 3], 'a'] - u = mi.unique_everseen(iterable, key=lambda x: x) - self.assertEqual(list(u), ['a', [1, 2, 3]]) - - -class UniqueJustseenTests(TestCase): - """Tests for ``unique_justseen()``""" - - def test_justseen(self): - """ensure only last item is remembered""" - u = mi.unique_justseen('AAAABBBCCDABB') - self.assertEqual(list('ABCDAB'), list(u)) - - def test_custom_key(self): - """ensure the custom key comparison works""" - u = mi.unique_justseen('AABCcAD', str.lower) - self.assertEqual(list('ABCAD'), list(u)) - - -class IterExceptTests(TestCase): - """Tests for ``iter_except()``""" - - def test_exact_exception(self): - """ensure the exact specified exception is caught""" - l = [1, 2, 3] - i = mi.iter_except(l.pop, IndexError) - self.assertEqual(list(i), [3, 2, 1]) - - def test_generic_exception(self): - """ensure the generic exception can be caught""" - l = [1, 2] - i = mi.iter_except(l.pop, Exception) - self.assertEqual(list(i), [2, 1]) - - def test_uncaught_exception_is_raised(self): - """ensure a non-specified exception is raised""" - l = [1, 2, 3] - i = mi.iter_except(l.pop, KeyError) - self.assertRaises(IndexError, lambda: list(i)) - - def test_first(self): - """ensure first is run before the function""" - l = [1, 2, 3] - f = lambda: 25 - i = mi.iter_except(l.pop, IndexError, f) - self.assertEqual(list(i), [25, 3, 2, 1]) - - -class FirstTrueTests(TestCase): - """Tests for ``first_true()``""" - - def test_something_true(self): - """Test with no keywords""" - self.assertEqual(mi.first_true(range(10)), 1) - - def test_nothing_true(self): - """Test default return value.""" - self.assertEqual(mi.first_true([0, 0, 0]), False) - - def test_default(self): - """Test with a default keyword""" - self.assertEqual(mi.first_true([0, 0, 0], default='!'), '!') - - def test_pred(self): - """Test with a custom predicate""" - self.assertEqual( - mi.first_true([2, 4, 6], pred=lambda x: x % 3 == 0), 6 - ) - - -class RandomProductTests(TestCase): - """Tests for ``random_product()`` - - Since random.choice() has different results with the same seed across - python versions 2.x and 3.x, these tests use highly probably events to - create predictable outcomes across platforms. - """ - - def test_simple_lists(self): - """Ensure that one item is chosen from each list in each pair. - Also ensure that each item from each list eventually appears in - the chosen combinations. - - Odds are roughly 1 in 7.1 * 10e16 that one item from either list will - not be chosen after 100 samplings of one item from each list. Just to - be safe, better use a known random seed, too. - - """ - nums = [1, 2, 3] - lets = ['a', 'b', 'c'] - n, m = zip(*[mi.random_product(nums, lets) for _ in range(100)]) - n, m = set(n), set(m) - self.assertEqual(n, set(nums)) - self.assertEqual(m, set(lets)) - self.assertEqual(len(n), len(nums)) - self.assertEqual(len(m), len(lets)) - - def test_list_with_repeat(self): - """ensure multiple items are chosen, and that they appear to be chosen - from one list then the next, in proper order. - - """ - nums = [1, 2, 3] - lets = ['a', 'b', 'c'] - r = list(mi.random_product(nums, lets, repeat=100)) - self.assertEqual(2 * 100, len(r)) - n, m = set(r[::2]), set(r[1::2]) - self.assertEqual(n, set(nums)) - self.assertEqual(m, set(lets)) - self.assertEqual(len(n), len(nums)) - self.assertEqual(len(m), len(lets)) - - -class RandomPermutationTests(TestCase): - """Tests for ``random_permutation()``""" - - def test_full_permutation(self): - """ensure every item from the iterable is returned in a new ordering - - 15 elements have a 1 in 1.3 * 10e12 of appearing in sorted order, so - we fix a seed value just to be sure. - - """ - i = range(15) - r = mi.random_permutation(i) - self.assertEqual(set(i), set(r)) - if i == r: - raise AssertionError("Values were not permuted") - - def test_partial_permutation(self): - """ensure all returned items are from the iterable, that the returned - permutation is of the desired length, and that all items eventually - get returned. - - Sampling 100 permutations of length 5 from a set of 15 leaves a - (2/3)^100 chance that an item will not be chosen. Multiplied by 15 - items, there is a 1 in 2.6e16 chance that at least 1 item will not - show up in the resulting output. Using a random seed will fix that. - - """ - items = range(15) - item_set = set(items) - all_items = set() - for _ in range(100): - permutation = mi.random_permutation(items, 5) - self.assertEqual(len(permutation), 5) - permutation_set = set(permutation) - self.assertLessEqual(permutation_set, item_set) - all_items |= permutation_set - self.assertEqual(all_items, item_set) - - -class RandomCombinationTests(TestCase): - """Tests for ``random_combination()``""" - - def test_psuedorandomness(self): - """ensure different subsets of the iterable get returned over many - samplings of random combinations""" - items = range(15) - all_items = set() - for _ in range(50): - combination = mi.random_combination(items, 5) - all_items |= set(combination) - self.assertEqual(all_items, set(items)) - - def test_no_replacement(self): - """ensure that elements are sampled without replacement""" - items = range(15) - for _ in range(50): - combination = mi.random_combination(items, len(items)) - self.assertEqual(len(combination), len(set(combination))) - self.assertRaises( - ValueError, lambda: mi.random_combination(items, len(items) + 1) - ) - - -class RandomCombinationWithReplacementTests(TestCase): - """Tests for ``random_combination_with_replacement()``""" - - def test_replacement(self): - """ensure that elements are sampled with replacement""" - items = range(5) - combo = mi.random_combination_with_replacement(items, len(items) * 2) - self.assertEqual(2 * len(items), len(combo)) - if len(set(combo)) == len(combo): - raise AssertionError("Combination contained no duplicates") - - def test_pseudorandomness(self): - """ensure different subsets of the iterable get returned over many - samplings of random combinations""" - items = range(15) - all_items = set() - for _ in range(50): - combination = mi.random_combination_with_replacement(items, 5) - all_items |= set(combination) - self.assertEqual(all_items, set(items)) - - -class NthCombinationTests(TestCase): - def test_basic(self): - iterable = 'abcdefg' - r = 4 - for index, expected in enumerate(combinations(iterable, r)): - actual = mi.nth_combination(iterable, r, index) - self.assertEqual(actual, expected) - - def test_long(self): - actual = mi.nth_combination(range(180), 4, 2000000) - expected = (2, 12, 35, 126) - self.assertEqual(actual, expected) - - def test_invalid_r(self): - for r in (-1, 3): - with self.assertRaises(ValueError): - mi.nth_combination([], r, 0) - - def test_invalid_index(self): - with self.assertRaises(IndexError): - mi.nth_combination('abcdefg', 3, -36) - - -class PrependTests(TestCase): - def test_basic(self): - value = 'a' - iterator = iter('bcdefg') - actual = list(mi.prepend(value, iterator)) - expected = list('abcdefg') - self.assertEqual(actual, expected) - - def test_multiple(self): - value = 'ab' - iterator = iter('cdefg') - actual = tuple(mi.prepend(value, iterator)) - expected = ('ab',) + tuple('cdefg') - self.assertEqual(actual, expected) diff --git a/libraries/portend.py b/libraries/portend.py deleted file mode 100644 index 4c393806..00000000 --- a/libraries/portend.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -A simple library for managing the availability of ports. -""" - -from __future__ import print_function, division - -import time -import socket -import argparse -import sys -import itertools -import contextlib -import collections -import platform - -from tempora import timing - - -def client_host(server_host): - """Return the host on which a client can connect to the given listener.""" - if server_host == '0.0.0.0': - # 0.0.0.0 is INADDR_ANY, which should answer on localhost. - return '127.0.0.1' - if server_host in ('::', '::0', '::0.0.0.0'): - # :: is IN6ADDR_ANY, which should answer on localhost. - # ::0 and ::0.0.0.0 are non-canonical but common - # ways to write IN6ADDR_ANY. - return '::1' - return server_host - - -class Checker(object): - def __init__(self, timeout=1.0): - self.timeout = timeout - - def assert_free(self, host, port=None): - """ - Assert that the given addr is free - in that all attempts to connect fail within the timeout - or raise a PortNotFree exception. - - >>> free_port = find_available_local_port() - - >>> Checker().assert_free('localhost', free_port) - >>> Checker().assert_free('127.0.0.1', free_port) - >>> Checker().assert_free('::1', free_port) - - Also accepts an addr tuple - - >>> addr = '::1', free_port, 0, 0 - >>> Checker().assert_free(addr) - - Host might refer to a server bind address like '::', which - should use localhost to perform the check. - - >>> Checker().assert_free('::', free_port) - """ - if port is None and isinstance(host, collections.Sequence): - host, port = host[:2] - if platform.system() == 'Windows': - host = client_host(host) - info = socket.getaddrinfo( - host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, - ) - list(itertools.starmap(self._connect, info)) - - def _connect(self, af, socktype, proto, canonname, sa): - s = socket.socket(af, socktype, proto) - # fail fast with a small timeout - s.settimeout(self.timeout) - - with contextlib.closing(s): - try: - s.connect(sa) - except socket.error: - return - - # the connect succeeded, so the port isn't free - port, host = sa[:2] - tmpl = "Port {port} is in use on {host}." - raise PortNotFree(tmpl.format(**locals())) - - -class Timeout(IOError): - pass - - -class PortNotFree(IOError): - pass - - -def free(host, port, timeout=float('Inf')): - """ - Wait for the specified port to become free (dropping or rejecting - requests). Return when the port is free or raise a Timeout if timeout has - elapsed. - - Timeout may be specified in seconds or as a timedelta. - If timeout is None or ∞, the routine will run indefinitely. - - >>> free('localhost', find_available_local_port()) - """ - if not host: - raise ValueError("Host values of '' or None are not allowed.") - - timer = timing.Timer(timeout) - - while not timer.expired(): - try: - # Expect a free port, so use a small timeout - Checker(timeout=0.1).assert_free(host, port) - return - except PortNotFree: - # Politely wait. - time.sleep(0.1) - - raise Timeout("Port {port} not free on {host}.".format(**locals())) -wait_for_free_port = free - - -def occupied(host, port, timeout=float('Inf')): - """ - Wait for the specified port to become occupied (accepting requests). - Return when the port is occupied or raise a Timeout if timeout has - elapsed. - - Timeout may be specified in seconds or as a timedelta. - If timeout is None or ∞, the routine will run indefinitely. - - >>> occupied('localhost', find_available_local_port(), .1) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - Timeout: Port ... not bound on localhost. - """ - if not host: - raise ValueError("Host values of '' or None are not allowed.") - - timer = timing.Timer(timeout) - - while not timer.expired(): - try: - Checker(timeout=.5).assert_free(host, port) - # Politely wait - time.sleep(0.1) - except PortNotFree: - # port is occupied - return - - raise Timeout("Port {port} not bound on {host}.".format(**locals())) -wait_for_occupied_port = occupied - - -def find_available_local_port(): - """ - Find a free port on localhost. - - >>> 0 < find_available_local_port() < 65536 - True - """ - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - addr = '', 0 - sock.bind(addr) - addr, port = sock.getsockname()[:2] - sock.close() - return port - - -class HostPort(str): - """ - A simple representation of a host/port pair as a string - - >>> hp = HostPort('localhost:32768') - - >>> hp.host - 'localhost' - - >>> hp.port - 32768 - - >>> len(hp) - 15 - """ - - @property - def host(self): - host, sep, port = self.partition(':') - return host - - @property - def port(self): - host, sep, port = self.partition(':') - return int(port) - - -def _main(): - parser = argparse.ArgumentParser() - global_lookup = lambda key: globals()[key] - parser.add_argument('target', metavar='host:port', type=HostPort) - parser.add_argument('func', metavar='state', type=global_lookup) - parser.add_argument('-t', '--timeout', default=None, type=float) - args = parser.parse_args() - try: - args.func(args.target.host, args.target.port, timeout=args.timeout) - except Timeout as timeout: - print(timeout, file=sys.stderr) - raise SystemExit(1) - - -if __name__ == '__main__': - _main() diff --git a/libraries/tempora/__init__.py b/libraries/tempora/__init__.py deleted file mode 100644 index e0cdead0..00000000 --- a/libraries/tempora/__init__.py +++ /dev/null @@ -1,505 +0,0 @@ -# -*- coding: UTF-8 -*- - -"Objects and routines pertaining to date and time (tempora)" - -from __future__ import division, unicode_literals - -import datetime -import time -import re -import numbers -import functools - -import six - -__metaclass__ = type - - -class Parser: - """ - Datetime parser: parses a date-time string using multiple possible - formats. - - >>> p = Parser(('%H%M', '%H:%M')) - >>> tuple(p.parse('1319')) - (1900, 1, 1, 13, 19, 0, 0, 1, -1) - >>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y')) - >>> tuple(dateParser.parse('2003-12-20')) - (2003, 12, 20, 0, 0, 0, 5, 354, -1) - >>> tuple(dateParser.parse('16-Dec-1994')) - (1994, 12, 16, 0, 0, 0, 4, 350, -1) - >>> tuple(dateParser.parse('5/19/2003')) - (2003, 5, 19, 0, 0, 0, 0, 139, -1) - >>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y')) - >>> tuple(dtParser.parse('2003-12-20 19:13:26')) - (2003, 12, 20, 19, 13, 26, 5, 354, -1) - >>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004')) - (2004, 1, 20, 16, 19, 33, 1, 20, -1) - - Be forewarned, a ValueError will be raised if more than one format - matches: - - >>> Parser(('%H%M', '%H%M%S')).parse('732') - Traceback (most recent call last): - ... - ValueError: More than one format string matched target 732. - """ - - formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y') - "some common default formats" - - def __init__(self, formats=None): - if formats: - self.formats = formats - - def parse(self, target): - self.target = target - results = tuple(filter(None, map(self._parse, self.formats))) - del self.target - if not results: - tmpl = "No format strings matched the target {target}." - raise ValueError(tmpl.format(**locals())) - if not len(results) == 1: - tmpl = "More than one format string matched target {target}." - raise ValueError(tmpl.format(**locals())) - return results[0] - - def _parse(self, format): - try: - result = time.strptime(self.target, format) - except ValueError: - result = False - return result - - -# some useful constants -osc_per_year = 290091329207984000 -""" -mean vernal equinox year expressed in oscillations of atomic cesium at the -year 2000 (see http://webexhibits.org/calendars/timeline.html for more info). -""" -osc_per_second = 9192631770 -seconds_per_second = 1 -seconds_per_year = 31556940 -seconds_per_minute = 60 -minutes_per_hour = 60 -hours_per_day = 24 -seconds_per_hour = seconds_per_minute * minutes_per_hour -seconds_per_day = seconds_per_hour * hours_per_day -days_per_year = seconds_per_year / seconds_per_day -thirty_days = datetime.timedelta(days=30) -# these values provide useful averages -six_months = datetime.timedelta(days=days_per_year / 2) -seconds_per_month = seconds_per_year / 12 -hours_per_month = hours_per_day * days_per_year / 12 - - -def strftime(fmt, t): - """A class to replace the strftime in datetime package or time module. - Identical to strftime behavior in those modules except supports any - year. - Also supports datetime.datetime times. - Also supports milliseconds using %s - Also supports microseconds using %u""" - if isinstance(t, (time.struct_time, tuple)): - t = datetime.datetime(*t[:6]) - assert isinstance(t, (datetime.datetime, datetime.time, datetime.date)) - try: - year = t.year - if year < 1900: - t = t.replace(year=1900) - except AttributeError: - year = 1900 - subs = ( - ('%Y', '%04d' % year), - ('%y', '%02d' % (year % 100)), - ('%s', '%03d' % (t.microsecond // 1000)), - ('%u', '%03d' % (t.microsecond % 1000)) - ) - - def doSub(s, sub): - return s.replace(*sub) - - def doSubs(s): - return functools.reduce(doSub, subs, s) - - fmt = '%%'.join(map(doSubs, fmt.split('%%'))) - return t.strftime(fmt) - - -def strptime(s, fmt, tzinfo=None): - """ - A function to replace strptime in the time module. Should behave - identically to the strptime function except it returns a datetime.datetime - object instead of a time.struct_time object. - Also takes an optional tzinfo parameter which is a time zone info object. - """ - res = time.strptime(s, fmt) - return datetime.datetime(tzinfo=tzinfo, *res[:6]) - - -class DatetimeConstructor: - """ - >>> cd = DatetimeConstructor.construct_datetime - >>> cd(datetime.datetime(2011,1,1)) - datetime.datetime(2011, 1, 1, 0, 0) - """ - @classmethod - def construct_datetime(cls, *args, **kwargs): - """Construct a datetime.datetime from a number of different time - types found in python and pythonwin""" - if len(args) == 1: - arg = args[0] - method = cls.__get_dt_constructor( - type(arg).__module__, - type(arg).__name__, - ) - result = method(arg) - try: - result = result.replace(tzinfo=kwargs.pop('tzinfo')) - except KeyError: - pass - if kwargs: - first_key = kwargs.keys()[0] - tmpl = ( - "{first_key} is an invalid keyword " - "argument for this function." - ) - raise TypeError(tmpl.format(**locals())) - else: - result = datetime.datetime(*args, **kwargs) - return result - - @classmethod - def __get_dt_constructor(cls, moduleName, name): - try: - method_name = '__dt_from_{moduleName}_{name}__'.format(**locals()) - return getattr(cls, method_name) - except AttributeError: - tmpl = ( - "No way to construct datetime.datetime from " - "{moduleName}.{name}" - ) - raise TypeError(tmpl.format(**locals())) - - @staticmethod - def __dt_from_datetime_datetime__(source): - dtattrs = ( - 'year', 'month', 'day', 'hour', 'minute', 'second', - 'microsecond', 'tzinfo', - ) - attrs = map(lambda a: getattr(source, a), dtattrs) - return datetime.datetime(*attrs) - - @staticmethod - def __dt_from___builtin___time__(pyt): - "Construct a datetime.datetime from a pythonwin time" - fmtString = '%Y-%m-%d %H:%M:%S' - result = strptime(pyt.Format(fmtString), fmtString) - # get milliseconds and microseconds. The only way to do this is - # to use the __float__ attribute of the time, which is in days. - microseconds_per_day = seconds_per_day * 1000000 - microseconds = float(pyt) * microseconds_per_day - microsecond = int(microseconds % 1000000) - result = result.replace(microsecond=microsecond) - return result - - @staticmethod - def __dt_from_timestamp__(timestamp): - return datetime.datetime.utcfromtimestamp(timestamp) - __dt_from___builtin___float__ = __dt_from_timestamp__ - __dt_from___builtin___long__ = __dt_from_timestamp__ - __dt_from___builtin___int__ = __dt_from_timestamp__ - - @staticmethod - def __dt_from_time_struct_time__(s): - return datetime.datetime(*s[:6]) - - -def datetime_mod(dt, period, start=None): - """ - Find the time which is the specified date/time truncated to the time delta - relative to the start date/time. - By default, the start time is midnight of the same day as the specified - date/time. - - >>> datetime_mod(datetime.datetime(2004, 1, 2, 3), - ... datetime.timedelta(days = 1.5), - ... start = datetime.datetime(2004, 1, 1)) - datetime.datetime(2004, 1, 1, 0, 0) - >>> datetime_mod(datetime.datetime(2004, 1, 2, 13), - ... datetime.timedelta(days = 1.5), - ... start = datetime.datetime(2004, 1, 1)) - datetime.datetime(2004, 1, 2, 12, 0) - >>> datetime_mod(datetime.datetime(2004, 1, 2, 13), - ... datetime.timedelta(days = 7), - ... start = datetime.datetime(2004, 1, 1)) - datetime.datetime(2004, 1, 1, 0, 0) - >>> datetime_mod(datetime.datetime(2004, 1, 10, 13), - ... datetime.timedelta(days = 7), - ... start = datetime.datetime(2004, 1, 1)) - datetime.datetime(2004, 1, 8, 0, 0) - """ - if start is None: - # use midnight of the same day - start = datetime.datetime.combine(dt.date(), datetime.time()) - # calculate the difference between the specified time and the start date. - delta = dt - start - - # now aggregate the delta and the period into microseconds - # Use microseconds because that's the highest precision of these time - # pieces. Also, using microseconds ensures perfect precision (no floating - # point errors). - def get_time_delta_microseconds(td): - return (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds - delta, period = map(get_time_delta_microseconds, (delta, period)) - offset = datetime.timedelta(microseconds=delta % period) - # the result is the original specified time minus the offset - result = dt - offset - return result - - -def datetime_round(dt, period, start=None): - """ - Find the nearest even period for the specified date/time. - - >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13), - ... datetime.timedelta(hours = 1)) - datetime.datetime(2004, 11, 13, 8, 0) - >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13), - ... datetime.timedelta(hours = 1)) - datetime.datetime(2004, 11, 13, 9, 0) - >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30), - ... datetime.timedelta(hours = 1)) - datetime.datetime(2004, 11, 13, 9, 0) - """ - result = datetime_mod(dt, period, start) - if abs(dt - result) >= period // 2: - result += period - return result - - -def get_nearest_year_for_day(day): - """ - Returns the nearest year to now inferred from a Julian date. - """ - now = time.gmtime() - result = now.tm_year - # if the day is far greater than today, it must be from last year - if day - now.tm_yday > 365 // 2: - result -= 1 - # if the day is far less than today, it must be for next year. - if now.tm_yday - day > 365 // 2: - result += 1 - return result - - -def gregorian_date(year, julian_day): - """ - Gregorian Date is defined as a year and a julian day (1-based - index into the days of the year). - - >>> gregorian_date(2007, 15) - datetime.date(2007, 1, 15) - """ - result = datetime.date(year, 1, 1) - result += datetime.timedelta(days=julian_day - 1) - return result - - -def get_period_seconds(period): - """ - return the number of seconds in the specified period - - >>> get_period_seconds('day') - 86400 - >>> get_period_seconds(86400) - 86400 - >>> get_period_seconds(datetime.timedelta(hours=24)) - 86400 - >>> get_period_seconds('day + os.system("rm -Rf *")') - Traceback (most recent call last): - ... - ValueError: period not in (second, minute, hour, day, month, year) - """ - if isinstance(period, six.string_types): - try: - name = 'seconds_per_' + period.lower() - result = globals()[name] - except KeyError: - msg = "period not in (second, minute, hour, day, month, year)" - raise ValueError(msg) - elif isinstance(period, numbers.Number): - result = period - elif isinstance(period, datetime.timedelta): - result = period.days * get_period_seconds('day') + period.seconds - else: - raise TypeError('period must be a string or integer') - return result - - -def get_date_format_string(period): - """ - For a given period (e.g. 'month', 'day', or some numeric interval - such as 3600 (in secs)), return the format string that can be - used with strftime to format that time to specify the times - across that interval, but no more detailed. - For example, - - >>> get_date_format_string('month') - '%Y-%m' - >>> get_date_format_string(3600) - '%Y-%m-%d %H' - >>> get_date_format_string('hour') - '%Y-%m-%d %H' - >>> get_date_format_string(None) - Traceback (most recent call last): - ... - TypeError: period must be a string or integer - >>> get_date_format_string('garbage') - Traceback (most recent call last): - ... - ValueError: period not in (second, minute, hour, day, month, year) - """ - # handle the special case of 'month' which doesn't have - # a static interval in seconds - if isinstance(period, six.string_types) and period.lower() == 'month': - return '%Y-%m' - file_period_secs = get_period_seconds(period) - format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S') - seconds_per_second = 1 - intervals = ( - seconds_per_year, - seconds_per_day, - seconds_per_hour, - seconds_per_minute, - seconds_per_second, - ) - mods = list(map(lambda interval: file_period_secs % interval, intervals)) - format_pieces = format_pieces[: mods.index(0) + 1] - return ''.join(format_pieces) - - -def divide_timedelta_float(td, divisor): - """ - Divide a timedelta by a float value - - >>> one_day = datetime.timedelta(days=1) - >>> half_day = datetime.timedelta(days=.5) - >>> divide_timedelta_float(one_day, 2.0) == half_day - True - >>> divide_timedelta_float(one_day, 2) == half_day - True - """ - # td is comprised of days, seconds, microseconds - dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')] - dsm = map(lambda elem: elem / divisor, dsm) - return datetime.timedelta(*dsm) - - -def calculate_prorated_values(): - """ - A utility function to prompt for a rate (a string in units per - unit time), and return that same rate for various time periods. - """ - rate = six.moves.input("Enter the rate (3/hour, 50/month)> ") - res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict() - value = float(res['value']) - value_per_second = value / get_period_seconds(res['period']) - for period in ('minute', 'hour', 'day', 'month', 'year'): - period_value = value_per_second * get_period_seconds(period) - print("per {period}: {period_value}".format(**locals())) - - -def parse_timedelta(str): - """ - Take a string representing a span of time and parse it to a time delta. - Accepts any string of comma-separated numbers each with a unit indicator. - - >>> parse_timedelta('1 day') - datetime.timedelta(days=1) - - >>> parse_timedelta('1 day, 30 seconds') - datetime.timedelta(days=1, seconds=30) - - >>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds') - datetime.timedelta(days=47, seconds=28848, microseconds=15400) - - Supports weeks, months, years - - >>> parse_timedelta('1 week') - datetime.timedelta(days=7) - - >>> parse_timedelta('1 year, 1 month') - datetime.timedelta(days=395, seconds=58685) - - Note that months and years strict intervals, not aligned - to a calendar: - - >>> now = datetime.datetime.now() - >>> later = now + parse_timedelta('1 year') - >>> later.replace(year=now.year) - now - datetime.timedelta(seconds=20940) - """ - deltas = (_parse_timedelta_part(part.strip()) for part in str.split(',')) - return sum(deltas, datetime.timedelta()) - - -def _parse_timedelta_part(part): - match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part) - if not match: - msg = "Unable to parse {part!r} as a time delta".format(**locals()) - raise ValueError(msg) - unit = match.group('unit').lower() - if not unit.endswith('s'): - unit += 's' - value = float(match.group('value')) - if unit == 'months': - unit = 'years' - value = value / 12 - if unit == 'years': - unit = 'days' - value = value * days_per_year - return datetime.timedelta(**{unit: value}) - - -def divide_timedelta(td1, td2): - """ - Get the ratio of two timedeltas - - >>> one_day = datetime.timedelta(days=1) - >>> one_hour = datetime.timedelta(hours=1) - >>> divide_timedelta(one_hour, one_day) == 1 / 24 - True - """ - try: - return td1 / td2 - except TypeError: - # Python 3.2 gets division - # http://bugs.python.org/issue2706 - return td1.total_seconds() / td2.total_seconds() - - -def date_range(start=None, stop=None, step=None): - """ - Much like the built-in function range, but works with dates - - >>> range_items = date_range( - ... datetime.datetime(2005,12,21), - ... datetime.datetime(2005,12,25), - ... ) - >>> my_range = tuple(range_items) - >>> datetime.datetime(2005,12,21) in my_range - True - >>> datetime.datetime(2005,12,22) in my_range - True - >>> datetime.datetime(2005,12,25) in my_range - False - """ - if step is None: - step = datetime.timedelta(days=1) - if start is None: - start = datetime.datetime.now() - while start < stop: - yield start - start += step diff --git a/libraries/tempora/schedule.py b/libraries/tempora/schedule.py deleted file mode 100644 index 1ad093b2..00000000 --- a/libraries/tempora/schedule.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Classes for calling functions a schedule. -""" - -from __future__ import absolute_import - -import datetime -import numbers -import abc -import bisect - -import pytz - -__metaclass__ = type - - -def now(): - """ - Provide the current timezone-aware datetime. - - A client may override this function to change the default behavior, - such as to use local time or timezone-naïve times. - """ - return datetime.datetime.utcnow().replace(tzinfo=pytz.utc) - - -def from_timestamp(ts): - """ - Convert a numeric timestamp to a timezone-aware datetime. - - A client may override this function to change the default behavior, - such as to use local time or timezone-naïve times. - """ - return datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=pytz.utc) - - -class DelayedCommand(datetime.datetime): - """ - A command to be executed after some delay (seconds or timedelta). - """ - - @classmethod - def from_datetime(cls, other): - return cls( - other.year, other.month, other.day, other.hour, - other.minute, other.second, other.microsecond, - other.tzinfo, - ) - - @classmethod - def after(cls, delay, target): - if not isinstance(delay, datetime.timedelta): - delay = datetime.timedelta(seconds=delay) - due_time = now() + delay - cmd = cls.from_datetime(due_time) - cmd.delay = delay - cmd.target = target - return cmd - - @staticmethod - def _from_timestamp(input): - """ - If input is a real number, interpret it as a Unix timestamp - (seconds sinc Epoch in UTC) and return a timezone-aware - datetime object. Otherwise return input unchanged. - """ - if not isinstance(input, numbers.Real): - return input - return from_timestamp(input) - - @classmethod - def at_time(cls, at, target): - """ - Construct a DelayedCommand to come due at `at`, where `at` may be - a datetime or timestamp. - """ - at = cls._from_timestamp(at) - cmd = cls.from_datetime(at) - cmd.delay = at - now() - cmd.target = target - return cmd - - def due(self): - return now() >= self - - -class PeriodicCommand(DelayedCommand): - """ - Like a delayed command, but expect this command to run every delay - seconds. - """ - def _next_time(self): - """ - Add delay to self, localized - """ - return self._localize(self + self.delay) - - @staticmethod - def _localize(dt): - """ - Rely on pytz.localize to ensure new result honors DST. - """ - try: - tz = dt.tzinfo - return tz.localize(dt.replace(tzinfo=None)) - except AttributeError: - return dt - - def next(self): - cmd = self.__class__.from_datetime(self._next_time()) - cmd.delay = self.delay - cmd.target = self.target - return cmd - - def __setattr__(self, key, value): - if key == 'delay' and not value > datetime.timedelta(): - raise ValueError( - "A PeriodicCommand must have a positive, " - "non-zero delay." - ) - super(PeriodicCommand, self).__setattr__(key, value) - - -class PeriodicCommandFixedDelay(PeriodicCommand): - """ - Like a periodic command, but don't calculate the delay based on - the current time. Instead use a fixed delay following the initial - run. - """ - - @classmethod - def at_time(cls, at, delay, target): - at = cls._from_timestamp(at) - cmd = cls.from_datetime(at) - if isinstance(delay, numbers.Number): - delay = datetime.timedelta(seconds=delay) - cmd.delay = delay - cmd.target = target - return cmd - - @classmethod - def daily_at(cls, at, target): - """ - Schedule a command to run at a specific time each day. - """ - daily = datetime.timedelta(days=1) - # convert when to the next datetime matching this time - when = datetime.datetime.combine(datetime.date.today(), at) - if when < now(): - when += daily - return cls.at_time(cls._localize(when), daily, target) - - -class Scheduler: - """ - A rudimentary abstract scheduler accepting DelayedCommands - and dispatching them on schedule. - """ - def __init__(self): - self.queue = [] - - def add(self, command): - assert isinstance(command, DelayedCommand) - bisect.insort(self.queue, command) - - def run_pending(self): - while self.queue: - command = self.queue[0] - if not command.due(): - break - self.run(command) - if isinstance(command, PeriodicCommand): - self.add(command.next()) - del self.queue[0] - - @abc.abstractmethod - def run(self, command): - """ - Run the command - """ - - -class InvokeScheduler(Scheduler): - """ - Command targets are functions to be invoked on schedule. - """ - def run(self, command): - command.target() - - -class CallbackScheduler(Scheduler): - """ - Command targets are passed to a dispatch callable on schedule. - """ - def __init__(self, dispatch): - super(CallbackScheduler, self).__init__() - self.dispatch = dispatch - - def run(self, command): - self.dispatch(command.target) diff --git a/libraries/tempora/tests/test_schedule.py b/libraries/tempora/tests/test_schedule.py deleted file mode 100644 index 38eb8dc9..00000000 --- a/libraries/tempora/tests/test_schedule.py +++ /dev/null @@ -1,118 +0,0 @@ -import time -import random -import datetime - -import pytest -import pytz -import freezegun - -from tempora import schedule - -__metaclass__ = type - - -@pytest.fixture -def naive_times(monkeypatch): - monkeypatch.setattr( - 'irc.schedule.from_timestamp', - datetime.datetime.fromtimestamp) - monkeypatch.setattr('irc.schedule.now', datetime.datetime.now) - - -do_nothing = type(None) -try: - do_nothing() -except TypeError: - # Python 2 compat - def do_nothing(): - return None - - -def test_delayed_command_order(): - """ - delayed commands should be sorted by delay time - """ - delays = [random.randint(0, 99) for x in range(5)] - cmds = sorted([ - schedule.DelayedCommand.after(delay, do_nothing) - for delay in delays - ]) - assert [c.delay.seconds for c in cmds] == sorted(delays) - - -def test_periodic_command_delay(): - "A PeriodicCommand must have a positive, non-zero delay." - with pytest.raises(ValueError) as exc_info: - schedule.PeriodicCommand.after(0, None) - assert str(exc_info.value) == test_periodic_command_delay.__doc__ - - -def test_periodic_command_fixed_delay(): - """ - Test that we can construct a periodic command with a fixed initial - delay. - """ - fd = schedule.PeriodicCommandFixedDelay.at_time( - at=schedule.now(), - delay=datetime.timedelta(seconds=2), - target=lambda: None, - ) - assert fd.due() is True - assert fd.next().due() is False - - -class TestCommands: - def test_delayed_command_from_timestamp(self): - """ - Ensure a delayed command can be constructed from a timestamp. - """ - t = time.time() - schedule.DelayedCommand.at_time(t, do_nothing) - - def test_command_at_noon(self): - """ - Create a periodic command that's run at noon every day. - """ - when = datetime.time(12, 0, tzinfo=pytz.utc) - cmd = schedule.PeriodicCommandFixedDelay.daily_at(when, target=None) - assert cmd.due() is False - next_cmd = cmd.next() - daily = datetime.timedelta(days=1) - day_from_now = schedule.now() + daily - two_days_from_now = day_from_now + daily - assert day_from_now < next_cmd < two_days_from_now - - -class TestTimezones: - def test_alternate_timezone_west(self): - target_tz = pytz.timezone('US/Pacific') - target = schedule.now().astimezone(target_tz) - cmd = schedule.DelayedCommand.at_time(target, target=None) - assert cmd.due() - - def test_alternate_timezone_east(self): - target_tz = pytz.timezone('Europe/Amsterdam') - target = schedule.now().astimezone(target_tz) - cmd = schedule.DelayedCommand.at_time(target, target=None) - assert cmd.due() - - def test_daylight_savings(self): - """ - A command at 9am should always be 9am regardless of - a DST boundary. - """ - with freezegun.freeze_time('2018-03-10 08:00:00'): - target_tz = pytz.timezone('US/Eastern') - target_time = datetime.time(9, tzinfo=target_tz) - cmd = schedule.PeriodicCommandFixedDelay.daily_at( - target_time, - target=lambda: None, - ) - - def naive(dt): - return dt.replace(tzinfo=None) - - assert naive(cmd) == datetime.datetime(2018, 3, 10, 9, 0, 0) - next_ = cmd.next() - assert naive(next_) == datetime.datetime(2018, 3, 11, 9, 0, 0) - assert next_ - cmd == datetime.timedelta(hours=23) diff --git a/libraries/tempora/timing.py b/libraries/tempora/timing.py deleted file mode 100644 index 03c22454..00000000 --- a/libraries/tempora/timing.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals, absolute_import - -import datetime -import functools -import numbers -import time - -__metaclass__ = type - - -class Stopwatch: - """ - A simple stopwatch which starts automatically. - - >>> w = Stopwatch() - >>> _1_sec = datetime.timedelta(seconds=1) - >>> w.split() < _1_sec - True - >>> import time - >>> time.sleep(1.0) - >>> w.split() >= _1_sec - True - >>> w.stop() >= _1_sec - True - >>> w.reset() - >>> w.start() - >>> w.split() < _1_sec - True - - It should be possible to launch the Stopwatch in a context: - - >>> with Stopwatch() as watch: - ... assert isinstance(watch.split(), datetime.timedelta) - - In that case, the watch is stopped when the context is exited, - so to read the elapsed time:: - - >>> watch.elapsed - datetime.timedelta(...) - >>> watch.elapsed.seconds - 0 - """ - def __init__(self): - self.reset() - self.start() - - def reset(self): - self.elapsed = datetime.timedelta(0) - if hasattr(self, 'start_time'): - del self.start_time - - def start(self): - self.start_time = datetime.datetime.utcnow() - - def stop(self): - stop_time = datetime.datetime.utcnow() - self.elapsed += stop_time - self.start_time - del self.start_time - return self.elapsed - - def split(self): - local_duration = datetime.datetime.utcnow() - self.start_time - return self.elapsed + local_duration - - # context manager support - def __enter__(self): - self.start() - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.stop() - - -class IntervalGovernor: - """ - Decorate a function to only allow it to be called once per - min_interval. Otherwise, it returns None. - """ - def __init__(self, min_interval): - if isinstance(min_interval, numbers.Number): - min_interval = datetime.timedelta(seconds=min_interval) - self.min_interval = min_interval - self.last_call = None - - def decorate(self, func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - allow = ( - not self.last_call - or self.last_call.split() > self.min_interval - ) - if allow: - self.last_call = Stopwatch() - return func(*args, **kwargs) - return wrapper - - __call__ = decorate - - -class Timer(Stopwatch): - """ - Watch for a target elapsed time. - - >>> t = Timer(0.1) - >>> t.expired() - False - >>> __import__('time').sleep(0.15) - >>> t.expired() - True - """ - def __init__(self, target=float('Inf')): - self.target = self._accept(target) - super(Timer, self).__init__() - - def _accept(self, target): - "Accept None or ∞ or datetime or numeric for target" - if isinstance(target, datetime.timedelta): - target = target.total_seconds() - - if target is None: - # treat None as infinite target - target = float('Inf') - - return target - - def expired(self): - return self.split().total_seconds() > self.target - - -class BackoffDelay: - """ - Exponential backoff delay. - - Useful for defining delays between retries. Consider for use - with ``jaraco.functools.retry_call`` as the cleanup. - - Default behavior has no effect; a delay or jitter must - be supplied for the call to be non-degenerate. - - >>> bd = BackoffDelay() - >>> bd() - >>> bd() - - The following instance will delay 10ms for the first call, - 20ms for the second, etc. - - >>> bd = BackoffDelay(delay=0.01, factor=2) - >>> bd() - >>> bd() - - Inspect and adjust the state of the delay anytime. - - >>> bd.delay - 0.04 - >>> bd.delay = 0.01 - - Set limit to prevent the delay from exceeding bounds. - - >>> bd = BackoffDelay(delay=0.01, factor=2, limit=0.015) - >>> bd() - >>> bd.delay - 0.015 - - Limit may be a callable taking a number and returning - the limited number. - - >>> at_least_one = lambda n: max(n, 1) - >>> bd = BackoffDelay(delay=0.01, factor=2, limit=at_least_one) - >>> bd() - >>> bd.delay - 1 - - Pass a jitter to add or subtract seconds to the delay. - - >>> bd = BackoffDelay(jitter=0.01) - >>> bd() - >>> bd.delay - 0.01 - - Jitter may be a callable. To supply a non-deterministic jitter - between -0.5 and 0.5, consider: - - >>> import random - >>> jitter=functools.partial(random.uniform, -0.5, 0.5) - >>> bd = BackoffDelay(jitter=jitter) - >>> bd() - >>> 0 <= bd.delay <= 0.5 - True - """ - - delay = 0 - - factor = 1 - "Multiplier applied to delay" - - jitter = 0 - "Number or callable returning extra seconds to add to delay" - - def __init__(self, delay=0, factor=1, limit=float('inf'), jitter=0): - self.delay = delay - self.factor = factor - if isinstance(limit, numbers.Number): - limit_ = limit - - def limit(n): - return max(0, min(limit_, n)) - self.limit = limit - if isinstance(jitter, numbers.Number): - jitter_ = jitter - - def jitter(): - return jitter_ - self.jitter = jitter - - def __call__(self): - time.sleep(self.delay) - self.delay = self.limit(self.delay * self.factor + self.jitter()) diff --git a/libraries/tempora/utc.py b/libraries/tempora/utc.py deleted file mode 100644 index 35bfdb06..00000000 --- a/libraries/tempora/utc.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Facilities for common time operations in UTC. - -Inspired by the `utc project <https://pypi.org/project/utc>`_. - ->>> dt = now() ->>> dt == fromtimestamp(dt.timestamp()) -True ->>> dt.tzinfo -datetime.timezone.utc - ->>> from time import time as timestamp ->>> now().timestamp() - timestamp() < 0.1 -True - ->>> datetime(2018, 6, 26, 0).tzinfo -datetime.timezone.utc - ->>> time(0, 0).tzinfo -datetime.timezone.utc -""" - -import datetime as std -import functools - - -__all__ = ['now', 'fromtimestamp', 'datetime', 'time'] - - -now = functools.partial(std.datetime.now, std.timezone.utc) -fromtimestamp = functools.partial( - std.datetime.fromtimestamp, - tz=std.timezone.utc, -) -datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc) -time = functools.partial(std.time, tzinfo=std.timezone.utc) diff --git a/libraries/zc/__init__.py b/libraries/zc/__init__.py deleted file mode 100644 index 146c3362..00000000 --- a/libraries/zc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__namespace__ = 'zc' \ No newline at end of file diff --git a/libraries/zc/lockfile/README.txt b/libraries/zc/lockfile/README.txt deleted file mode 100644 index 89ef33e9..00000000 --- a/libraries/zc/lockfile/README.txt +++ /dev/null @@ -1,70 +0,0 @@ -Lock file support -================= - -The ZODB lock_file module provides support for creating file system -locks. These are locks that are implemented with lock files and -OS-provided locking facilities. To create a lock, instantiate a -LockFile object with a file name: - - >>> import zc.lockfile - >>> lock = zc.lockfile.LockFile('lock') - -If we try to lock the same name, we'll get a lock error: - - >>> import zope.testing.loggingsupport - >>> handler = zope.testing.loggingsupport.InstalledHandler('zc.lockfile') - >>> try: - ... zc.lockfile.LockFile('lock') - ... except zc.lockfile.LockError: - ... print("Can't lock file") - Can't lock file - -.. We don't log failure to acquire. - - >>> for record in handler.records: # doctest: +ELLIPSIS - ... print(record.levelname+' '+record.getMessage()) - -To release the lock, use it's close method: - - >>> lock.close() - -The lock file is not removed. It is left behind: - - >>> import os - >>> os.path.exists('lock') - True - -Of course, now that we've released the lock, we can create it again: - - >>> lock = zc.lockfile.LockFile('lock') - >>> lock.close() - -.. Cleanup - - >>> import os - >>> os.remove('lock') - -Hostname in lock file -===================== - -In a container environment (e.g. Docker), the PID is typically always -identical even if multiple containers are running under the same operating -system instance. - -Clearly, inspecting lock files doesn't then help much in debugging. To identify -the container which created the lock file, we need information about the -container in the lock file. Since Docker uses the container identifier or name -as the hostname, this information can be stored in the lock file in addition to -or instead of the PID. - -Use the ``content_template`` keyword argument to ``LockFile`` to specify a -custom lock file content format: - - >>> lock = zc.lockfile.LockFile('lock', content_template='{pid};{hostname}') - >>> lock.close() - -If you now inspected the lock file, you would see e.g.: - - $ cat lock - 123;myhostname - diff --git a/libraries/zc/lockfile/__init__.py b/libraries/zc/lockfile/__init__.py deleted file mode 100644 index a0ac2ff1..00000000 --- a/libraries/zc/lockfile/__init__.py +++ /dev/null @@ -1,104 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## - -import os -import errno -import logging -logger = logging.getLogger("zc.lockfile") - -class LockError(Exception): - """Couldn't get a lock - """ - -try: - import fcntl -except ImportError: - try: - import msvcrt - except ImportError: - def _lock_file(file): - raise TypeError('No file-locking support on this platform') - def _unlock_file(file): - raise TypeError('No file-locking support on this platform') - - else: - # Windows - def _lock_file(file): - # Lock just the first byte - try: - msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1) - except IOError: - raise LockError("Couldn't lock %r" % file.name) - - def _unlock_file(file): - try: - file.seek(0) - msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1) - except IOError: - raise LockError("Couldn't unlock %r" % file.name) - -else: - # Unix - _flags = fcntl.LOCK_EX | fcntl.LOCK_NB - - def _lock_file(file): - try: - fcntl.flock(file.fileno(), _flags) - except IOError: - raise LockError("Couldn't lock %r" % file.name) - - def _unlock_file(file): - fcntl.flock(file.fileno(), fcntl.LOCK_UN) - -class LazyHostName(object): - """Avoid importing socket and calling gethostname() unnecessarily""" - def __str__(self): - import socket - return socket.gethostname() - - -class LockFile: - - _fp = None - - def __init__(self, path, content_template='{pid}'): - self._path = path - try: - # Try to open for writing without truncation: - fp = open(path, 'r+') - except IOError: - # If the file doesn't exist, we'll get an IO error, try a+ - # Note that there may be a race here. Multiple processes - # could fail on the r+ open and open the file a+, but only - # one will get the the lock and write a pid. - fp = open(path, 'a+') - - try: - _lock_file(fp) - except: - fp.close() - raise - - # We got the lock, record info in the file. - self._fp = fp - fp.write(" %s\n" % content_template.format(pid=os.getpid(), - hostname=LazyHostName())) - fp.truncate() - fp.flush() - - def close(self): - if self._fp is not None: - _unlock_file(self._fp) - self._fp.close() - self._fp = None diff --git a/libraries/zc/lockfile/tests.py b/libraries/zc/lockfile/tests.py deleted file mode 100644 index e9fcbff3..00000000 --- a/libraries/zc/lockfile/tests.py +++ /dev/null @@ -1,193 +0,0 @@ -############################################################################## -# -# Copyright (c) 2004 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -import os, re, sys, unittest, doctest -import zc.lockfile, time, threading -from zope.testing import renormalizing, setupstack -import tempfile -try: - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - -checker = renormalizing.RENormalizing([ - # Python 3 adds module path to error class name. - (re.compile("zc\.lockfile\.LockError:"), - r"LockError:"), - ]) - -def inc(): - while 1: - try: - lock = zc.lockfile.LockFile('f.lock') - except zc.lockfile.LockError: - continue - else: - break - f = open('f', 'r+b') - v = int(f.readline().strip()) - time.sleep(0.01) - v += 1 - f.seek(0) - f.write(('%d\n' % v).encode('ASCII')) - f.close() - lock.close() - -def many_threads_read_and_write(): - r""" - >>> with open('f', 'w+b') as file: - ... _ = file.write(b'0\n') - >>> with open('f.lock', 'w+b') as file: - ... _ = file.write(b'0\n') - - >>> n = 50 - >>> threads = [threading.Thread(target=inc) for i in range(n)] - >>> _ = [thread.start() for thread in threads] - >>> _ = [thread.join() for thread in threads] - >>> with open('f', 'rb') as file: - ... saved = int(file.read().strip()) - >>> saved == n - True - - >>> os.remove('f') - - We should only have one pid in the lock file: - - >>> f = open('f.lock') - >>> len(f.read().strip().split()) - 1 - >>> f.close() - - >>> os.remove('f.lock') - - """ - -def pid_in_lockfile(): - r""" - >>> import os, zc.lockfile - >>> pid = os.getpid() - >>> lock = zc.lockfile.LockFile("f.lock") - >>> f = open("f.lock") - >>> _ = f.seek(1) - >>> f.read().strip() == str(pid) - True - >>> f.close() - - Make sure that locking twice does not overwrite the old pid: - - >>> lock = zc.lockfile.LockFile("f.lock") - Traceback (most recent call last): - ... - LockError: Couldn't lock 'f.lock' - - >>> f = open("f.lock") - >>> _ = f.seek(1) - >>> f.read().strip() == str(pid) - True - >>> f.close() - - >>> lock.close() - """ - - -def hostname_in_lockfile(): - r""" - hostname is correctly written into the lock file when it's included in the - lock file content template - - >>> import zc.lockfile - >>> with patch('socket.gethostname', Mock(return_value='myhostname')): - ... lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}') - >>> f = open("f.lock") - >>> _ = f.seek(1) - >>> f.read().rstrip() - 'myhostname' - >>> f.close() - - Make sure that locking twice does not overwrite the old hostname: - - >>> lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}') - Traceback (most recent call last): - ... - LockError: Couldn't lock 'f.lock' - - >>> f = open("f.lock") - >>> _ = f.seek(1) - >>> f.read().rstrip() - 'myhostname' - >>> f.close() - - >>> lock.close() - """ - - -class TestLogger(object): - def __init__(self): - self.log_entries = [] - - def exception(self, msg, *args): - self.log_entries.append((msg,) + args) - - -class LockFileLogEntryTestCase(unittest.TestCase): - """Tests for logging in case of lock failure""" - def setUp(self): - self.here = os.getcwd() - self.tmp = tempfile.mkdtemp(prefix='zc.lockfile-test-') - os.chdir(self.tmp) - - def tearDown(self): - os.chdir(self.here) - setupstack.rmtree(self.tmp) - - def test_log_formatting(self): - # PID and hostname are parsed and logged from lock file on failure - with patch('os.getpid', Mock(return_value=123)): - with patch('socket.gethostname', Mock(return_value='myhostname')): - lock = zc.lockfile.LockFile('f.lock', - content_template='{pid}/{hostname}') - with open('f.lock') as f: - self.assertEqual(' 123/myhostname\n', f.read()) - - lock.close() - - def test_unlock_and_lock_while_multiprocessing_process_running(self): - import multiprocessing - - lock = zc.lockfile.LockFile('l') - q = multiprocessing.Queue() - p = multiprocessing.Process(target=q.get) - p.daemon = True - p.start() - - # release and re-acquire should work (obviously) - lock.close() - lock = zc.lockfile.LockFile('l') - self.assertTrue(p.is_alive()) - - q.put(0) - lock.close() - p.join() - - -def test_suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocFileSuite( - 'README.txt', checker=checker, - setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown)) - suite.addTest(doctest.DocTestSuite( - setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown, - checker=checker)) - # Add unittest test cases from this module - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(__name__)) - return suite diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 7f692846..512b844c 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -2,13 +2,13 @@ ################################################################################################# +import BaseHTTPServer import logging +import httplib import threading +import urlparse import xbmc -import xbmcvfs - -import cherrypy ################################################################################################# @@ -17,60 +17,125 @@ LOG = logging.getLogger("EMBY."+__name__) ################################################################################################# - -class Root(object): - - @cherrypy.expose - def default(self, *args, **kwargs): - - try: - if not kwargs.get('Id').isdigit(): - raise IndexError("Incorrect Id format: %s" % kwargs.get('Id')) - - LOG.info("Webservice called with params: %s", kwargs) - - return ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" - % (kwargs.get('Id'), kwargs.get('KodiId'), kwargs.get('Name'), kwargs.get('transcode') or False)) - - except IndexError as error: - LOG.error(error) - - raise cherrypy.HTTPError(404, error) - - except Exception as error: - LOG.exception(error) - - raise cherrypy.HTTPError(500, "Exception occurred: %s" % error) - class WebService(threading.Thread): - root = None + ''' Run a webservice to trigger playback. + ''' + stop_thread = False def __init__(self): - - self.root = Root() - cherrypy.config.update({ - 'engine.autoreload.on' : False, - 'log.screen': False, - 'engine.timeout_monitor.frequency': 5, - 'server.shutdown_timeout': 1, - }) threading.Thread.__init__(self) - def run(self): - - LOG.info("--->[ webservice/%s ]", PORT) - conf = { - 'global': { - 'server.socket_host': '0.0.0.0', - 'server.socket_port': PORT - }, '/': {} - } - cherrypy.quickstart(self.root, '/', conf) - def stop(self): - cherrypy.engine.exit() - self.join(0) + ''' Called when the thread needs to stop + ''' + try: + conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT) + conn.request("QUIT", "/") + conn.getresponse() + self.stop_thread = True + except Exception as error: + pass + + def run(self): + + ''' Called to start the webservice. + ''' + LOG.info("--->[ webservice/%s ]", PORT) + + try: + server = HttpServer(('127.0.0.1', PORT), requestHandler) + server.serve_forever() + except Exception as error: + + if '10053' not in error: # ignore host diconnected errors + LOG.exception(error) + + LOG.info("---<[ webservice ]") + + +class HttpServer(BaseHTTPServer.HTTPServer): + + ''' Http server that reacts to self.stop flag. + ''' + def serve_forever(self): + + ''' Handle one request at a time until stopped. + ''' + self.stop = False + + while not self.stop: + self.handle_request() + + +class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + + #Handler for the GET requests + + def log_message(self, format, *args): + + ''' Mute the webservice requests. + ''' + pass + + def get_params(self): + + ''' Get the params + ''' + try: + path = self.path[1:] + + if '?' in path: + path = path.split('?', 1)[1] + + params = dict(urlparse.parse_qsl(path)) + except Exception: + params = {} + + return params + + def do_HEAD(self): + + ''' Called on HEAD requests + ''' + self.send_response(200) + self.end_headers() + + return + + def do_GET(self): + + ''' Return plugin path + ''' + try: + params = self.get_params() + + if not params: + raise IndexError("Incomplete URL format") + + if not params.get('Id').isdigit(): + raise IndexError("Incorrect Id format %s" % params.get('Id')) + + xbmc.log("[ webservice ] path: %s params: %s" % (str(self.path), str(params)), xbmc.LOGWARNING) + + path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" + % (params.get('Id'), params.get('KodiId'), params.get('Name'), params.get('transcode') or False)) + + self.send_response(200) + self.send_header('Content-type','text/html') + self.end_headers() + self.wfile.write(path) + + except IndexError as error: + + xbmc.log(str(error), xbmc.LOGWARNING) + self.send_error(404, "Exception occurred: %s" % error) + + except Exception as error: + + xbmc.log(str(error), xbmc.LOGWARNING) + self.send_error(500, "Exception occurred: %s" % error) + + return - del self.root From c9d51690dfb166d7c3216f45335872ebb045616e Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Thu, 31 Jan 2019 07:25:21 -0600 Subject: [PATCH 09/12] Fix Kodi hangup on exit --- resources/lib/webservice.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 512b844c..9e5f9d33 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -21,8 +21,6 @@ class WebService(threading.Thread): ''' Run a webservice to trigger playback. ''' - stop_thread = False - def __init__(self): threading.Thread.__init__(self) @@ -34,7 +32,6 @@ class WebService(threading.Thread): conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT) conn.request("QUIT", "/") conn.getresponse() - self.stop_thread = True except Exception as error: pass @@ -71,7 +68,9 @@ class HttpServer(BaseHTTPServer.HTTPServer): class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - #Handler for the GET requests + ''' Http request handler. Do not use LOG here, + it will hang requests in Kodi > show information dialog. + ''' def log_message(self, format, *args): @@ -79,6 +78,14 @@ class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ''' pass + def do_QUIT(self): + + ''' send 200 OK response, and set server.stop to True + ''' + self.send_response(200) + self.end_headers() + self.server.stop = True + def get_params(self): ''' Get the params From 4ebcb05ed5a56bb48af9ef931736b2f0fb34190e Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Fri, 1 Feb 2019 03:07:47 -0600 Subject: [PATCH 10/12] Update local krypton --- resources/lib/objects/__init__.py | 2 +- resources/lib/objects/kodi/queries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/objects/__init__.py b/resources/lib/objects/__init__.py index 55eb734d..6695119b 100644 --- a/resources/lib/objects/__init__.py +++ b/resources/lib/objects/__init__.py @@ -1,4 +1,4 @@ -version = "171076029" +version = "171076030" from movies import Movies from musicvideos import MusicVideos diff --git a/resources/lib/objects/kodi/queries.py b/resources/lib/objects/kodi/queries.py index c823bf93..d2c37110 100644 --- a/resources/lib/objects/kodi/queries.py +++ b/resources/lib/objects/kodi/queries.py @@ -355,7 +355,7 @@ update_path_tvshow_obj = [ "{Path}",None,None,1,"{PathId}" ] update_path_episode_obj = [ "{Path}",None,None,1,"{PathId}" ] -update_path_mvideo_obj = [ "{Path}","musicvideos","metadata.local",1,"{PathId}" +update_path_mvideo_obj = [ "{Path}","musicvideos",None,1,"{PathId}" ] update_file = """ UPDATE files SET idPath = ?, strFilename = ?, dateAdded = ? From e966b613b049d82a17fe194cabc468494509c1d9 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Fri, 1 Feb 2019 03:09:21 -0600 Subject: [PATCH 11/12] Ensure userid is set for token --- resources/lib/emby/core/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/emby/core/http.py b/resources/lib/emby/core/http.py index e7d585ea..8b8a6f83 100644 --- a/resources/lib/emby/core/http.py +++ b/resources/lib/emby/core/http.py @@ -222,7 +222,7 @@ class HTTP(object): data['headers'].update({'Authorization': auth}) - if self.config['auth.token']: + if self.config['auth.token'] and self.config['auth.user_id']: auth += ', UserId=%s' % self.config['auth.user_id'].encode('utf-8') data['headers'].update({'Authorization': auth, 'X-MediaBrowser-Token': self.config['auth.token'].encode('utf-8')}) From 021d29fc24e31c1eac70ca34928f01ce91ab6c01 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Fri, 1 Feb 2019 03:14:14 -0600 Subject: [PATCH 12/12] Version bump 4.0.2 --- addon.xml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/addon.xml b/addon.xml index e3789fe0..2474e0c5 100644 --- a/addon.xml +++ b/addon.xml @@ -1,13 +1,13 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <addon id="plugin.video.emby" name="Emby" - version="4.0.1" + version="4.0.2" provider-name="angelblue05"> <requires> <import addon="xbmc.python" version="2.25.0"/> - <import addon="plugin.video.emby.movies" version="0.13" /> - <import addon="plugin.video.emby.tvshows" version="0.13" /> - <import addon="plugin.video.emby.musicvideos" version="0.13" /> + <import addon="plugin.video.emby.movies" version="0.14" /> + <import addon="plugin.video.emby.tvshows" version="0.14" /> + <import addon="plugin.video.emby.musicvideos" version="0.14" /> </requires> <extension point="xbmc.python.pluginsource" library="default.py"> @@ -38,8 +38,11 @@ <description lang="en">Welcome to Emby for Kodi A whole new way to manage and view your media library. The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server. Emby for Kodi is the absolute best way to enjoy the incredible Kodi playback engine combined with the power of Emby's centralized database. Features: Direct integration with the Kodi library for native Kodi speed Instant synchronization with the Emby server Full support for Movie, TV and Music collections Emby Server direct stream and transcoding support - use Kodi when you are away from home!</description> <news> New stable release - The wiki has been updated: https://github.com/MediaBrowser/plugin.video.emby/wiki - Small fixes + The wiki has been updated, PLEASE READ: https://github.com/MediaBrowser/plugin.video.emby/wiki + Fix playback for Kodi Leia + Fix masterlock + Home videos and pictures now show under videos and picture add-ons + Dependencies were updated to 0.14! </news> </extension> </addon>