From dc1f15a62144974f06127c5dfe32b58eeb528efc Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:06:26 -0700 Subject: Drop Python 2.7 support officially --- .github/workflows/ci-tests.yml | 8 +------- CHANGES.txt | 5 +++++ README.rst | 13 +++++++------ pyproject.toml | 2 +- setup.cfg | 4 +--- tox.ini | 3 +-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 54b229e..c8c660e 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -15,7 +15,6 @@ jobs: strategy: matrix: py: - - "2.7" - "3.5" - "3.6" - "3.7" @@ -56,11 +55,6 @@ jobs: name: Validate coverage steps: - uses: actions/checkout@v2 - - name: Setup python 2.7 - uses: actions/setup-python@v2 - with: - python-version: 2.7 - architecture: x64 - name: Setup python 3.8 uses: actions/setup-python@v2 with: @@ -68,7 +62,7 @@ jobs: architecture: x64 - run: pip install tox - - run: tox -e py38,py27,coverage + - run: tox -e py38,coverage docs: runs-on: ubuntu-latest name: Build the documentation diff --git a/CHANGES.txt b/CHANGES.txt index 5550995..f4d1acc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +2.0.0 (unreleased) +------------------ + +- Drop Python 2.7 support + 1.4.4 (2020-06-01) ------------------ diff --git a/README.rst b/README.rst index f9bf1a8..b85e49d 100644 --- a/README.rst +++ b/README.rst @@ -16,10 +16,11 @@ Waitress :target: https://webchat.freenode.net/?channels=pyramid :alt: IRC Freenode -Waitress is meant to be a production-quality pure-Python WSGI server with very -acceptable performance. It has no dependencies except ones which live in the -Python standard library. It runs on CPython on Unix and Windows under Python -2.7+ and Python 3.5+. It is also known to run on PyPy 1.6.0+ on UNIX. It -supports HTTP/1.0 and HTTP/1.1. +Waitress is a production-quality pure-Python WSGI server with very acceptable +performance. It has no dependencies except ones which live in the Python +standard library. It runs on CPython on Unix and Windows under Python 3.5+. It +is also known to run on PyPy (version 3.5 compatible) on UNIX. It supports +HTTP/1.0 and HTTP/1.1. -For more information, see the "docs" directory of the Waitress package or visit https://docs.pylonsproject.org/projects/waitress/en/latest/ +For more information, see the "docs" directory of the Waitress package or visit +https://docs.pylonsproject.org/projects/waitress/en/latest/ diff --git a/pyproject.toml b/pyproject.toml index 7f50ece..1bc058b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools >= 41"] build-backend = "setuptools.build_meta" [tool.black] -py36 = false +target-version = ['py35', 'py36', 'py37', 'py38'] exclude = ''' /( \.git diff --git a/setup.cfg b/setup.cfg index a10b6a1..d7c1c73 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,8 +12,6 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: Zope Public License Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 @@ -39,7 +37,7 @@ maintainer_email = pylons-discuss@googlegroups.com package_dir= =src packages=find: -python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* +python_requires = >=3.5.0 [options.entry_points] paste.server_runner = diff --git a/tox.ini b/tox.ini index 6ca71ad..2040825 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = lint, - py27,pypy, py35,py36,py37,py38,pypy3, docs, coverage @@ -26,7 +25,7 @@ deps = coverage setenv = COVERAGE_FILE=.coverage -depends = py27, py38 +depends = py38 [testenv:lint] skip_install = True -- cgit v1.2.1 From 68384558429978a6cec0645d49121d7fd53511bd Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:54:02 -0700 Subject: Remove object from class definition --- src/waitress/adjustments.py | 4 ++-- src/waitress/buffers.py | 4 ++-- src/waitress/channel.py | 2 +- src/waitress/parser.py | 2 +- src/waitress/receiver.py | 4 ++-- src/waitress/server.py | 4 ++-- src/waitress/task.py | 4 ++-- src/waitress/trigger.py | 2 +- src/waitress/utilities.py | 2 +- tests/fixtureapps/filewrapper.py | 2 +- tests/test_buffers.py | 8 ++++---- tests/test_channel.py | 26 +++++++++++++------------- tests/test_functional.py | 20 ++++++++++---------- tests/test_init.py | 4 ++-- tests/test_parser.py | 2 +- tests/test_proxy_headers.py | 6 +++--- tests/test_receiver.py | 2 +- tests/test_server.py | 12 ++++++------ tests/test_task.py | 12 ++++++------ tests/test_wasyncore.py | 12 ++++++------ 20 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index 93439ea..fb3233d 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -100,11 +100,11 @@ class _int_marker(int): pass -class _bool_marker(object): +class _bool_marker: pass -class Adjustments(object): +class Adjustments: """This class contains tunable parameters. """ diff --git a/src/waitress/buffers.py b/src/waitress/buffers.py index 04f6b42..0086fe8 100644 --- a/src/waitress/buffers.py +++ b/src/waitress/buffers.py @@ -22,7 +22,7 @@ COPY_BYTES = 1 << 18 # 256K STRBUF_LIMIT = 8192 -class FileBasedBuffer(object): +class FileBasedBuffer: remain = 0 @@ -187,7 +187,7 @@ class ReadOnlyFileBasedBuffer(FileBasedBuffer): raise NotImplementedError -class OverflowableBuffer(object): +class OverflowableBuffer: """ This buffer implementation has four stages: - No data diff --git a/src/waitress/channel.py b/src/waitress/channel.py index bc9a2bb..dfb36bb 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -37,7 +37,7 @@ class ClientDisconnected(Exception): """ Raised when attempting to write to a closed socket.""" -class HTTPChannel(wasyncore.dispatcher, object): +class HTTPChannel(wasyncore.dispatcher): """ Setting self.requests = [somerequest] prevents more requests from being received until the out buffers have been flushed. diff --git a/src/waitress/parser.py b/src/waitress/parser.py index 765fe59..c2789eb 100644 --- a/src/waitress/parser.py +++ b/src/waitress/parser.py @@ -40,7 +40,7 @@ class TransferEncodingNotImplemented(Exception): pass -class HTTPRequestParser(object): +class HTTPRequestParser: """A structure that collects the HTTP request. Once the stream is completed, the instance is passed to diff --git a/src/waitress/receiver.py b/src/waitress/receiver.py index 5d1568d..8785280 100644 --- a/src/waitress/receiver.py +++ b/src/waitress/receiver.py @@ -17,7 +17,7 @@ from waitress.utilities import BadRequest, find_double_newline -class FixedStreamReceiver(object): +class FixedStreamReceiver: # See IStreamConsumer completed = False @@ -59,7 +59,7 @@ class FixedStreamReceiver(object): return self.buf -class ChunkedReceiver(object): +class ChunkedReceiver: chunk_remainder = 0 validate_chunk_end = False diff --git a/src/waitress/server.py b/src/waitress/server.py index ae56699..02a6668 100644 --- a/src/waitress/server.py +++ b/src/waitress/server.py @@ -137,7 +137,7 @@ def create_server( # This class is only ever used if we have multiple listen sockets. It allows # the serve() API to call .run() which starts the wasyncore loop, and catches # SystemExit/KeyboardInterrupt so that it can atempt to cleanly shut down. -class MultiSocketServer(object): +class MultiSocketServer: asyncore = wasyncore # test shim def __init__( @@ -172,7 +172,7 @@ class MultiSocketServer(object): wasyncore.close_all(self.map) -class BaseWSGIServer(wasyncore.dispatcher, object): +class BaseWSGIServer(wasyncore.dispatcher): channel_class = HTTPChannel next_channel_cleanup = 0 diff --git a/src/waitress/task.py b/src/waitress/task.py index 6350e34..6ab6bfe 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -41,7 +41,7 @@ hop_by_hop = frozenset( ) -class ThreadedTaskDispatcher(object): +class ThreadedTaskDispatcher: """A Task Dispatcher that creates a thread for each task. """ @@ -141,7 +141,7 @@ class ThreadedTaskDispatcher(object): return False -class Task(object): +class Task: close_on_finish = False status = "200 OK" wrote_header = False diff --git a/src/waitress/trigger.py b/src/waitress/trigger.py index 6a57c12..10ee185 100644 --- a/src/waitress/trigger.py +++ b/src/waitress/trigger.py @@ -50,7 +50,7 @@ from . import wasyncore # the main thread is trying to remove some] -class _triggerbase(object): +class _triggerbase: """OS-independent base class for OS-dependent trigger class.""" kind = None # subclass must set to "pipe" or "loopback"; used by repr diff --git a/src/waitress/utilities.py b/src/waitress/utilities.py index 556bed2..3caaa33 100644 --- a/src/waitress/utilities.py +++ b/src/waitress/utilities.py @@ -273,7 +273,7 @@ def cleanup_unix_socket(path): pass -class Error(object): +class Error: code = 500 reason = "Internal Server Error" diff --git a/tests/fixtureapps/filewrapper.py b/tests/fixtureapps/filewrapper.py index 63df5a6..40b7685 100644 --- a/tests/fixtureapps/filewrapper.py +++ b/tests/fixtureapps/filewrapper.py @@ -5,7 +5,7 @@ here = os.path.dirname(os.path.abspath(__file__)) fn = os.path.join(here, "groundhog1.jpg") -class KindaFilelike(object): # pragma: no cover +class KindaFilelike: # pragma: no cover def __init__(self, bytes): self.bytes = bytes diff --git a/tests/test_buffers.py b/tests/test_buffers.py index a1330ac..029acfc 100644 --- a/tests/test_buffers.py +++ b/tests/test_buffers.py @@ -413,7 +413,7 @@ class TestOverflowableBuffer(unittest.TestCase): def test_prune_with_buf(self): inst = self._makeOne() - class Buf(object): + class Buf: def prune(self): self.pruned = True @@ -477,7 +477,7 @@ class TestOverflowableBuffer(unittest.TestCase): self.buffers_to_close.remove(inst) def test_close_withbuf(self): - class Buffer(object): + class Buffer: def close(self): self.closed = True @@ -489,7 +489,7 @@ class TestOverflowableBuffer(unittest.TestCase): self.buffers_to_close.remove(inst) -class KindaFilelike(object): +class KindaFilelike: def __init__(self, bytes, close=None, tellresults=None): self.bytes = bytes self.tellresults = tellresults @@ -506,7 +506,7 @@ class Filelike(KindaFilelike): return v -class DummyBuffer(object): +class DummyBuffer: def __init__(self, length=0): self.length = length diff --git a/tests/test_channel.py b/tests/test_channel.py index 14ef5a0..1b119b9 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -29,13 +29,13 @@ class TestHTTPChannel(unittest.TestCase): inst, _, map = self._makeOneWithMap() - class DummyBuffer(object): + class DummyBuffer: chunks = [] def append(self, data): self.chunks.append(data) - class DummyData(object): + class DummyData: def __len__(self): return MAXINT @@ -367,7 +367,7 @@ class TestHTTPChannel(unittest.TestCase): inst, sock, map = self._makeOneWithMap() - class DummyHugeOutbuffer(object): + class DummyHugeOutbuffer: def __init__(self): self.length = MAXINT + 1 @@ -705,7 +705,7 @@ class TestHTTPChannel(unittest.TestCase): self.assertEqual(inst.requests, []) -class DummySock(object): +class DummySock: blocking = False closed = False @@ -732,7 +732,7 @@ class DummySock(object): return len(data) -class DummyLock(object): +class DummyLock: notified = False def __init__(self, acquirable=True): @@ -759,7 +759,7 @@ class DummyLock(object): pass -class DummyBuffer(object): +class DummyBuffer: closed = False def __init__(self, data, toraise=None): @@ -783,7 +783,7 @@ class DummyBuffer(object): self.closed = True -class DummyAdjustments(object): +class DummyAdjustments: outbuf_overflow = 1048576 outbuf_high_watermark = 1048576 inbuf_overflow = 512000 @@ -798,7 +798,7 @@ class DummyAdjustments(object): max_request_header_size = 10000 -class DummyServer(object): +class DummyServer: trigger_pulled = False adj = DummyAdjustments() @@ -813,7 +813,7 @@ class DummyServer(object): self.trigger_pulled = True -class DummyParser(object): +class DummyParser: version = 1 data = None completed = True @@ -831,7 +831,7 @@ class DummyParser(object): return len(data) -class DummyRequest(object): +class DummyRequest: error = None path = "/" version = "1.0" @@ -844,7 +844,7 @@ class DummyRequest(object): self.closed = True -class DummyLogger(object): +class DummyLogger: def __init__(self): self.exceptions = [] self.infos = [] @@ -857,13 +857,13 @@ class DummyLogger(object): self.exceptions.append(msg) -class DummyError(object): +class DummyError: code = "431" reason = "Bleh" body = "My body" -class DummyTaskClass(object): +class DummyTaskClass: wrote_header = True close_on_finish = False serviced = False diff --git a/tests/test_functional.py b/tests/test_functional.py index e894497..d8e60a4 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -61,7 +61,7 @@ class FixtureTcpWSGIServer(server.TcpWSGIServer): queue.put((host, port)) -class SubprocessTests(object): +class SubprocessTests: # For nose: all tests may be ran in separate processes. _multiprocess_can_split_ = True @@ -158,7 +158,7 @@ class SleepyThreadTests(TcpTests, unittest.TestCase): self.assertEqual(result, b"notsleepy returnedsleepy returned") -class EchoTests(object): +class EchoTests: def setUp(self): from tests.fixtureapps import echo @@ -480,7 +480,7 @@ class EchoTests(object): self.assertEqual(echo.remote_host, "192.168.1.1") -class PipeliningTests(object): +class PipeliningTests: def setUp(self): from tests.fixtureapps import echo @@ -522,7 +522,7 @@ class PipeliningTests(object): self.assertEqual(response_body, expect_body) -class ExpectContinueTests(object): +class ExpectContinueTests: def setUp(self): from tests.fixtureapps import echo @@ -561,7 +561,7 @@ class ExpectContinueTests(object): self.assertEqual(response_body, tobytes(data)) -class BadContentLengthTests(object): +class BadContentLengthTests: def setUp(self): from tests.fixtureapps import badcl @@ -627,7 +627,7 @@ class BadContentLengthTests(object): self.assertEqual(int(status), 200) -class NoContentLengthTests(object): +class NoContentLengthTests: def setUp(self): from tests.fixtureapps import nocl @@ -764,7 +764,7 @@ class NoContentLengthTests(object): self.assertRaises(ConnectionClosed, read_http, fp) -class WriteCallbackTests(object): +class WriteCallbackTests: def setUp(self): from tests.fixtureapps import writecb @@ -865,7 +865,7 @@ class WriteCallbackTests(object): self.assertRaises(ConnectionClosed, read_http, fp) -class TooLargeTests(object): +class TooLargeTests: toobig = 1050 @@ -1062,7 +1062,7 @@ class TooLargeTests(object): self.assertRaises(ConnectionClosed, read_http, fp) -class InternalServerErrorTests(object): +class InternalServerErrorTests: def setUp(self): from tests.fixtureapps import error @@ -1214,7 +1214,7 @@ class InternalServerErrorTests(object): self.assertRaises(ConnectionClosed, read_http, fp) -class FileWrapperTests(object): +class FileWrapperTests: def setUp(self): from tests.fixtureapps import filewrapper diff --git a/tests/test_init.py b/tests/test_init.py index f9b91d7..c824c21 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -31,7 +31,7 @@ class Test_serve_paste(unittest.TestCase): self.assertEqual(server.ran, True) -class DummyServerFactory(object): +class DummyServerFactory: ran = False def __call__(self, app, **kw): @@ -44,7 +44,7 @@ class DummyServerFactory(object): self.ran = True -class DummyAdj(object): +class DummyAdj: verbose = False def __init__(self, kw): diff --git a/tests/test_parser.py b/tests/test_parser.py index 91837c7..ae0b263 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -721,7 +721,7 @@ class TestHTTPRequestParserIntegration(unittest.TestCase): self.assertEqual(self.parser.headers, {"CONTENT_LENGTH": "6",}) -class DummyBodyStream(object): +class DummyBodyStream: def getfile(self): return self diff --git a/tests/test_proxy_headers.py b/tests/test_proxy_headers.py index 15b4a08..1aea477 100644 --- a/tests/test_proxy_headers.py +++ b/tests/test_proxy_headers.py @@ -681,7 +681,7 @@ class TestProxyHeadersMiddleware(unittest.TestCase): self.assertIn(b'Header "X-Forwarded-Host" malformed', response.body) -class DummyLogger(object): +class DummyLogger: def __init__(self): self.logged = [] @@ -689,14 +689,14 @@ class DummyLogger(object): self.logged.append(msg % args) -class DummyApp(object): +class DummyApp: def __call__(self, environ, start_response): self.environ = environ start_response("200 OK", [("Content-Type", "text/plain")]) yield "hello" -class DummyResponse(object): +class DummyResponse: status = None headers = None body = None diff --git a/tests/test_receiver.py b/tests/test_receiver.py index b4910bb..f55aa68 100644 --- a/tests/test_receiver.py +++ b/tests/test_receiver.py @@ -226,7 +226,7 @@ class TestChunkedReceiver(unittest.TestCase): self.assertEqual(inst.error, None) -class DummyBuffer(object): +class DummyBuffer: def __init__(self, data=None): if data is None: data = [] diff --git a/tests/test_server.py b/tests/test_server.py index 9134fb8..857ba8f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -263,7 +263,7 @@ class TestWSGIServer(unittest.TestCase): def test_maintenance(self): inst = self._makeOneWithMap() - class DummyChannel(object): + class DummyChannel: requests = [] zombie = DummyChannel() @@ -479,7 +479,7 @@ class DummySock(socket.socket): pass -class DummyTaskDispatcher(object): +class DummyTaskDispatcher: def __init__(self): self.tasks = [] @@ -490,7 +490,7 @@ class DummyTaskDispatcher(object): self.was_shutdown = True -class DummyTask(object): +class DummyTask: serviced = False start_response_called = False wrote_header = False @@ -512,12 +512,12 @@ class DummyAdj: channel_timeout = 300 -class DummyAsyncore(object): +class DummyAsyncore: def loop(self, timeout=30.0, use_poll=False, map=None, count=None): raise SystemExit -class DummyTrigger(object): +class DummyTrigger: def pull_trigger(self): self.pulled = True @@ -525,7 +525,7 @@ class DummyTrigger(object): pass -class DummyLogger(object): +class DummyLogger: def __init__(self): self.logged = [] diff --git a/tests/test_task.py b/tests/test_task.py index 6466823..10b0344 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -922,7 +922,7 @@ class TestErrorTask(unittest.TestCase): self.assertEqual(lines[8], b"(generated by waitress)") -class DummyTask(object): +class DummyTask: serviced = False cancelled = False @@ -933,7 +933,7 @@ class DummyTask(object): self.cancelled = True -class DummyAdj(object): +class DummyAdj: log_socket_errors = True ident = "waitress" host = "127.0.0.1" @@ -941,7 +941,7 @@ class DummyAdj(object): url_prefix = "" -class DummyServer(object): +class DummyServer: server_name = "localhost" effective_port = 80 @@ -949,7 +949,7 @@ class DummyServer(object): self.adj = DummyAdj() -class DummyChannel(object): +class DummyChannel: closed_when_done = False adj = DummyAdj() creation_time = 0 @@ -970,7 +970,7 @@ class DummyChannel(object): return len(data) -class DummyParser(object): +class DummyParser: version = "1.0" command = "GET" path = "/" @@ -990,7 +990,7 @@ def filter_lines(s): return list(filter(None, s.split(b"\r\n"))) -class DummyLogger(object): +class DummyLogger: def __init__(self): self.logged = [] diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py index 9c23509..d7c8ed3 100644 --- a/tests/test_wasyncore.py +++ b/tests/test_wasyncore.py @@ -33,7 +33,7 @@ else: TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid()) -class DummyLogger(object): # pragma: no cover +class DummyLogger: # pragma: no cover def __init__(self): self.messages = [] @@ -41,7 +41,7 @@ class DummyLogger(object): # pragma: no cover self.messages.append((severity, message)) -class WarningsRecorder(object): # pragma: no cover +class WarningsRecorder: # pragma: no cover """Convenience wrapper for the warnings list returned on entry to the warnings.catch_warnings() context manager. """ @@ -1680,7 +1680,7 @@ class Test_close_all(unittest.TestCase): self.assertRaises(RuntimeError, self._callFUT, map) -class DummyDispatcher(object): +class DummyDispatcher: read_event_handled = False write_event_handled = False expt_event_handled = False @@ -1723,7 +1723,7 @@ class DummyDispatcher(object): raise self.exc -class DummyTime(object): +class DummyTime: def __init__(self): self.sleepvals = [] @@ -1731,7 +1731,7 @@ class DummyTime(object): self.sleepvals.append(val) -class DummySelect(object): +class DummySelect: error = select.error def __init__(self, exc=None, pollster=None): @@ -1748,7 +1748,7 @@ class DummySelect(object): return self.pollster -class DummyPollster(object): +class DummyPollster: def __init__(self, exc=None): self.polled = [] self.exc = exc -- cgit v1.2.1 From 78f48d4130da04aee0deac5d6d8491e2e3e0c750 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:56:20 -0700 Subject: socket.error/IOError -> OSError --- src/waitress/channel.py | 4 ++-- src/waitress/server.py | 6 +++--- src/waitress/task.py | 2 +- src/waitress/trigger.py | 4 ++-- src/waitress/wasyncore.py | 24 ++++++++++++------------ tests/test_channel.py | 2 +- tests/test_functional.py | 4 ++-- tests/test_server.py | 2 +- tests/test_task.py | 2 +- tests/test_wasyncore.py | 16 ++++++++-------- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/waitress/channel.py b/src/waitress/channel.py index dfb36bb..3430481 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -116,7 +116,7 @@ class HTTPChannel(wasyncore.dispatcher): if flush: try: flush() - except socket.error: + except OSError: if self.adj.log_socket_errors: self.logger.exception("Socket error") self.will_close = True @@ -142,7 +142,7 @@ class HTTPChannel(wasyncore.dispatcher): def handle_read(self): try: data = self.recv(self.adj.recv_bytes) - except socket.error: + except OSError: if self.adj.log_socket_errors: self.logger.exception("Socket error") self.handle_close() diff --git a/src/waitress/server.py b/src/waitress/server.py index 02a6668..a5a246c 100644 --- a/src/waitress/server.py +++ b/src/waitress/server.py @@ -260,7 +260,7 @@ class BaseWSGIServer(wasyncore.dispatcher): if server_name == "0.0.0.0" or server_name == "::": try: return str(self.socketmod.gethostname()) - except (socket.error, UnicodeDecodeError): # pragma: no cover + except (OSError, UnicodeDecodeError): # pragma: no cover # We also deal with UnicodeDecodeError in case of Windows with # non-ascii hostname return "localhost" @@ -268,7 +268,7 @@ class BaseWSGIServer(wasyncore.dispatcher): # Now let's try and convert the IP address to a proper hostname try: server_name = self.socketmod.gethostbyaddr(server_name)[0] - except (socket.error, UnicodeDecodeError): # pragma: no cover + except (OSError, UnicodeDecodeError): # pragma: no cover # We also deal with UnicodeDecodeError in case of Windows with # non-ascii hostname pass @@ -312,7 +312,7 @@ class BaseWSGIServer(wasyncore.dispatcher): if v is None: return conn, addr = v - except socket.error: + except OSError: # Linux: On rare occasions we get a bogus socket back from # accept. socketmodule.c:makesockaddr complains that the # address family is unknown. We don't want the whole server diff --git a/src/waitress/task.py b/src/waitress/task.py index 6ab6bfe..1e30839 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -170,7 +170,7 @@ class Task: self.start() self.execute() self.finish() - except socket.error: + except OSError: self.close_on_finish = True if self.channel.adj.log_socket_errors: raise diff --git a/src/waitress/trigger.py b/src/waitress/trigger.py index 10ee185..09c291e 100644 --- a/src/waitress/trigger.py +++ b/src/waitress/trigger.py @@ -98,7 +98,7 @@ class _triggerbase: def handle_read(self): try: self.recv(8192) - except (OSError, socket.error): + except OSError: return with self.lock: for thunk in self.thunks: @@ -173,7 +173,7 @@ else: # pragma: no cover try: w.connect(connect_address) break # success - except socket.error as detail: + except OSError as detail: if detail[0] != errno.WSAEADDRINUSE: # "Address already in use" is the only error # I've seen on two WinXP Pro SP2 boxes, under diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index 09bcafa..ae9a6a9 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -138,7 +138,7 @@ def readwrite(obj, flags): obj.handle_expt_event() if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL): obj.handle_close() - except socket.error as e: + except OSError as e: if e.args[0] not in _DISCONNECTED: obj.handle_error() else: @@ -172,7 +172,7 @@ def poll(timeout=0.0, map=None): try: r, w, e = select.select(r, w, e, timeout) - except select.error as err: + except OSError as err: if err.args[0] != EINTR: raise else: @@ -218,7 +218,7 @@ def poll2(timeout=0.0, map=None): try: r = pollster.poll(timeout) - except select.error as err: + except OSError as err: if err.args[0] != EINTR: raise r = [] @@ -305,7 +305,7 @@ class dispatcher: # passed be connected. try: self.addr = sock.getpeername() - except socket.error as err: + except OSError as err: if err.args[0] in (ENOTCONN, EINVAL): # To handle the case where we got an unconnected # socket. @@ -368,7 +368,7 @@ class dispatcher: socket.SO_REUSEADDR, self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1, ) - except socket.error: + except OSError: pass # ================================================== @@ -412,7 +412,7 @@ class dispatcher: self.addr = address self.handle_connect_event() else: - raise socket.error(err, errorcode[err]) + raise OSError(err, errorcode[err]) def accept(self): # XXX can return either an address pair or None @@ -420,7 +420,7 @@ class dispatcher: conn, addr = self.socket.accept() except TypeError: return None - except socket.error as why: + except OSError as why: if why.args[0] in (EWOULDBLOCK, ECONNABORTED, EAGAIN): return None else: @@ -432,7 +432,7 @@ class dispatcher: try: result = self.socket.send(data) return result - except socket.error as why: + except OSError as why: if why.args[0] == EWOULDBLOCK: return 0 elif why.args[0] in _DISCONNECTED: @@ -451,7 +451,7 @@ class dispatcher: return b"" else: return data - except socket.error as why: + except OSError as why: # winsock sometimes raises ENOTCONN if why.args[0] in _DISCONNECTED: self.handle_close() @@ -467,7 +467,7 @@ class dispatcher: if self.socket is not None: try: self.socket.close() - except socket.error as why: + except OSError as why: if why.args[0] not in (ENOTCONN, EBADF): raise @@ -501,7 +501,7 @@ class dispatcher: def handle_connect_event(self): err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if err != 0: - raise socket.error(err, _strerror(err)) + raise OSError(err, _strerror(err)) self.handle_connect() self.connected = True self.connecting = False @@ -608,7 +608,7 @@ def close_all(map=None, ignore_all=False): for x in list(map.values()): # list() FBO py3 try: x.close() - except socket.error as x: + except OSError as x: if x.args[0] == EBADF: pass elif not ignore_all: diff --git a/tests/test_channel.py b/tests/test_channel.py index 1b119b9..a8102fc 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -195,7 +195,7 @@ class TestHTTPChannel(unittest.TestCase): inst.will_close = False def recv(b): - raise socket.error + raise OSError inst.recv = recv inst.last_activity = 0 diff --git a/tests/test_functional.py b/tests/test_functional.py index d8e60a4..f92faaf 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1531,7 +1531,7 @@ if hasattr(socket, "AF_UNIX"): # 'Broken pipe' error when the socket it closed. try: self.sock.send(to_send) - except socket.error as exc: + except OSError as exc: self.assertEqual(get_errno(exc), errno.EPIPE) class UnixEchoTests(EchoTests, UnixTests, unittest.TestCase): @@ -1602,7 +1602,7 @@ class ConnectionClosed(Exception): def read_http(fp): # pragma: no cover try: response_line = fp.readline() - except socket.error as exc: + except OSError as exc: fp.close() # errno 104 is ENOTRECOVERABLE, In WinSock 10054 is ECONNRESET if get_errno(exc) in (errno.ECONNABORTED, errno.ECONNRESET, 104, 10054): diff --git a/tests/test_server.py b/tests/test_server.py index 857ba8f..7242aa7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -240,7 +240,7 @@ class TestWSGIServer(unittest.TestCase): inst.adj = DummyAdj def foo(): - raise socket.error + raise OSError inst.accept = foo inst.logger = DummyLogger() diff --git a/tests/test_task.py b/tests/test_task.py index 10b0344..9c170bb 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -400,7 +400,7 @@ class TestWSGITask(unittest.TestCase): inst = self._makeOne() def execute(): - raise socket.error + raise OSError inst.execute = execute self.assertRaises(socket.error, inst.service) diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py index d7c8ed3..df896db 100644 --- a/tests/test_wasyncore.py +++ b/tests/test_wasyncore.py @@ -401,7 +401,7 @@ def _is_ipv6_enabled(): # pragma: no cover sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock.bind(("::1", 0)) return True - except socket.error: + except OSError: pass finally: if sock: @@ -1420,7 +1420,7 @@ class Test_dispatcher(unittest.TestCase): sock = dummysocket() def getpeername(): - raise socket.error(errno.EBADF) + raise OSError(errno.EBADF) map = {} sock.getpeername = getpeername @@ -1454,7 +1454,7 @@ class Test_dispatcher(unittest.TestCase): def setsockopt(*arg, **kw): sock.errored = True - raise socket.error + raise OSError sock.setsockopt = setsockopt sock.getsockopt = lambda *arg: 0 @@ -1486,7 +1486,7 @@ class Test_dispatcher(unittest.TestCase): map = {} def accept(*arg, **kw): - raise socket.error(122) + raise OSError(122) sock.accept = accept inst = self._makeOne(sock=sock, map=map) @@ -1497,7 +1497,7 @@ class Test_dispatcher(unittest.TestCase): map = {} def send(*arg, **kw): - raise socket.error(errno.EWOULDBLOCK) + raise OSError(errno.EWOULDBLOCK) sock.send = send inst = self._makeOne(sock=sock, map=map) @@ -1509,7 +1509,7 @@ class Test_dispatcher(unittest.TestCase): map = {} def send(*arg, **kw): - raise socket.error(122) + raise OSError(122) sock.send = send inst = self._makeOne(sock=sock, map=map) @@ -1520,7 +1520,7 @@ class Test_dispatcher(unittest.TestCase): map = {} def recv(*arg, **kw): - raise socket.error(errno.ECONNRESET) + raise OSError(errno.ECONNRESET) def handle_close(): inst.close_handled = True @@ -1537,7 +1537,7 @@ class Test_dispatcher(unittest.TestCase): map = {} def close(): - raise socket.error(122) + raise OSError(122) sock.close = close inst = self._makeOne(sock=sock, map=map) -- cgit v1.2.1 From 17cc79cb9d2dd0a93257de75515168fee659e086 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:57:13 -0700 Subject: Bare super() --- src/waitress/proxy_headers.py | 2 +- src/waitress/server.py | 2 +- tests/test_channel.py | 2 +- tests/test_functional.py | 6 +++--- tests/test_task.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/waitress/proxy_headers.py b/src/waitress/proxy_headers.py index 1df8b8e..ea7ac56 100644 --- a/src/waitress/proxy_headers.py +++ b/src/waitress/proxy_headers.py @@ -22,7 +22,7 @@ class MalformedProxyHeader(Exception): self.header = header self.reason = reason self.value = value - super(MalformedProxyHeader, self).__init__(header, reason, value) + super().__init__(header, reason, value) def proxy_headers_middleware( diff --git a/src/waitress/server.py b/src/waitress/server.py index a5a246c..8c477c6 100644 --- a/src/waitress/server.py +++ b/src/waitress/server.py @@ -405,7 +405,7 @@ if hasattr(socket, "AF_UNIX"): if sockinfo is None: sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None) - super(UnixWSGIServer, self).__init__( + super().__init__( application, map=map, _start=_start, diff --git a/tests/test_channel.py b/tests/test_channel.py index a8102fc..4d8aa23 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -270,7 +270,7 @@ class TestHTTPChannel(unittest.TestCase): class Lock(DummyLock): def wait(self): inst.total_outbufs_len = 0 - super(Lock, self).wait() + super().wait() inst.outbuf_lock = Lock() wrote = inst.write_soon(b"xyz") diff --git a/tests/test_functional.py b/tests/test_functional.py index f92faaf..add50f3 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -54,7 +54,7 @@ class FixtureTcpWSGIServer(server.TcpWSGIServer): def __init__(self, application, queue, **kw): # pragma: no cover # Coverage doesn't see this as it's ran in a separate process. kw["port"] = 0 # Bind to any available port. - super(FixtureTcpWSGIServer, self).__init__(application, **kw) + super().__init__(application, **kw) host, port = self.socket.getsockname() if os.name == "nt": host = "127.0.0.1" @@ -1512,7 +1512,7 @@ if hasattr(socket, "AF_UNIX"): # Coverage doesn't see this as it's ran in a separate process. # To permit parallel testing, use a PID-dependent socket. kw["unix_socket"] = "/tmp/waitress.test-%d.sock" % os.getpid() - super(FixtureUnixWSGIServer, self).__init__(application, **kw) + super().__init__(application, **kw) queue.put(self.socket.getsockname()) class UnixTests(SubprocessTests): @@ -1523,7 +1523,7 @@ if hasattr(socket, "AF_UNIX"): return UnixHTTPConnection(self.bound_to) def stop_subprocess(self): - super(UnixTests, self).stop_subprocess() + super().stop_subprocess() cleanup_unix_socket(self.bound_to) def send_check_error(self, to_send): diff --git a/tests/test_task.py b/tests/test_task.py index 9c170bb..6848db2 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -15,7 +15,7 @@ class TestThreadedTaskDispatcher(unittest.TestCase): class BadDummyTask(DummyTask): def service(self): - super(BadDummyTask, self).service() + super().service() inst.stop_count += 1 raise Exception -- cgit v1.2.1 From cf029b4a34b049203640da7a013d4973ec8e465c Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:57:51 -0700 Subject: Remove coding line --- tests/test_compat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_compat.py b/tests/test_compat.py index 37c2193..1940e79 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import unittest -- cgit v1.2.1 From 08b3629cd3fe89f9ce7daee3d445fe03588e3d14 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:58:48 -0700 Subject: Cleanup string formatting --- src/waitress/proxy_headers.py | 2 +- src/waitress/runner.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/waitress/proxy_headers.py b/src/waitress/proxy_headers.py index ea7ac56..d411da0 100644 --- a/src/waitress/proxy_headers.py +++ b/src/waitress/proxy_headers.py @@ -53,7 +53,7 @@ def proxy_headers_middleware( ex.reason, ex.value, ) - error = BadRequest('Header "{0}" malformed.'.format(ex.header)) + error = BadRequest('Header "{}" malformed.'.format(ex.header)) return error.wsgi_response(environ, start_response) # Clear out the untrusted proxy headers diff --git a/src/waitress/runner.py b/src/waitress/runner.py index 2495084..4fb3e6b 100644 --- a/src/waitress/runner.py +++ b/src/waitress/runner.py @@ -14,7 +14,6 @@ """Command line runner. """ -from __future__ import print_function, unicode_literals import getopt import os @@ -191,7 +190,7 @@ RUNNER_PATTERN = re.compile( def match(obj_name): matches = RUNNER_PATTERN.match(obj_name) if not matches: - raise ValueError("Malformed application '{0}'".format(obj_name)) + raise ValueError("Malformed application '{}'".format(obj_name)) return matches.group("module"), matches.group("object") @@ -216,7 +215,7 @@ def resolve(module_name, object_name): def show_help(stream, name, error=None): # pragma: no cover if error is not None: - print("Error: {0}\n".format(error), file=stream) + print("Error: {}\n".format(error), file=stream) print(HELP.format(name), file=stream) @@ -224,7 +223,7 @@ def show_exception(stream): exc_type, exc_value = sys.exc_info()[:2] args = getattr(exc_value, "args", None) print( - ("There was an exception ({0}) importing your module.\n").format( + ("There was an exception ({}) importing your module.\n").format( exc_type.__name__, ), file=stream, @@ -232,7 +231,7 @@ def show_exception(stream): if args: print("It had these arguments: ", file=stream) for idx, arg in enumerate(args, start=1): - print("{0}. {1}\n".format(idx, arg), file=stream) + print("{}. {}\n".format(idx, arg), file=stream) else: print("It had no arguments.", file=stream) @@ -269,11 +268,11 @@ def run(argv=sys.argv, _serve=serve): try: app = resolve(module, obj_name) except ImportError: - show_help(sys.stderr, name, "Bad module '{0}'".format(module)) + show_help(sys.stderr, name, "Bad module '{}'".format(module)) show_exception(sys.stderr) return 1 except AttributeError: - show_help(sys.stderr, name, "Bad object name '{0}'".format(obj_name)) + show_help(sys.stderr, name, "Bad object name '{}'".format(obj_name)) show_exception(sys.stderr) return 1 if kw["call"]: -- cgit v1.2.1 From c4965cfd807a75ff6c0c7cdc12232e891399f5d8 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 6 May 2020 23:59:09 -0700 Subject: yield from instead of loop + yield --- tests/fixtureapps/nocl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/fixtureapps/nocl.py b/tests/fixtureapps/nocl.py index f82bba0..c95a4f5 100644 --- a/tests/fixtureapps/nocl.py +++ b/tests/fixtureapps/nocl.py @@ -6,8 +6,7 @@ def chunks(l, n): # pragma: no cover def gen(body): # pragma: no cover - for chunk in chunks(body, 10): - yield chunk + yield from chunks(body, 10) def app(environ, start_response): # pragma: no cover -- cgit v1.2.1 From 0c88f5485572da3da8f18a833090c7dc0972ae64 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:43:08 -0700 Subject: Re-raise the original exception --- src/waitress/task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/waitress/task.py b/src/waitress/task.py index 1e30839..604bc8e 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -19,7 +19,7 @@ import time from collections import deque from .buffers import ReadOnlyFileBasedBuffer -from .compat import reraise, tobytes +from .compat import tobytes from .utilities import build_http_date, logger, queue_logger rename_headers = { # or keep them without the HTTP_ prefix added @@ -385,7 +385,7 @@ class WSGITask(Task): # 1. "service" method in task.py # 2. "service" method in channel.py # 3. "handler_thread" method in task.py - reraise(exc_info[0], exc_info[1], exc_info[2]) + raise exc_info[1] else: # As per WSGI spec existing headers must be cleared self.response_headers = [] -- cgit v1.2.1 From 6b625826219694f791f81684b21a89de0c1d9e9b Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:43:34 -0700 Subject: Goodbye PY2 --- src/waitress/adjustments.py | 3 +-- tests/test_adjustments.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index fb3233d..6851a7c 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -19,7 +19,6 @@ import warnings from .proxy_headers import PROXY_HEADERS from .compat import ( - PY2, WIN, string_types, HAS_IPV6, @@ -346,7 +345,7 @@ class Adjustments: else: (host, port) = (i, str(self.port)) - if WIN and PY2: # pragma: no cover + if WIN: # pragma: no cover try: # Try turning the port into an integer port = int(port) diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 303c1aa..c6dadea 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -3,7 +3,6 @@ import socket import warnings from waitress.compat import ( - PY2, WIN, ) @@ -220,7 +219,7 @@ class TestAdjustments(unittest.TestCase): self.assertRaises(ValueError, self._makeOne, listen="127.0.0.1:test") def test_service_port(self): - if WIN and PY2: # pragma: no cover + if WIN: # pragma: no cover # On Windows and Python 2 this is broken, so we raise a ValueError self.assertRaises( ValueError, self._makeOne, listen="127.0.0.1:http", -- cgit v1.2.1 From 43d6ad5c54725fd556adda776e9d1a9c8ea0de67 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:43:58 -0700 Subject: No longer rely on compat --- src/waitress/wasyncore.py | 6 +++--- tests/test_wasyncore.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index ae9a6a9..debe0e4 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -320,7 +320,7 @@ class dispatcher: self.socket = None def __repr__(self): - status = [self.__class__.__module__ + "." + compat.qualname(self.__class__)] + status = [self.__class__.__module__ + "." + self.__class__.__qualname__] if self.accepting and self.addr: status.append("listening") elif self.connected: @@ -646,7 +646,7 @@ if os.name == "posix": def __del__(self): if self.fd >= 0: - warnings.warn("unclosed file %r" % self, compat.ResourceWarning) + warnings.warn("unclosed file %r" % self, ResourceWarning) self.close() def recv(self, *args): @@ -685,7 +685,7 @@ if os.name == "posix": pass self.set_file(fd) # set it to non-blocking mode - compat.set_nonblocking(fd) + os.set_blocking(fd, False) def set_file(self, fd): self.socket = file_wrapper(fd) diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py index df896db..a7713a8 100644 --- a/tests/test_wasyncore.py +++ b/tests/test_wasyncore.py @@ -732,7 +732,7 @@ class FileWrapperTest(unittest.TestCase): os.close(fd) try: - with check_warnings(("", compat.ResourceWarning)): + with check_warnings(("", ResourceWarning)): f = None gc_collect() except AssertionError: # pragma: no cover -- cgit v1.2.1 From d2894cbc5c2e66572254fca59d207b9e8d62cf73 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:44:21 -0700 Subject: PY3 is only code path --- tests/test_compat.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/test_compat.py b/tests/test_compat.py index 1940e79..e371348 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -8,13 +8,7 @@ class Test_unquote_bytes_to_wsgi(unittest.TestCase): return unquote_bytes_to_wsgi(v) def test_highorder(self): - from waitress.compat import PY3 - val = b"/a%C5%9B" result = self._callFUT(val) - if PY3: # pragma: no cover - # PEP 3333 urlunquoted-latin1-decoded-bytes - self.assertEqual(result, "/aÅ\x9b") - else: # pragma: no cover - # sanity - self.assertEqual(result, b"/a\xc5\x9b") + # PEP 3333 urlunquoted-latin1-decoded-bytes + self.assertEqual(result, "/aÅ\x9b") -- cgit v1.2.1 From 9ceda5383da82f8118e19753ddf9ed6c631046fc Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:44:53 -0700 Subject: No cover wide exception --- src/waitress/channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/waitress/channel.py b/src/waitress/channel.py index 3430481..d91d0a1 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -120,7 +120,7 @@ class HTTPChannel(wasyncore.dispatcher): if self.adj.log_socket_errors: self.logger.exception("Socket error") self.will_close = True - except Exception: + except Exception: # pragma: nocover self.logger.exception("Unexpected exception when flushing") self.will_close = True -- cgit v1.2.1 From b4a0094a2cf21fa76f9d48eb92303dd758c967e2 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:45:08 -0700 Subject: Cleanup waitress.compat --- src/waitress/compat.py | 163 ++++++++----------------------------------------- 1 file changed, 25 insertions(+), 138 deletions(-) diff --git a/src/waitress/compat.py b/src/waitress/compat.py index fe72a76..aaf7a78 100644 --- a/src/waitress/compat.py +++ b/src/waitress/compat.py @@ -1,135 +1,52 @@ import os -import sys -import types import platform -import warnings - -try: - import urlparse -except ImportError: # pragma: no cover - from urllib import parse as urlparse -try: - import fcntl -except ImportError: # pragma: no cover - fcntl = None # windows +# Fix for issue reported in https://github.com/Pylons/waitress/issues/138, +# Python on Windows may not define IPPROTO_IPV6 in socket. +import socket +import sys +import warnings +from http import client as httplib +from io import StringIO as NativeIO +from urllib import parse as urlparse +from urllib.parse import unquote_to_bytes -# True if we are running on Python 3. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 +import _thread as thread # True if we are running on Windows WIN = platform.system() == "Windows" -if PY3: # pragma: no cover - string_types = (str,) - integer_types = (int,) - class_types = (type,) - text_type = str - binary_type = bytes - long = int -else: - string_types = (basestring,) - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - long = long - -if PY3: # pragma: no cover - from urllib.parse import unquote_to_bytes - - def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring).decode("latin-1") +string_types = (str,) +integer_types = (int,) +class_types = (type,) +text_type = str +binary_type = bytes +long = int -else: - from urlparse import unquote as unquote_to_bytes - - def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring) +def unquote_bytes_to_wsgi(bytestring): + return unquote_to_bytes(bytestring).decode("latin-1") def text_(s, encoding="latin-1", errors="strict"): """ If ``s`` is an instance of ``binary_type``, return ``s.decode(encoding, errors)``, otherwise return ``s``""" + if isinstance(s, binary_type): return s.decode(encoding, errors) - return s # pragma: no cover - - -if PY3: # pragma: no cover - - def tostr(s): - if isinstance(s, text_type): - s = s.encode("latin-1") - return str(s, "latin-1", "strict") - - def tobytes(s): - return bytes(s, "latin-1") - - -else: - tostr = str - def tobytes(s): - return s - - -if PY3: # pragma: no cover - import builtins - - exec_ = getattr(builtins, "exec") + return s # pragma: no cover - def reraise(tp, value, tb=None): - if value is None: - value = tp - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - del builtins +def tostr(s): + return str(s, "latin-1", "strict") -else: # pragma: no cover - def exec_(code, globs=None, locs=None): - """Execute code in a namespace.""" - if globs is None: - frame = sys._getframe(1) - globs = frame.f_globals - if locs is None: - locs = frame.f_locals - del frame - elif locs is None: - locs = globs - exec("""exec code in globs, locs""") - - exec_( - """def reraise(tp, value, tb=None): - raise tp, value, tb -""" - ) - -try: - from StringIO import StringIO as NativeIO -except ImportError: # pragma: no cover - from io import StringIO as NativeIO - -try: - import httplib -except ImportError: # pragma: no cover - from http import client as httplib - -try: - MAXINT = sys.maxint -except AttributeError: # pragma: no cover - MAXINT = sys.maxsize +def tobytes(s): + return bytes(s, "latin-1") -# Fix for issue reported in https://github.com/Pylons/waitress/issues/138, -# Python on Windows may not define IPPROTO_IPV6 in socket. -import socket - +MAXINT = sys.maxsize HAS_IPV6 = socket.has_ipv6 if hasattr(socket, "IPPROTO_IPV6") and hasattr(socket, "IPV6_V6ONLY"): @@ -147,33 +64,3 @@ else: # pragma: no cover RuntimeWarning, ) HAS_IPV6 = False - - -def set_nonblocking(fd): # pragma: no cover - if PY3 and sys.version_info[1] >= 5: - os.set_blocking(fd, False) - elif fcntl is None: - raise RuntimeError("no fcntl module present") - else: - flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) - flags = flags | os.O_NONBLOCK - fcntl.fcntl(fd, fcntl.F_SETFL, flags) - - -if PY3: - ResourceWarning = ResourceWarning -else: - ResourceWarning = UserWarning - - -def qualname(cls): - if PY3: - return cls.__qualname__ - return cls.__name__ - - -try: - import thread -except ImportError: - # py3 - import _thread as thread -- cgit v1.2.1 From 6bb51a78333004526c2144925e8aad49b0b37a8e Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:47:51 -0700 Subject: We haven't supported Py2.6 in a LONG time --- tests/test_adjustments.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index c6dadea..6347be3 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -1,15 +1,9 @@ -import sys import socket +import sys +import unittest import warnings -from waitress.compat import ( - WIN, -) - -if sys.version_info[:2] == (2, 6): # pragma: no cover - import unittest2 as unittest -else: # pragma: no cover - import unittest +from waitress.compat import WIN class Test_asbool(unittest.TestCase): @@ -59,10 +53,12 @@ class Test_as_socket_list(unittest.TestCase): socket.socket(socket.AF_INET, socket.SOCK_STREAM), socket.socket(socket.AF_INET6, socket.SOCK_STREAM), ] + if hasattr(socket, "AF_UNIX"): sockets.append(socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)) new_sockets = as_socket_list(sockets) self.assertEqual(sockets, new_sockets) + for sock in sockets: sock.close() @@ -76,6 +72,7 @@ class Test_as_socket_list(unittest.TestCase): ] new_sockets = as_socket_list(sockets) self.assertEqual(new_sockets, [sockets[0], sockets[1]]) + for sock in [sock for sock in sockets if isinstance(sock, socket.socket)]: sock.close() @@ -98,6 +95,7 @@ class TestAdjustments(unittest.TestCase): return True except socket.gaierror as e: # Check to see what the error is + if e.errno == socket.EAI_ADDRFAMILY: return False else: @@ -224,6 +222,7 @@ class TestAdjustments(unittest.TestCase): self.assertRaises( ValueError, self._makeOne, listen="127.0.0.1:http", ) + return inst = self._makeOne(listen="127.0.0.1:http 0.0.0.0:https") -- cgit v1.2.1 From df5501b3a7a6c6dbf5b7ece3abf9f3258ab89105 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:48:17 -0700 Subject: Blacken the codebase --- src/waitress/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/waitress/server.py b/src/waitress/server.py index 8c477c6..7ab33d7 100644 --- a/src/waitress/server.py +++ b/src/waitress/server.py @@ -413,7 +413,7 @@ if hasattr(socket, "AF_UNIX"): dispatcher=dispatcher, adj=adj, sockinfo=sockinfo, - **kw + **kw, ) def bind_server_socket(self): -- cgit v1.2.1 From 62949f600aa04fcef78f4ae004ae992fa2b6cc06 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 7 May 2020 00:50:21 -0700 Subject: Next version: 2.0.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d7c1c73..7fd9527 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = waitress -version = 1.4.4 +version = 2.0.0dev0 description = Waitress WSGI server long_description = file: README.rst, CHANGES.txt long_description_content_type = text/x-rst -- cgit v1.2.1 From 2ed8a2b60ca7dd3f7b0a3cb4ce1d104ee52d0c58 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 15 Aug 2020 21:06:30 -0700 Subject: Prune _build from docs --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7540038..b41b4db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,5 +17,6 @@ include .coveragerc .flake8 include tox.ini rtd.txt exclude TODO.txt +prune docs/_build recursive-exclude * __pycache__ *.py[cod] -- cgit v1.2.1 From 1b4bcce97cceaae588b5508d42308f13be926ce2 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 15 Aug 2020 21:07:51 -0700 Subject: Add isort to the project --- pyproject.toml | 15 +++++++++++++++ src/waitress/__init__.py | 3 ++- src/waitress/adjustments.py | 6 +----- src/waitress/channel.py | 13 ++----------- src/waitress/compat.py | 9 ++++----- src/waitress/parser.py | 3 ++- src/waitress/proxy_headers.py | 3 +-- src/waitress/server.py | 5 +---- src/waitress/task.py | 2 +- src/waitress/trigger.py | 2 +- src/waitress/wasyncore.py | 36 +++++++++++++++++------------------- tests/fixtureapps/getline.py | 4 ++-- tests/test_buffers.py | 2 +- tests/test_channel.py | 2 +- tests/test_functional.py | 1 + tests/test_parser.py | 4 ++-- tests/test_runner.py | 4 ++-- tests/test_server.py | 4 ++-- tests/test_task.py | 2 +- tests/test_trigger.py | 2 +- tests/test_utilities.py | 7 ++++--- tests/test_wasyncore.py | 17 ++++++++--------- tox.ini | 13 +++++++++---- 23 files changed, 81 insertions(+), 78 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1bc058b..b68b905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,18 @@ exclude = ''' | .tox )/ ''' + + # This next section only exists for people that have their editors +# automatically call isort, black already sorts entries on its own when run. +[tool.isort] +profile = "black" +multi_line_output = 3 +src_paths = ["src", "tests"] +skip_glob = ["docs/*"] +include_trailing_comma = true +force_grid_wrap = false +combine_as_imports = true +line_length = 88 +force_sort_within_sections = true +default_section = "THIRDPARTY" +known_first_party = "waitress" diff --git a/src/waitress/__init__.py b/src/waitress/__init__.py index e6e5911..bbb99da 100644 --- a/src/waitress/__init__.py +++ b/src/waitress/__init__.py @@ -1,6 +1,7 @@ -from waitress.server import create_server import logging +from waitress.server import create_server + def serve(app, **kw): _server = kw.pop("_server", create_server) # test shim diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index 6851a7c..f121b6e 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -17,12 +17,8 @@ import getopt import socket import warnings +from .compat import HAS_IPV6, WIN, string_types from .proxy_headers import PROXY_HEADERS -from .compat import ( - WIN, - string_types, - HAS_IPV6, -) truthy = frozenset(("t", "true", "y", "yes", "on", "1")) diff --git a/src/waitress/channel.py b/src/waitress/channel.py index d91d0a1..7332e40 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -16,18 +16,9 @@ import threading import time import traceback -from waitress.buffers import ( - OverflowableBuffer, - ReadOnlyFileBasedBuffer, -) - +from waitress.buffers import OverflowableBuffer, ReadOnlyFileBasedBuffer from waitress.parser import HTTPRequestParser - -from waitress.task import ( - ErrorTask, - WSGITask, -) - +from waitress.task import ErrorTask, WSGITask from waitress.utilities import InternalServerError from . import wasyncore diff --git a/src/waitress/compat.py b/src/waitress/compat.py index aaf7a78..7c2630c 100644 --- a/src/waitress/compat.py +++ b/src/waitress/compat.py @@ -1,3 +1,6 @@ +import _thread as thread +from http import client as httplib +from io import StringIO as NativeIO import os import platform @@ -5,13 +8,9 @@ import platform # Python on Windows may not define IPPROTO_IPV6 in socket. import socket import sys -import warnings -from http import client as httplib -from io import StringIO as NativeIO from urllib import parse as urlparse from urllib.parse import unquote_to_bytes - -import _thread as thread +import warnings # True if we are running on Windows WIN = platform.system() == "Windows" diff --git a/src/waitress/parser.py b/src/waitress/parser.py index c2789eb..4530b23 100644 --- a/src/waitress/parser.py +++ b/src/waitress/parser.py @@ -16,8 +16,8 @@ This server uses asyncore to accept connections and do initial processing but threads to do work. """ -import re from io import BytesIO +import re from waitress.buffers import OverflowableBuffer from waitress.compat import tostr, unquote_bytes_to_wsgi, urlparse @@ -29,6 +29,7 @@ from waitress.utilities import ( ServerNotImplemented, find_double_newline, ) + from .rfc7230 import HEADER_FIELD diff --git a/src/waitress/proxy_headers.py b/src/waitress/proxy_headers.py index d411da0..13cb2ed 100644 --- a/src/waitress/proxy_headers.py +++ b/src/waitress/proxy_headers.py @@ -1,7 +1,6 @@ from collections import namedtuple -from .utilities import logger, undquote, BadRequest - +from .utilities import BadRequest, logger, undquote PROXY_HEADERS = frozenset( { diff --git a/src/waitress/server.py b/src/waitress/server.py index 7ab33d7..06bb957 100644 --- a/src/waitress/server.py +++ b/src/waitress/server.py @@ -20,13 +20,10 @@ import time from waitress import trigger from waitress.adjustments import Adjustments from waitress.channel import HTTPChannel +from waitress.compat import IPPROTO_IPV6, IPV6_V6ONLY from waitress.task import ThreadedTaskDispatcher from waitress.utilities import cleanup_unix_socket -from waitress.compat import ( - IPPROTO_IPV6, - IPV6_V6ONLY, -) from . import wasyncore from .proxy_headers import proxy_headers_middleware diff --git a/src/waitress/task.py b/src/waitress/task.py index 604bc8e..b82109f 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -12,11 +12,11 @@ # ############################################################################## +from collections import deque import socket import sys import threading import time -from collections import deque from .buffers import ReadOnlyFileBasedBuffer from .compat import tobytes diff --git a/src/waitress/trigger.py b/src/waitress/trigger.py index 09c291e..24c4d0d 100644 --- a/src/waitress/trigger.py +++ b/src/waitress/trigger.py @@ -12,9 +12,9 @@ # ############################################################################## +import errno import os import socket -import errno import threading from . import wasyncore diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index debe0e4..9a68c51 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -51,33 +51,31 @@ in the stdlib will be dropped soon. It is neither a copy of the 2.7 asyncore nor the 3.X asyncore; it is a version compatible with either 2.7 or 3.X. """ -from . import compat -from . import utilities - -import logging -import select -import socket -import sys -import time -import warnings - -import os from errno import ( + EAGAIN, EALREADY, - EINPROGRESS, - EWOULDBLOCK, + EBADF, + ECONNABORTED, ECONNRESET, + EINPROGRESS, + EINTR, EINVAL, - ENOTCONN, - ESHUTDOWN, EISCONN, - EBADF, - ECONNABORTED, + ENOTCONN, EPIPE, - EAGAIN, - EINTR, + ESHUTDOWN, + EWOULDBLOCK, errorcode, ) +import logging +import os +import select +import socket +import sys +import time +import warnings + +from . import compat, utilities _DISCONNECTED = frozenset({ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, EBADF}) diff --git a/tests/fixtureapps/getline.py b/tests/fixtureapps/getline.py index 5e0ad3a..bb5b39c 100644 --- a/tests/fixtureapps/getline.py +++ b/tests/fixtureapps/getline.py @@ -2,9 +2,9 @@ import sys if __name__ == "__main__": try: - from urllib.request import urlopen, URLError + from urllib.request import URLError, urlopen except ImportError: - from urllib2 import urlopen, URLError + from urllib2 import URLError, urlopen url = sys.argv[1] headers = {"Content-Type": "text/plain; charset=utf-8"} diff --git a/tests/test_buffers.py b/tests/test_buffers.py index 029acfc..01cdc2d 100644 --- a/tests/test_buffers.py +++ b/tests/test_buffers.py @@ -1,5 +1,5 @@ -import unittest import io +import unittest class TestFileBasedBuffer(unittest.TestCase): diff --git a/tests/test_channel.py b/tests/test_channel.py index 4d8aa23..df3d450 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -1,5 +1,5 @@ -import unittest import io +import unittest class TestHTTPChannel(unittest.TestCase): diff --git a/tests/test_functional.py b/tests/test_functional.py index add50f3..9d94b8e 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -9,6 +9,7 @@ import subprocess import sys import time import unittest + from waitress import server from waitress.compat import httplib, tobytes from waitress.utilities import cleanup_unix_socket diff --git a/tests/test_parser.py b/tests/test_parser.py index ae0b263..e0e4d25 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -20,8 +20,8 @@ from waitress.compat import text_, tobytes class TestHTTPRequestParser(unittest.TestCase): def setUp(self): - from waitress.parser import HTTPRequestParser from waitress.adjustments import Adjustments + from waitress.parser import HTTPRequestParser my_adj = Adjustments() self.parser = HTTPRequestParser(my_adj) @@ -595,8 +595,8 @@ class Test_crack_first_line(unittest.TestCase): class TestHTTPRequestParserIntegration(unittest.TestCase): def setUp(self): - from waitress.parser import HTTPRequestParser from waitress.adjustments import Adjustments + from waitress.parser import HTTPRequestParser my_adj = Adjustments() self.parser = HTTPRequestParser(my_adj) diff --git a/tests/test_runner.py b/tests/test_runner.py index e53018b..e77d103 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -119,7 +119,7 @@ class Test_run(unittest.TestCase): ) def test_simple_call(self): - import tests.fixtureapps.runner as _apps + from tests.fixtureapps import runner as _apps def check_server(app, **kw): self.assertIs(app, _apps.app) @@ -133,7 +133,7 @@ class Test_run(unittest.TestCase): self.assertEqual(runner.run(argv=argv, _serve=check_server), 0) def test_returned_app(self): - import tests.fixtureapps.runner as _apps + from tests.fixtureapps import runner as _apps def check_server(app, **kw): self.assertIs(app, _apps.app) diff --git a/tests/test_server.py b/tests/test_server.py index 7242aa7..05f6b4e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -274,8 +274,8 @@ class TestWSGIServer(unittest.TestCase): self.assertEqual(zombie.will_close, True) def test_backward_compatibility(self): - from waitress.server import WSGIServer, TcpWSGIServer from waitress.adjustments import Adjustments + from waitress.server import TcpWSGIServer, WSGIServer self.assertTrue(WSGIServer is TcpWSGIServer) self.inst = WSGIServer(None, _start=False, port=1234) @@ -411,8 +411,8 @@ if hasattr(socket, "AF_UNIX"): def test_create_with_unix_socket(self): from waitress.server import ( - MultiSocketServer, BaseWSGIServer, + MultiSocketServer, TcpWSGIServer, UnixWSGIServer, ) diff --git a/tests/test_task.py b/tests/test_task.py index 6848db2..0965bf5 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,5 +1,5 @@ -import unittest import io +import unittest class TestThreadedTaskDispatcher(unittest.TestCase): diff --git a/tests/test_trigger.py b/tests/test_trigger.py index af740f6..265679a 100644 --- a/tests/test_trigger.py +++ b/tests/test_trigger.py @@ -1,6 +1,6 @@ -import unittest import os import sys +import unittest if not sys.platform.startswith("win"): diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 15cd24f..ea08477 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -39,16 +39,17 @@ class Test_parse_http_date(unittest.TestCase): class Test_build_http_date(unittest.TestCase): def test_rountdrip(self): - from waitress.utilities import build_http_date, parse_http_date from time import time + from waitress.utilities import build_http_date, parse_http_date + t = int(time()) self.assertEqual(t, parse_http_date(build_http_date(t))) class Test_unpack_rfc850(unittest.TestCase): def _callFUT(self, val): - from waitress.utilities import unpack_rfc850, rfc850_reg + from waitress.utilities import rfc850_reg, unpack_rfc850 return unpack_rfc850(rfc850_reg.match(val.lower())) @@ -60,7 +61,7 @@ class Test_unpack_rfc850(unittest.TestCase): class Test_unpack_rfc_822(unittest.TestCase): def _callFUT(self, val): - from waitress.utilities import unpack_rfc822, rfc822_reg + from waitress.utilities import rfc822_reg, unpack_rfc822 return unpack_rfc822(rfc822_reg.match(val.lower())) diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py index a7713a8..a7e2878 100644 --- a/tests/test_wasyncore.py +++ b/tests/test_wasyncore.py @@ -1,21 +1,20 @@ -from waitress import wasyncore as asyncore -from waitress import compat import contextlib +import errno import functools import gc -import unittest -import select +from io import BytesIO import os -import socket -import sys -import time -import errno import re +import select +import socket import struct +import sys import threading +import time +import unittest import warnings -from io import BytesIO +from waitress import compat, wasyncore as asyncore TIMEOUT = 3 HAS_UNIX_SOCKETS = hasattr(socket, "AF_UNIX") diff --git a/tox.ini b/tox.ini index 2040825..ff0ff14 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = lint, py35,py36,py37,py38,pypy3, + py39, docs, coverage isolated_build = True @@ -31,6 +32,7 @@ depends = py38 skip_install = True commands = black --check --diff . + isort --check-only --df src/waitress tests check-manifest # flake8 src/waitress/ tests # build sdist/wheel @@ -38,12 +40,13 @@ commands = twine check dist/* deps = black - readme_renderer check-manifest - pep517 - twine flake8 flake8-bugbear + isort + pep517 + readme_renderer + twine [testenv:docs] whitelist_externals = @@ -61,12 +64,14 @@ deps = flake8 flake8-bugbear -[testenv:run-black] +[testenv:run-format] skip_install = True commands = + isort src/waitress tests black . deps = black + isort [testenv:build] skip_install = true -- cgit v1.2.1 From d347823b8eb5bdc9ccecc440168032658bd752fa Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 15 Aug 2020 22:29:01 -0700 Subject: Reduce compat.py to minimum size --- src/waitress/adjustments.py | 4 +- src/waitress/compat.py | 36 ---- src/waitress/parser.py | 52 +++-- src/waitress/rfc7230.py | 6 +- src/waitress/task.py | 7 +- tests/test_compat.py | 14 -- tests/test_functional.py | 448 +++++++++++++++++++++----------------------- tests/test_parser.py | 104 +++------- tests/test_proxy_headers.py | 4 +- tests/test_runner.py | 4 +- tests/test_wasyncore.py | 48 ++++- 11 files changed, 337 insertions(+), 390 deletions(-) delete mode 100644 tests/test_compat.py diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index f121b6e..145ac86 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -17,7 +17,7 @@ import getopt import socket import warnings -from .compat import HAS_IPV6, WIN, string_types +from .compat import HAS_IPV6, WIN from .proxy_headers import PROXY_HEADERS truthy = frozenset(("t", "true", "y", "yes", "on", "1")) @@ -47,7 +47,7 @@ def asoctal(s): def aslist_cronly(value): - if isinstance(value, string_types): + if isinstance(value, str): value = filter(None, [x.strip() for x in value.splitlines()]) return list(value) diff --git a/src/waitress/compat.py b/src/waitress/compat.py index 7c2630c..67543b9 100644 --- a/src/waitress/compat.py +++ b/src/waitress/compat.py @@ -1,50 +1,14 @@ -import _thread as thread -from http import client as httplib -from io import StringIO as NativeIO -import os import platform # Fix for issue reported in https://github.com/Pylons/waitress/issues/138, # Python on Windows may not define IPPROTO_IPV6 in socket. import socket import sys -from urllib import parse as urlparse -from urllib.parse import unquote_to_bytes import warnings # True if we are running on Windows WIN = platform.system() == "Windows" -string_types = (str,) -integer_types = (int,) -class_types = (type,) -text_type = str -binary_type = bytes -long = int - - -def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring).decode("latin-1") - - -def text_(s, encoding="latin-1", errors="strict"): - """ If ``s`` is an instance of ``binary_type``, return - ``s.decode(encoding, errors)``, otherwise return ``s``""" - - if isinstance(s, binary_type): - return s.decode(encoding, errors) - - return s # pragma: no cover - - -def tostr(s): - return str(s, "latin-1", "strict") - - -def tobytes(s): - return bytes(s, "latin-1") - - MAXINT = sys.maxsize HAS_IPV6 = socket.has_ipv6 diff --git a/src/waitress/parser.py b/src/waitress/parser.py index 4530b23..3b99921 100644 --- a/src/waitress/parser.py +++ b/src/waitress/parser.py @@ -18,9 +18,10 @@ processing but threads to do work. """ from io import BytesIO import re +from urllib import parse +from urllib.parse import unquote_to_bytes from waitress.buffers import OverflowableBuffer -from waitress.compat import tostr, unquote_bytes_to_wsgi, urlparse from waitress.receiver import ChunkedReceiver, FixedStreamReceiver from waitress.utilities import ( BadRequest, @@ -33,6 +34,10 @@ from waitress.utilities import ( from .rfc7230 import HEADER_FIELD +def unquote_bytes_to_wsgi(bytestring): + return unquote_to_bytes(bytestring).decode("latin-1") + + class ParsingError(Exception): pass @@ -80,11 +85,13 @@ class HTTPRequestParser: bytes consumed. Sets the completed flag once both the header and the body have been received. """ + if self.completed: return 0 # Can't consume any more. datalen = len(data) br = self.body_rcv + if br is None: # In header. max_header = self.adj.max_request_header_size @@ -106,12 +113,14 @@ class HTTPRequestParser: # If the first line + headers is over the max length, we return a # RequestHeaderFieldsTooLarge error rather than continuing to # attempt to parse the headers. + if self.header_bytes_received >= max_header: self.parse_header(b"GET / HTTP/1.0\r\n") self.error = RequestHeaderFieldsTooLarge( "exceeds max_header of %s" % max_header ) self.completed = True + return consumed if index >= 0: @@ -195,6 +204,7 @@ class HTTPRequestParser: first line of the request). """ index = header_plus.find(b"\r\n") + if index >= 0: first_line = header_plus[:index].rstrip() header = header_plus[index + 2 :] @@ -209,6 +219,7 @@ class HTTPRequestParser: lines = get_header_lines(header) headers = self.headers + for line in lines: header = HEADER_FIELD.match(line) @@ -219,25 +230,26 @@ class HTTPRequestParser: if b"_" in key: # TODO(xistence): Should we drop this request instead? + continue # Only strip off whitespace that is considered valid whitespace by # RFC7230, don't strip the rest value = value.strip(b" \t") - key1 = tostr(key.upper().replace(b"-", b"_")) + key1 = key.upper().replace(b"-", b"_").decode("latin-1") # If a header already exists, we append subsequent values # separated by a comma. Applications already need to handle # the comma separated values, as HTTP front ends might do # the concatenation for you (behavior specified in RFC2616). try: - headers[key1] += tostr(b", " + value) + headers[key1] += (b", " + value).decode("latin-1") except KeyError: - headers[key1] = tostr(value) + headers[key1] = value.decode("latin-1") # command, uri, version will be bytes command, uri, version = crack_first_line(first_line) - version = tostr(version) - command = tostr(command) + version = version.decode("latin-1") + command = command.decode("latin-1") self.command = command self.version = version ( @@ -280,6 +292,7 @@ class HTTPRequestParser: # Note: the identity transfer-coding was removed in RFC7230: # https://tools.ietf.org/html/rfc7230#appendix-A.2 and is thus # not supported + if encoding not in {"chunked"}: raise TransferEncodingNotImplemented( "Transfer-Encoding requested is not supported." @@ -296,6 +309,7 @@ class HTTPRequestParser: expect = headers.get("EXPECT", "").lower() self.expect_continue = expect == "100-continue" + if connection.lower() == "close": self.connection_close = True @@ -306,12 +320,14 @@ class HTTPRequestParser: raise ParsingError("Content-Length is invalid") self.content_length = cl + if cl > 0: buf = OverflowableBuffer(self.adj.inbuf_overflow) self.body_rcv = FixedStreamReceiver(cl, buf) def get_body_stream(self): body_rcv = self.body_rcv + if body_rcv is not None: return body_rcv.getfile() else: @@ -319,6 +335,7 @@ class HTTPRequestParser: def close(self): body_rcv = self.body_rcv + if body_rcv is not None: body_rcv.getbuf().close() @@ -346,16 +363,16 @@ def split_uri(uri): path, query = path.split(b"?", 1) else: try: - scheme, netloc, path, query, fragment = urlparse.urlsplit(uri) + scheme, netloc, path, query, fragment = parse.urlsplit(uri) except UnicodeError: raise ParsingError("Bad URI") return ( - tostr(scheme), - tostr(netloc), + scheme.decode("latin-1"), + netloc.decode("latin-1"), unquote_bytes_to_wsgi(path), - tostr(query), - tostr(fragment), + query.decode("latin-1"), + fragment.decode("latin-1"), ) @@ -365,20 +382,24 @@ def get_header_lines(header): """ r = [] lines = header.split(b"\r\n") + for line in lines: if not line: continue if b"\r" in line or b"\n" in line: - raise ParsingError('Bare CR or LF found in header line "%s"' % tostr(line)) + raise ParsingError( + 'Bare CR or LF found in header line "%s"' % str(line, "latin-1") + ) if line.startswith((b" ", b"\t")): if not r: # https://corte.si/posts/code/pathod/pythonservers/index.html - raise ParsingError('Malformed header line "%s"' % tostr(line)) + raise ParsingError('Malformed header line "%s"' % str(line, "latin-1")) r[-1] += line else: r.append(line) + return r @@ -391,6 +412,7 @@ first_line_re = re.compile( def crack_first_line(line): m = first_line_re.match(line) + if m is not None and m.end() == len(line): if m.group(3): version = m.group(5) @@ -407,9 +429,11 @@ def crack_first_line(line): # unsuspecting souls from sending lowercase HTTP methods to waitress # and having the request complete, while servers like nginx drop the # request onto the floor. + if method != method.upper(): - raise ParsingError('Malformed HTTP method "%s"' % tostr(method)) + raise ParsingError('Malformed HTTP method "%s"' % str(method, "latin-1")) uri = m.group(2) + return method, uri, version else: return b"", b"", b"" diff --git a/src/waitress/rfc7230.py b/src/waitress/rfc7230.py index cd33c90..9b25fbd 100644 --- a/src/waitress/rfc7230.py +++ b/src/waitress/rfc7230.py @@ -5,8 +5,6 @@ needed to properly parse HTTP messages. import re -from .compat import tobytes - WS = "[ \t]" OWS = WS + "{0,}?" RWS = WS + "{1,}?" @@ -46,7 +44,7 @@ FIELD_CONTENT = FIELD_VCHAR + "+(?:[ \t]+" + FIELD_VCHAR + "+)*" FIELD_VALUE = "(?:" + FIELD_CONTENT + ")?" HEADER_FIELD = re.compile( - tobytes( + ( "^(?P" + TOKEN + "):" + OWS + "(?P" + FIELD_VALUE + ")" + OWS + "$" - ) + ).encode("latin-1") ) diff --git a/src/waitress/task.py b/src/waitress/task.py index b82109f..779f01e 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -19,7 +19,6 @@ import threading import time from .buffers import ReadOnlyFileBasedBuffer -from .compat import tobytes from .utilities import build_http_date, logger, queue_logger rename_headers = { # or keep them without the HTTP_ prefix added @@ -281,7 +280,7 @@ class Task: lines = [first_line] + next_lines res = "%s\r\n\r\n" % "\r\n".join(lines) - return tobytes(res) + return res.encode("latin-1") def remove_content_length_header(self): response_headers = [] @@ -317,7 +316,7 @@ class Task: cl = self.content_length if self.chunked_response: # use chunked encoding response - towrite = tobytes(hex(len(data))[2:].upper()) + b"\r\n" + towrite = hex(len(data))[2:].upper().encode("latin-1") + b"\r\n" towrite += data + b"\r\n" elif cl is not None: towrite = data[: cl - self.content_bytes_written] @@ -361,7 +360,7 @@ class ErrorTask(Task): self.response_headers.append(("Connection", "close")) self.close_on_finish = True self.content_length = len(body) - self.write(tobytes(body)) + self.write(body.encode("latin-1")) class WSGITask(Task): diff --git a/tests/test_compat.py b/tests/test_compat.py deleted file mode 100644 index e371348..0000000 --- a/tests/test_compat.py +++ /dev/null @@ -1,14 +0,0 @@ -import unittest - - -class Test_unquote_bytes_to_wsgi(unittest.TestCase): - def _callFUT(self, v): - from waitress.compat import unquote_bytes_to_wsgi - - return unquote_bytes_to_wsgi(v) - - def test_highorder(self): - val = b"/a%C5%9B" - result = self._callFUT(val) - # PEP 3333 urlunquoted-latin1-decoded-bytes - self.assertEqual(result, "/aÅ\x9b") diff --git a/tests/test_functional.py b/tests/test_functional.py index 9d94b8e..968fbe6 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,4 +1,5 @@ import errno +from http import client as httplib import logging import multiprocessing import os @@ -11,7 +12,6 @@ import time import unittest from waitress import server -from waitress.compat import httplib, tobytes from waitress.utilities import cleanup_unix_socket dn = os.path.dirname @@ -57,6 +57,7 @@ class FixtureTcpWSGIServer(server.TcpWSGIServer): kw["port"] = 0 # Bind to any available port. super().__init__(application, **kw) host, port = self.socket.getsockname() + if os.name == "nt": host = "127.0.0.1" queue.put((host, port)) @@ -99,9 +100,9 @@ class SubprocessTests: def assertline(self, line, status, reason, version): v, s, r = (x.strip() for x in line.split(None, 2)) - self.assertEqual(s, tobytes(status)) - self.assertEqual(r, tobytes(reason)) - self.assertEqual(v, tobytes(version)) + self.assertEqual(s, status.encode("latin-1")) + self.assertEqual(r, reason.encode("latin-1")) + self.assertEqual(v, version.encode("latin-1")) def create_socket(self): return socket.socket(self.server.family, socket.SOCK_STREAM) @@ -143,9 +144,11 @@ class SleepyThreadTests(TcpTests, unittest.TestCase): ) r, w = os.pipe() procs = [] + for cmd in cmds: procs.append(subprocess.Popen(cmd, stdout=w)) time.sleep(3) + for proc in procs: if proc.returncode is not None: # pragma: no cover proc.terminate() @@ -178,11 +181,11 @@ class EchoTests: from tests.fixtureapps import echo line, headers, body = read_http(fp) + return line, headers, echo.parse_response(body) def test_date_and_server(self): - to_send = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -193,8 +196,7 @@ class EchoTests: def test_bad_host_header(self): # https://corte.si/posts/code/pathod/pythonservers/index.html - to_send = "GET / HTTP/1.0\r\n Host: 0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET / HTTP/1.0\r\n Host: 0\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -204,9 +206,8 @@ class EchoTests: self.assertTrue(headers.get("date")) def test_send_with_body(self): - to_send = "GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n" - to_send += "hello" - to_send = tobytes(to_send) + to_send = b"GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n" + to_send += b"hello" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -216,8 +217,7 @@ class EchoTests: self.assertEqual(echo.body, b"hello") def test_send_empty_body(self): - to_send = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -228,6 +228,7 @@ class EchoTests: def test_multiple_requests_with_body(self): orig_sock = self.sock + for x in range(3): self.sock = self.create_socket() self.test_send_with_body() @@ -236,6 +237,7 @@ class EchoTests: def test_multiple_requests_without_body(self): orig_sock = self.sock + for x in range(3): self.sock = self.create_socket() self.test_send_empty_body() @@ -243,13 +245,13 @@ class EchoTests: self.sock = orig_sock def test_without_crlf(self): - data = "Echo\r\nthis\r\nplease" - s = tobytes( - "GET / HTTP/1.0\r\n" - "Connection: close\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s" % (len(data), data) + data = b"Echo\r\nthis\r\nplease" + s = ( + b"GET / HTTP/1.0\r\n" + b"Connection: close\r\n" + b"Content-Length: %d\r\n" + b"\r\n" + b"%s" % (len(data), data) ) self.connect() self.sock.send(s) @@ -258,40 +260,42 @@ class EchoTests: self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(int(echo.content_length), len(data)) self.assertEqual(len(echo.body), len(data)) - self.assertEqual(echo.body, tobytes(data)) + self.assertEqual(echo.body, (data)) def test_large_body(self): # 1024 characters. - body = "This string has 32 characters.\r\n" * 32 - s = tobytes( - "GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(body), body) - ) + body = b"This string has 32 characters.\r\n" * 32 + s = b"GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(body), body) self.connect() self.sock.send(s) fp = self.sock.makefile("rb", 0) line, headers, echo = self._read_echo(fp) self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(echo.content_length, "1024") - self.assertEqual(echo.body, tobytes(body)) + self.assertEqual(echo.body, body) def test_many_clients(self): conns = [] + for n in range(50): h = self.make_http_connection() h.request("GET", "/", headers={"Accept": "text/plain"}) conns.append(h) responses = [] + for h in conns: response = h.getresponse() self.assertEqual(response.status, 200) responses.append(response) + for response in responses: response.read() + for h in conns: h.close() def test_chunking_request_without_content(self): - header = tobytes("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n") + header = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" self.connect() self.sock.send(header) self.sock.send(b"0\r\n\r\n") @@ -306,10 +310,11 @@ class EchoTests: control_line = b"20;\r\n" # 20 hex = 32 dec s = b"This string has 32 characters.\r\n" expected = s * 12 - header = tobytes("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n") + header = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" self.connect() self.sock.send(header) fp = self.sock.makefile("rb", 0) + for n in range(12): self.sock.send(control_line) self.sock.send(s) @@ -322,13 +327,12 @@ class EchoTests: self.assertFalse("transfer-encoding" in headers) def test_broken_chunked_encoding(self): - control_line = "20;\r\n" # 20 hex = 32 dec - s = "This string has 32 characters.\r\n" - to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" - to_send += control_line + s + "\r\n" + control_line = b"20;\r\n" # 20 hex = 32 dec + s = b"This string has 32 characters.\r\n" + to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + to_send += control_line + s + b"\r\n" # garbage in input - to_send += "garbage\r\n" - to_send = tobytes(to_send) + to_send += b"garbage\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -347,13 +351,12 @@ class EchoTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_broken_chunked_encoding_missing_chunk_end(self): - control_line = "20;\r\n" # 20 hex = 32 dec - s = "This string has 32 characters.\r\n" - to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + control_line = b"20;\r\n" # 20 hex = 32 dec + s = b"This string has 32 characters.\r\n" + to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" to_send += control_line + s # garbage in input - to_send += "garbage" - to_send = tobytes(to_send) + to_send += b"garbage" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -374,10 +377,8 @@ class EchoTests: def test_keepalive_http_10(self): # Handling of Keep-Alive within HTTP 1.0 - data = "Default: Don't keep me alive" - s = tobytes( - "GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data) - ) + data = b"Default: Don't keep me alive" + s = b"GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data) self.connect() self.sock.send(s) response = httplib.HTTPResponse(self.sock) @@ -392,13 +393,13 @@ class EchoTests: # If header Connection: Keep-Alive is explicitly sent, # we want to keept the connection open, we also need to return # the corresponding header - data = "Keep me alive" - s = tobytes( - "GET / HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s" % (len(data), data) + data = b"Keep me alive" + s = ( + b"GET / HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: %d\r\n" + b"\r\n" + b"%s" % (len(data), data) ) self.connect() self.sock.send(s) @@ -412,10 +413,8 @@ class EchoTests: # Handling of Keep-Alive within HTTP 1.1 # All connections are kept alive, unless stated otherwise - data = "Default: Keep me alive" - s = tobytes( - "GET / HTTP/1.1\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data) - ) + data = b"Default: Keep me alive" + s = b"GET / HTTP/1.1\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data) self.connect() self.sock.send(s) response = httplib.HTTPResponse(self.sock) @@ -425,13 +424,13 @@ class EchoTests: def test_keepalive_http11_explicit(self): # Explicitly set keep-alive - data = "Default: Keep me alive" - s = tobytes( - "GET / HTTP/1.1\r\n" - "Connection: keep-alive\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s" % (len(data), data) + data = b"Default: Keep me alive" + s = ( + b"GET / HTTP/1.1\r\n" + b"Connection: keep-alive\r\n" + b"Content-Length: %d\r\n" + b"\r\n" + b"%s" % (len(data), data) ) self.connect() self.sock.send(s) @@ -442,13 +441,13 @@ class EchoTests: def test_keepalive_http11_connclose(self): # specifying Connection: close explicitly - data = "Don't keep me alive" - s = tobytes( - "GET / HTTP/1.1\r\n" - "Connection: close\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s" % (len(data), data) + data = b"Don't keep me alive" + s = ( + b"GET / HTTP/1.1\r\n" + b"Connection: close\r\n" + b"Content-Length: %d\r\n" + b"\r\n" + b"%s" % (len(data), data) ) self.connect() self.sock.send(s) @@ -459,14 +458,13 @@ class EchoTests: def test_proxy_headers(self): to_send = ( - "GET / HTTP/1.0\r\n" - "Content-Length: 0\r\n" - "Host: www.google.com:8080\r\n" - "X-Forwarded-For: 192.168.1.1\r\n" - "X-Forwarded-Proto: https\r\n" - "X-Forwarded-Port: 5000\r\n\r\n" + b"GET / HTTP/1.0\r\n" + b"Content-Length: 0\r\n" + b"Host: www.google.com:8080\r\n" + b"X-Forwarded-For: 192.168.1.1\r\n" + b"X-Forwarded-Proto: https\r\n" + b"X-Forwarded-Port: 5000\r\n\r\n" ) - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -492,27 +490,30 @@ class PipeliningTests: def test_pipelining(self): s = ( - "GET / HTTP/1.0\r\n" - "Connection: %s\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s" + b"GET / HTTP/1.0\r\n" + b"Connection: %s\r\n" + b"Content-Length: %d\r\n" + b"\r\n" + b"%s" ) to_send = b"" count = 25 + for n in range(count): - body = "Response #%d\r\n" % (n + 1) + body = b"Response #%d\r\n" % (n + 1) + if n + 1 < count: - conn = "keep-alive" + conn = b"keep-alive" else: - conn = "close" - to_send += tobytes(s % (conn, len(body), body)) + conn = b"close" + to_send += s % (conn, len(body), body) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) + for n in range(count): - expect_body = tobytes("Response #%d\r\n" % (n + 1)) + expect_body = b"Response #%d\r\n" % (n + 1) line = fp.readline() # status line version, status, reason = (x.strip() for x in line.split(None, 2)) headers = parse_headers(fp) @@ -534,14 +535,14 @@ class ExpectContinueTests: def test_expect_continue(self): # specifying Connection: close explicitly - data = "I have expectations" - to_send = tobytes( - "GET / HTTP/1.1\r\n" - "Connection: close\r\n" - "Content-Length: %d\r\n" - "Expect: 100-continue\r\n" - "\r\n" - "%s" % (len(data), data) + data = b"I have expectations" + to_send = ( + b"GET / HTTP/1.1\r\n" + b"Connection: close\r\n" + b"Content-Length: %d\r\n" + b"Expect: 100-continue\r\n" + b"\r\n" + b"%s" % (len(data), data) ) self.connect() self.sock.send(to_send) @@ -559,7 +560,7 @@ class ExpectContinueTests: response_body = fp.read(length) self.assertEqual(int(status), 200) self.assertEqual(length, len(response_body)) - self.assertEqual(response_body, tobytes(data)) + self.assertEqual(response_body, data) class BadContentLengthTests: @@ -574,11 +575,11 @@ class BadContentLengthTests: def test_short_body(self): # check to see if server closes connection when body is too short # for cl header - to_send = tobytes( - "GET /short_body HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /short_body HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -591,7 +592,7 @@ class BadContentLengthTests: self.assertEqual(int(status), 200) self.assertNotEqual(content_length, len(response_body)) self.assertEqual(len(response_body), content_length - 1) - self.assertEqual(response_body, tobytes("abcdefghi")) + self.assertEqual(response_body, b"abcdefghi") # remote closed connection (despite keepalive header); not sure why # first send succeeds self.send_check_error(to_send) @@ -600,11 +601,11 @@ class BadContentLengthTests: def test_long_body(self): # check server doesnt close connection when body is too short # for cl header - to_send = tobytes( - "GET /long_body HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /long_body HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -616,7 +617,7 @@ class BadContentLengthTests: response_body = fp.read(content_length) self.assertEqual(int(status), 200) self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes("abcdefgh")) + self.assertEqual(response_body, b"abcdefgh") # remote does not close connection (keepalive header) self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -638,14 +639,13 @@ class NoContentLengthTests: self.stop_subprocess() def test_http10_generator(self): - body = string.ascii_letters + body = string.ascii_letters.encode("latin-1") to_send = ( - "GET / HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: %d\r\n\r\n" % len(body) + b"GET / HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: %d\r\n\r\n" % len(body) ) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -653,21 +653,20 @@ class NoContentLengthTests: self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(headers.get("content-length"), None) self.assertEqual(headers.get("connection"), "close") - self.assertEqual(response_body, tobytes(body)) + self.assertEqual(response_body, body) # remote closed connection (despite keepalive header), because # generators cannot have a content-length divined self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) def test_http10_list(self): - body = string.ascii_letters + body = string.ascii_letters.encode("latin-1") to_send = ( - "GET /list HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: %d\r\n\r\n" % len(body) + b"GET /list HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: %d\r\n\r\n" % len(body) ) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -675,7 +674,7 @@ class NoContentLengthTests: self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(headers["content-length"], str(len(body))) self.assertEqual(headers.get("connection"), "Keep-Alive") - self.assertEqual(response_body, tobytes(body)) + self.assertEqual(response_body, body) # remote keeps connection open because it divined the content length # from a length-1 list self.sock.send(to_send) @@ -683,14 +682,13 @@ class NoContentLengthTests: self.assertline(line, "200", "OK", "HTTP/1.0") def test_http10_listlentwo(self): - body = string.ascii_letters + body = string.ascii_letters.encode("latin-1") to_send = ( - "GET /list_lentwo HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: %d\r\n\r\n" % len(body) + b"GET /list_lentwo HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: %d\r\n\r\n" % len(body) ) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -698,7 +696,7 @@ class NoContentLengthTests: self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(headers.get("content-length"), None) self.assertEqual(headers.get("connection"), "close") - self.assertEqual(response_body, tobytes(body)) + self.assertEqual(response_body, body) # remote closed connection (despite keepalive header), because # lists of length > 1 cannot have their content length divined self.send_check_error(to_send) @@ -706,18 +704,20 @@ class NoContentLengthTests: def test_http11_generator(self): body = string.ascii_letters - to_send = "GET / HTTP/1.1\r\nContent-Length: %s\r\n\r\n" % len(body) + body = body.encode("latin-1") + to_send = b"GET / HTTP/1.1\r\nContent-Length: %d\r\n\r\n" % len(body) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") line, headers, response_body = read_http(fp) self.assertline(line, "200", "OK", "HTTP/1.1") expected = b"" + for chunk in chunks(body, 10): - expected += tobytes( - "%s\r\n%s\r\n" % (str(hex(len(chunk))[2:].upper()), chunk) + expected += b"%s\r\n%s\r\n" % ( + hex(len(chunk))[2:].upper().encode("latin-1"), + chunk, ) expected += b"0\r\n\r\n" self.assertEqual(response_body, expected) @@ -726,17 +726,16 @@ class NoContentLengthTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_http11_list(self): - body = string.ascii_letters - to_send = "GET /list HTTP/1.1\r\nContent-Length: %d\r\n\r\n" % len(body) + body = string.ascii_letters.encode("latin-1") + to_send = b"GET /list HTTP/1.1\r\nContent-Length: %d\r\n\r\n" % len(body) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) line, headers, response_body = read_http(fp) self.assertline(line, "200", "OK", "HTTP/1.1") self.assertEqual(headers["content-length"], str(len(body))) - self.assertEqual(response_body, tobytes(body)) + self.assertEqual(response_body, body) # remote keeps connection open because it divined the content length # from a length-1 list self.sock.send(to_send) @@ -744,19 +743,20 @@ class NoContentLengthTests: self.assertline(line, "200", "OK", "HTTP/1.1") def test_http11_listlentwo(self): - body = string.ascii_letters - to_send = "GET /list_lentwo HTTP/1.1\r\nContent-Length: %s\r\n\r\n" % len(body) + body = string.ascii_letters.encode("latin-1") + to_send = b"GET /list_lentwo HTTP/1.1\r\nContent-Length: %d\r\n\r\n" % len(body) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") line, headers, response_body = read_http(fp) self.assertline(line, "200", "OK", "HTTP/1.1") expected = b"" - for chunk in (body[0], body[1:]): - expected += tobytes( - "%s\r\n%s\r\n" % (str(hex(len(chunk))[2:].upper()), chunk) + + for chunk in (body[:1], body[1:]): + expected += b"%s\r\n%s\r\n" % ( + (hex(len(chunk))[2:].upper().encode("latin-1")), + chunk, ) expected += b"0\r\n\r\n" self.assertEqual(response_body, expected) @@ -777,11 +777,11 @@ class WriteCallbackTests: def test_short_body(self): # check to see if server closes connection when body is too short # for cl header - to_send = tobytes( - "GET /short_body HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /short_body HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -793,7 +793,7 @@ class WriteCallbackTests: self.assertEqual(cl, 9) self.assertNotEqual(cl, len(response_body)) self.assertEqual(len(response_body), cl - 1) - self.assertEqual(response_body, tobytes("abcdefgh")) + self.assertEqual(response_body, b"abcdefgh") # remote closed connection (despite keepalive header) self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -801,11 +801,11 @@ class WriteCallbackTests: def test_long_body(self): # check server doesnt close connection when body is too long # for cl header - to_send = tobytes( - "GET /long_body HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /long_body HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -814,7 +814,7 @@ class WriteCallbackTests: content_length = int(headers.get("content-length")) or None self.assertEqual(content_length, 9) self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes("abcdefghi")) + self.assertEqual(response_body, b"abcdefghi") # remote does not close connection (keepalive header) self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -824,11 +824,11 @@ class WriteCallbackTests: def test_equal_body(self): # check server doesnt close connection when body is equal to # cl header - to_send = tobytes( - "GET /equal_body HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /equal_body HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -838,7 +838,7 @@ class WriteCallbackTests: self.assertEqual(content_length, 9) self.assertline(line, "200", "OK", "HTTP/1.0") self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes("abcdefghi")) + self.assertEqual(response_body, b"abcdefghi") # remote does not close connection (keepalive header) self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -847,11 +847,11 @@ class WriteCallbackTests: def test_no_content_length(self): # wtf happens when there's no content-length - to_send = tobytes( - "GET /no_content_length HTTP/1.0\r\n" - "Connection: Keep-Alive\r\n" - "Content-Length: 0\r\n" - "\r\n" + to_send = ( + b"GET /no_content_length HTTP/1.0\r\n" + b"Connection: Keep-Alive\r\n" + b"Content-Length: 0\r\n" + b"\r\n" ) self.connect() self.sock.send(to_send) @@ -860,7 +860,7 @@ class WriteCallbackTests: line, headers, response_body = read_http(fp) content_length = headers.get("content-length") self.assertEqual(content_length, None) - self.assertEqual(response_body, tobytes("abcdefghi")) + self.assertEqual(response_body, b"abcdefghi") # remote closed connection (despite keepalive header) self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -881,10 +881,9 @@ class TooLargeTests: self.stop_subprocess() def test_request_body_too_large_with_wrong_cl_http10(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") @@ -900,12 +899,11 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_wrong_cl_http10_keepalive(self): - body = "a" * self.toobig + body = b"a" * self.toobig to_send = ( - "GET / HTTP/1.0\r\nContent-Length: 5\r\nConnection: Keep-Alive\r\n\r\n" + b"GET / HTTP/1.0\r\nContent-Length: 5\r\nConnection: Keep-Alive\r\n\r\n" ) to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") @@ -923,10 +921,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_no_cl_http10(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.0\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.0\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -939,10 +936,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_no_cl_http10_keepalive(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.0\r\nConnection: Keep-Alive\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.0\r\nConnection: Keep-Alive\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -962,10 +958,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_wrong_cl_http11(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") @@ -984,10 +979,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_wrong_cl_http11_connclose(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.1\r\nContent-Length: 5\r\nConnection: close\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.1\r\nContent-Length: 5\r\nConnection: close\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1001,10 +995,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_no_cl_http11(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.1\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.1\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb") @@ -1026,10 +1019,9 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_with_no_cl_http11_connclose(self): - body = "a" * self.toobig - to_send = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n" + body = b"a" * self.toobig + to_send = b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n" to_send += body - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1043,12 +1035,11 @@ class TooLargeTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_request_body_too_large_chunked_encoding(self): - control_line = "20;\r\n" # 20 hex = 32 dec - s = "This string has 32 characters.\r\n" - to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + control_line = b"20;\r\n" # 20 hex = 32 dec + s = b"This string has 32 characters.\r\n" + to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" repeat = control_line + s to_send += repeat * ((self.toobig // len(repeat)) + 1) - to_send = tobytes(to_send) self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1073,8 +1064,7 @@ class InternalServerErrorTests: self.stop_subprocess() def test_before_start_response_http_10(self): - to_send = "GET /before_start_response HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /before_start_response HTTP/1.0\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1089,8 +1079,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_before_start_response_http_11(self): - to_send = "GET /before_start_response HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /before_start_response HTTP/1.1\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1108,9 +1097,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_before_start_response_http_11_close(self): - to_send = tobytes( - "GET /before_start_response HTTP/1.1\r\nConnection: close\r\n\r\n" - ) + to_send = b"GET /before_start_response HTTP/1.1\r\nConnection: close\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1129,8 +1116,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_after_start_response_http10(self): - to_send = "GET /after_start_response HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /after_start_response HTTP/1.0\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1149,8 +1135,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_after_start_response_http11(self): - to_send = "GET /after_start_response HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /after_start_response HTTP/1.1\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1168,9 +1153,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_after_start_response_http11_close(self): - to_send = tobytes( - "GET /after_start_response HTTP/1.1\r\nConnection: close\r\n\r\n" - ) + to_send = b"GET /after_start_response HTTP/1.1\r\nConnection: close\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1189,8 +1172,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_after_write_cb(self): - to_send = "GET /after_write_cb HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /after_write_cb HTTP/1.1\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1202,8 +1184,7 @@ class InternalServerErrorTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_in_generator(self): - to_send = "GET /in_generator HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /in_generator HTTP/1.1\r\n\r\n" self.connect() self.sock.send(to_send) fp = self.sock.makefile("rb", 0) @@ -1225,8 +1206,7 @@ class FileWrapperTests: self.stop_subprocess() def test_filelike_http11(self): - to_send = "GET /filelike HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike HTTP/1.1\r\n\r\n" self.connect() @@ -1243,8 +1223,7 @@ class FileWrapperTests: # connection has not been closed def test_filelike_nocl_http11(self): - to_send = "GET /filelike_nocl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike_nocl HTTP/1.1\r\n\r\n" self.connect() @@ -1261,8 +1240,7 @@ class FileWrapperTests: # connection has not been closed def test_filelike_shortcl_http11(self): - to_send = "GET /filelike_shortcl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike_shortcl HTTP/1.1\r\n\r\n" self.connect() @@ -1280,8 +1258,7 @@ class FileWrapperTests: # connection has not been closed def test_filelike_longcl_http11(self): - to_send = "GET /filelike_longcl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike_longcl HTTP/1.1\r\n\r\n" self.connect() @@ -1298,8 +1275,7 @@ class FileWrapperTests: # connection has not been closed def test_notfilelike_http11(self): - to_send = "GET /notfilelike HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike HTTP/1.1\r\n\r\n" self.connect() @@ -1316,8 +1292,7 @@ class FileWrapperTests: # connection has not been closed def test_notfilelike_iobase_http11(self): - to_send = "GET /notfilelike_iobase HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike_iobase HTTP/1.1\r\n\r\n" self.connect() @@ -1334,8 +1309,7 @@ class FileWrapperTests: # connection has not been closed def test_notfilelike_nocl_http11(self): - to_send = "GET /notfilelike_nocl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike_nocl HTTP/1.1\r\n\r\n" self.connect() @@ -1351,8 +1325,7 @@ class FileWrapperTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_notfilelike_shortcl_http11(self): - to_send = "GET /notfilelike_shortcl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike_shortcl HTTP/1.1\r\n\r\n" self.connect() @@ -1370,8 +1343,7 @@ class FileWrapperTests: # connection has not been closed def test_notfilelike_longcl_http11(self): - to_send = "GET /notfilelike_longcl HTTP/1.1\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike_longcl HTTP/1.1\r\n\r\n" self.connect() @@ -1389,8 +1361,7 @@ class FileWrapperTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_filelike_http10(self): - to_send = "GET /filelike HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike HTTP/1.0\r\n\r\n" self.connect() @@ -1408,8 +1379,7 @@ class FileWrapperTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_filelike_nocl_http10(self): - to_send = "GET /filelike_nocl HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /filelike_nocl HTTP/1.0\r\n\r\n" self.connect() @@ -1427,8 +1397,7 @@ class FileWrapperTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_notfilelike_http10(self): - to_send = "GET /notfilelike HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike HTTP/1.0\r\n\r\n" self.connect() @@ -1446,8 +1415,7 @@ class FileWrapperTests: self.assertRaises(ConnectionClosed, read_http, fp) def test_notfilelike_nocl_http10(self): - to_send = "GET /notfilelike_nocl HTTP/1.0\r\n\r\n" - to_send = tobytes(to_send) + to_send = b"GET /notfilelike_nocl HTTP/1.0\r\n\r\n" self.connect() @@ -1571,13 +1539,16 @@ def parse_headers(fp): """Parses only RFC2822 headers from a file pointer. """ headers = {} + while True: line = fp.readline() + if line in (b"\r\n", b"\n", b""): break line = line.decode("iso-8859-1") name, value = line.strip().split(":", 1) headers[name.lower().strip()] = value.lower().strip() + return headers @@ -1606,22 +1577,28 @@ def read_http(fp): # pragma: no cover except OSError as exc: fp.close() # errno 104 is ENOTRECOVERABLE, In WinSock 10054 is ECONNRESET + if get_errno(exc) in (errno.ECONNABORTED, errno.ECONNRESET, 104, 10054): raise ConnectionClosed raise + if not response_line: raise ConnectionClosed header_lines = [] + while True: line = fp.readline() + if line in (b"\r\n", b"\r\n", b""): break else: header_lines.append(line) headers = dict() + for x in header_lines: x = x.strip() + if not x: continue key, value = x.split(b": ", 1) @@ -1634,8 +1611,10 @@ def read_http(fp): # pragma: no cover num = int(headers["content-length"]) body = b"" left = num + while left > 0: data = fp.read(left) + if not data: break body += data @@ -1671,5 +1650,6 @@ def get_errno(exc): # pragma: no cover def chunks(l, n): """ Yield successive n-sized chunks from l. """ + for i in range(0, len(l), n): yield l[i : i + n] diff --git a/tests/test_parser.py b/tests/test_parser.py index e0e4d25..eace4af 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -15,13 +15,26 @@ """ import unittest -from waitress.compat import text_, tobytes +from waitress.adjustments import Adjustments +from waitress.parser import ( + HTTPRequestParser, + ParsingError, + TransferEncodingNotImplemented, + crack_first_line, + get_header_lines, + split_uri, + unquote_bytes_to_wsgi, +) +from waitress.utilities import ( + BadRequest, + RequestEntityTooLarge, + RequestHeaderFieldsTooLarge, + ServerNotImplemented, +) class TestHTTPRequestParser(unittest.TestCase): def setUp(self): - from waitress.adjustments import Adjustments - from waitress.parser import HTTPRequestParser my_adj = Adjustments() self.parser = HTTPRequestParser(my_adj) @@ -45,8 +58,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers, {}) def test_received_bad_host_header(self): - from waitress.utilities import BadRequest - data = b"HTTP/1.0 GET /foobar\r\n Host: foo\r\n\r\n" result = self.parser.received(data) self.assertEqual(result, 36) @@ -54,8 +65,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.error.__class__, BadRequest) def test_received_bad_transfer_encoding(self): - from waitress.utilities import ServerNotImplemented - data = ( b"GET /foobar HTTP/1.1\r\n" b"Transfer-Encoding: foo\r\n" @@ -89,7 +98,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(result, 0) def test_received_cl_too_large(self): - from waitress.utilities import RequestEntityTooLarge self.parser.adj.max_request_body_size = 2 data = b"GET /foobar HTTP/8.4\r\nContent-Length: 10\r\n\r\n" @@ -99,7 +107,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(isinstance(self.parser.error, RequestEntityTooLarge)) def test_received_headers_too_large(self): - from waitress.utilities import RequestHeaderFieldsTooLarge self.parser.adj.max_request_header_size = 2 data = b"GET /foobar HTTP/8.4\r\nX-Foo: 1\r\n\r\n" @@ -109,8 +116,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(isinstance(self.parser.error, RequestHeaderFieldsTooLarge)) def test_received_body_too_large(self): - from waitress.utilities import RequestEntityTooLarge - self.parser.adj.max_request_body_size = 2 data = ( b"GET /foobar HTTP/1.1\r\n" @@ -129,8 +134,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(isinstance(self.parser.error, RequestEntityTooLarge)) def test_received_error_from_parser(self): - from waitress.utilities import BadRequest - data = ( b"GET /foobar HTTP/1.1\r\n" b"Transfer-Encoding: chunked\r\n" @@ -171,8 +174,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers["FOO"], "bar") def test_parse_header_no_cr_in_headerplus(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4" try: @@ -183,8 +184,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_bad_content_length(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\r\ncontent-length: abc\r\n" try: @@ -195,8 +194,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_multiple_content_length(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\r\ncontent-length: 10\r\ncontent-length: 20\r\n" try: @@ -213,8 +210,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.body_rcv.__class__.__name__, "ChunkedReceiver") def test_parse_header_transfer_encoding_invalid(self): - from waitress.parser import TransferEncodingNotImplemented - data = b"GET /foobar HTTP/1.1\r\ntransfer-encoding: gzip\r\n" try: @@ -225,7 +220,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_transfer_encoding_invalid_multiple(self): - from waitress.parser import TransferEncodingNotImplemented data = b"GET /foobar HTTP/1.1\r\ntransfer-encoding: gzip\r\ntransfer-encoding: chunked\r\n" @@ -237,8 +231,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_transfer_encoding_invalid_whitespace(self): - from waitress.parser import TransferEncodingNotImplemented - data = b"GET /foobar HTTP/1.1\r\nTransfer-Encoding:\x85chunked\r\n" try: @@ -249,8 +241,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_transfer_encoding_invalid_unicode(self): - from waitress.parser import TransferEncodingNotImplemented - # This is the binary encoding for the UTF-8 character # https://www.compart.com/en/unicode/U+212A "unicode character "K"" # which if waitress were to accidentally do the wrong thing get @@ -286,8 +276,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.parser.close() # doesn't raise def test_parse_header_lf_only(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\nfoo: bar" try: @@ -298,8 +286,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_cr_only(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\rfoo: bar" try: self.parser.parse_header(data) @@ -309,8 +295,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_extra_lf_in_header(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\r\nfoo: \nbar\r\n" try: self.parser.parse_header(data) @@ -320,8 +304,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_extra_lf_in_first_line(self): - from waitress.parser import ParsingError - data = b"GET /foobar\n HTTP/8.4\r\n" try: self.parser.parse_header(data) @@ -331,8 +313,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_invalid_whitespace(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/8.4\r\nfoo : bar\r\n" try: self.parser.parse_header(data) @@ -342,8 +322,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_invalid_whitespace_vtab(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo:\x0bbar\r\n" try: self.parser.parse_header(data) @@ -353,8 +331,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_invalid_no_colon(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\nnotvalid\r\n" try: self.parser.parse_header(data) @@ -364,8 +340,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_invalid_folding_spacing(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\n\t\x0bbaz\r\n" try: self.parser.parse_header(data) @@ -375,8 +349,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_invalid_chars(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\nfoo: \x0bbaz\r\n" try: self.parser.parse_header(data) @@ -386,8 +358,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_empty(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\nempty:\r\n" self.parser.parse_header(data) @@ -397,8 +367,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers["FOO"], "bar") def test_parse_header_multiple_values(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar, whatever, more, please, yes\r\n" self.parser.parse_header(data) @@ -406,8 +374,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers["FOO"], "bar, whatever, more, please, yes") def test_parse_header_multiple_values_header_folded(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar, whatever,\r\n more, please, yes\r\n" self.parser.parse_header(data) @@ -415,8 +381,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers["FOO"], "bar, whatever, more, please, yes") def test_parse_header_multiple_values_header_folded_multiple(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar, whatever,\r\n more\r\nfoo: please, yes\r\n" self.parser.parse_header(data) @@ -425,8 +389,6 @@ class TestHTTPRequestParser(unittest.TestCase): def test_parse_header_multiple_values_extra_space(self): # Tests errata from: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: abrowser/0.001 (C O M M E N T)\r\n" self.parser.parse_header(data) @@ -434,8 +396,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertEqual(self.parser.headers["FOO"], "abrowser/0.001 (C O M M E N T)") def test_parse_header_invalid_backtrack_bad(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\nfoo: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x10\r\n" try: self.parser.parse_header(data) @@ -445,8 +405,6 @@ class TestHTTPRequestParser(unittest.TestCase): self.assertTrue(False) def test_parse_header_short_values(self): - from waitress.parser import ParsingError - data = b"GET /foobar HTTP/1.1\r\none: 1\r\ntwo: 22\r\n" self.parser.parse_header(data) @@ -458,8 +416,6 @@ class TestHTTPRequestParser(unittest.TestCase): class Test_split_uri(unittest.TestCase): def _callFUT(self, uri): - from waitress.parser import split_uri - ( self.proxy_scheme, self.proxy_netloc, @@ -499,7 +455,6 @@ class Test_split_uri(unittest.TestCase): def test_split_uri_unicode_error_raises_parsing_error(self): # See https://github.com/Pylons/waitress/issues/64 - from waitress.parser import ParsingError # Either pass or throw a ParsingError, just don't throw another type of # exception as that will cause the connection to close badly: @@ -535,8 +490,6 @@ class Test_split_uri(unittest.TestCase): class Test_get_header_lines(unittest.TestCase): def _callFUT(self, data): - from waitress.parser import get_header_lines - return get_header_lines(data) def test_get_header_lines(self): @@ -561,15 +514,11 @@ class Test_get_header_lines(unittest.TestCase): def test_get_header_lines_malformed(self): # https://corte.si/posts/code/pathod/pythonservers/index.html - from waitress.parser import ParsingError - self.assertRaises(ParsingError, self._callFUT, b" Host: localhost\r\n\r\n") class Test_crack_first_line(unittest.TestCase): def _callFUT(self, line): - from waitress.parser import crack_first_line - return crack_first_line(line) def test_crack_first_line_matchok(self): @@ -577,8 +526,6 @@ class Test_crack_first_line(unittest.TestCase): self.assertEqual(result, (b"GET", b"/", b"1.0")) def test_crack_first_line_lowercase_method(self): - from waitress.parser import ParsingError - self.assertRaises(ParsingError, self._callFUT, b"get / HTTP/1.0") def test_crack_first_line_nomatch(self): @@ -595,9 +542,6 @@ class Test_crack_first_line(unittest.TestCase): class TestHTTPRequestParserIntegration(unittest.TestCase): def setUp(self): - from waitress.adjustments import Adjustments - from waitress.parser import HTTPRequestParser - my_adj = Adjustments() self.parser = HTTPRequestParser(my_adj) @@ -657,8 +601,8 @@ class TestHTTPRequestParserIntegration(unittest.TestCase): ) # path should be utf-8 encoded self.assertEqual( - tobytes(parser.path).decode("utf-8"), - text_(b"/foo/a++/\xc3\xa4=&a:int", "utf-8"), + parser.path.encode("latin-1").decode("utf-8"), + b"/foo/a++/\xc3\xa4=&a:int".decode("utf-8"), ) self.assertEqual( parser.query, "d=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6" @@ -721,6 +665,18 @@ class TestHTTPRequestParserIntegration(unittest.TestCase): self.assertEqual(self.parser.headers, {"CONTENT_LENGTH": "6",}) +class Test_unquote_bytes_to_wsgi(unittest.TestCase): + def _callFUT(self, v): + + return unquote_bytes_to_wsgi(v) + + def test_highorder(self): + val = b"/a%C5%9B" + result = self._callFUT(val) + # PEP 3333 urlunquoted-latin1-decoded-bytes + self.assertEqual(result, "/aÅ\x9b") + + class DummyBodyStream: def getfile(self): return self diff --git a/tests/test_proxy_headers.py b/tests/test_proxy_headers.py index 1aea477..e6f0ed6 100644 --- a/tests/test_proxy_headers.py +++ b/tests/test_proxy_headers.py @@ -1,7 +1,5 @@ import unittest -from waitress.compat import tobytes - class TestProxyHeadersMiddleware(unittest.TestCase): def _makeOne(self, app, **kw): @@ -18,7 +16,7 @@ class TestProxyHeadersMiddleware(unittest.TestCase): response.headers = response_headers response.steps = list(app(environ, start_response)) - response.body = b"".join(tobytes(s) for s in response.steps) + response.body = b"".join(s.encode("latin-1") for s in response.steps) return response def test_get_environment_values_w_scheme_override_untrusted(self): diff --git a/tests/test_runner.py b/tests/test_runner.py index e77d103..2ac302c 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -181,9 +181,9 @@ class Test_helper(unittest.TestCase): @contextlib.contextmanager def capture(): - from waitress.compat import NativeIO + from io import StringIO - fd = NativeIO() + fd = StringIO() sys.stdout = fd sys.stderr = fd yield fd diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py index a7e2878..970e993 100644 --- a/tests/test_wasyncore.py +++ b/tests/test_wasyncore.py @@ -1,3 +1,4 @@ +import _thread as thread import contextlib import errno import functools @@ -23,6 +24,7 @@ HOSTv4 = "127.0.0.1" HOSTv6 = "::1" # Filename used for testing + if os.name == "java": # pragma: no cover # Jython disallows @ in module names TESTFN = "$test" @@ -66,6 +68,7 @@ def _filterwarnings(filters, quiet=False): # pragma: no cover # in order to re-raise the warnings. frame = sys._getframe(2) registry = frame.f_globals.get("__warningregistry__") + if registry: registry.clear() with warnings.catch_warnings(record=True) as w: @@ -77,19 +80,25 @@ def _filterwarnings(filters, quiet=False): # pragma: no cover # Filter the recorded warnings reraise = list(w) missing = [] + for msg, cat in filters: seen = False + for w in reraise[:]: warning = w.message # Filter out the matching messages + if re.match(msg, str(warning), re.I) and issubclass(warning.__class__, cat): seen = True reraise.remove(w) + if not seen and not quiet: # This filter caught nothing missing.append((msg, cat.__name__)) + if reraise: raise AssertionError("unhandled warning %s" % reraise[0]) + if missing: raise AssertionError("filter (%r, %s) did not catch any warning" % missing[0]) @@ -110,11 +119,14 @@ def check_warnings(*filters, **kwargs): # pragma: no cover check_warnings(("", Warning), quiet=True) """ quiet = kwargs.get("quiet") + if not filters: filters = (("", Warning),) # Preserve backward compatibility + if quiet is None: quiet = True + return _filterwarnings(filters, quiet) @@ -129,6 +141,7 @@ def gc_collect(): # pragma: no cover objects to disappear. """ gc.collect() + if sys.platform.startswith("java"): time.sleep(0.1) gc.collect() @@ -136,7 +149,7 @@ def gc_collect(): # pragma: no cover def threading_setup(): # pragma: no cover - return (compat.thread._count(), None) + return (thread._count(), None) def threading_cleanup(*original_values): # pragma: no cover @@ -145,7 +158,8 @@ def threading_cleanup(*original_values): # pragma: no cover _MAX_COUNT = 100 for count in range(_MAX_COUNT): - values = (compat.thread._count(), None) + values = (thread._count(), None) + if values == original_values: break @@ -185,6 +199,7 @@ def join_thread(thread, timeout=30.0): # pragma: no cover after timeout seconds. """ thread.join(timeout) + if thread.is_alive(): msg = "failed to join the thread in %.1f seconds" % timeout raise AssertionError(msg) @@ -212,6 +227,7 @@ def bind_port(sock, host=HOST): # pragma: no cover "tests should never set the SO_REUSEADDR " "socket option on TCP/IP sockets!" ) + if hasattr(socket, "SO_REUSEPORT"): try: if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1: @@ -224,11 +240,13 @@ def bind_port(sock, host=HOST): # pragma: no cover # thus defining SO_REUSEPORT but this process is running # under an older kernel that does not support SO_REUSEPORT. pass + if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) sock.bind((host, 0)) port = sock.getsockname()[1] + return port @@ -302,13 +320,16 @@ def capture_server(evt, buf, serv): # pragma no cover else: n = 200 start = time.time() + while n > 0 and time.time() - start < 3.0: r, w, e = select.select([conn], [], [], 0.1) + if r: n -= 1 data = conn.recv(10) # keep everything except for the newline terminator buf.write(data.replace(b"\n", b"")) + if b"\n" in data: break time.sleep(0.01) @@ -331,6 +352,7 @@ def bind_unix_socket(sock, addr): # pragma: no cover def bind_af_aware(sock, addr): """Helper function to bind a socket according to its family.""" + if HAS_UNIX_SOCKETS and sock.family == socket.AF_UNIX: # Make sure the path doesn't exist. unlink(addr) @@ -345,6 +367,7 @@ if sys.platform.startswith("win"): # pragma: no cover # Perform the operation func(pathname) # Now setup the wait loop + if waitall: dirname = pathname else: @@ -357,6 +380,7 @@ if sys.platform.startswith("win"): # pragma: no cover # Testing on an i7@4.3GHz shows that usually only 1 iteration is # required when contention occurs. timeout = 0.001 + while timeout < 1.0: # Note we are only testing for the existence of the file(s) in # the contents of the directory regardless of any security or @@ -366,6 +390,7 @@ if sys.platform.startswith("win"): # pragma: no cover # Other Windows APIs can fail or give incorrect results when # dealing with files that are pending deletion. L = os.listdir(dirname) + if not (L if waitall else name in L): return # Increase the timeout and try again @@ -394,17 +419,20 @@ def unlink(filename): def _is_ipv6_enabled(): # pragma: no cover """Check whether IPv6 is enabled on this host.""" + if compat.HAS_IPV6: sock = None try: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock.bind(("::1", 0)) + return True except OSError: pass finally: if sock: sock.close() + return False @@ -486,6 +514,7 @@ class HelperFunctionTests(unittest.TestCase): # Only the attribute modified by the routine we expect to be # called should be True. + for attr in attributes: self.assertEqual(getattr(tobj, attr), attr == expectedattr) @@ -512,6 +541,7 @@ class HelperFunctionTests(unittest.TestCase): l = [] testmap = {} + for i in range(10): c = dummychannel() l.append(c) @@ -605,6 +635,7 @@ class DispatcherTests(unittest.TestCase): def test_strerror(self): # refers to bug #8573 err = asyncore._strerror(errno.EPERM) + if hasattr(os, "strerror"): self.assertEqual(err, os.strerror(errno.EPERM)) err = asyncore._strerror(-1) @@ -655,6 +686,7 @@ class DispatcherWithSendTests(unittest.TestCase): d.send(b"\n") n = 1000 + while d.out_buffer and n > 0: # pragma: no cover asyncore.poll() n -= 1 @@ -722,6 +754,7 @@ class FileWrapperTest(unittest.TestCase): def test_resource_warning(self): # Issue #11453 got_warning = False + while got_warning is False: # we try until we get the outcome we want because this # test is not deterministic (gc_collect() may not @@ -818,8 +851,10 @@ class BaseTestAPI: def loop_waiting_for_flag(self, instance, timeout=5): # pragma: no cover timeout = float(timeout) / 100 count = 100 + while asyncore.socket_map and count > 0: asyncore.loop(timeout=0.01, count=1, use_poll=self.use_poll) + if instance.flag: return count -= 1 @@ -965,6 +1000,7 @@ class BaseTestAPI: # Make sure handle_expt is called on OOB data received. # Note: this might fail on some platforms as OOB data is # tenuously supported and rarely used. + if HAS_UNIX_SOCKETS and self.family == socket.AF_UNIX: self.skipTest("Not applicable to AF_UNIX sockets.") @@ -979,7 +1015,7 @@ class BaseTestAPI: class TestHandler(BaseTestHandler): def __init__(self, conn): BaseTestHandler.__init__(self, conn) - self.socket.send(compat.tobytes(chr(244)), socket.MSG_OOB) + self.socket.send(chr(244).encode("latin-1"), socket.MSG_OOB) server = BaseServer(self.family, self.addr, TestHandler) client = TestClient(self.family, server.address) @@ -1081,6 +1117,7 @@ class BaseTestAPI: @reap_threads def test_quick_connect(self): # pragma: no cover # see: http://bugs.python.org/issue10340 + if self.family not in (socket.AF_INET, getattr(socket, "AF_INET6", object())): self.skipTest("test specific to AF_INET and AF_INET6") @@ -1692,16 +1729,19 @@ class DummyDispatcher: def handle_read_event(self): self.read_event_handled = True + if self.exc is not None: raise self.exc def handle_write_event(self): self.write_event_handled = True + if self.exc is not None: raise self.exc def handle_expt_event(self): self.expt_event_handled = True + if self.exc is not None: raise self.exc @@ -1740,6 +1780,7 @@ class DummySelect: def select(self, *arg): self.selected.append(arg) + if self.exc is not None: raise self.exc @@ -1754,6 +1795,7 @@ class DummyPollster: def poll(self, timeout): self.polled.append(timeout) + if self.exc is not None: raise self.exc else: # pragma: no cover -- cgit v1.2.1 From 17aca847a69a455125842db3a6278abfdb90449d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 00:07:53 -0700 Subject: Remove universal wheel --- setup.cfg | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7fd9527..894d563 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,13 +59,10 @@ docs = docutils pylons-sphinx-themes>=1.0.9 -[bdist_wheel] -universal = 1 - [tool:pytest] python_files = test_*.py # For the benefit of test_wasyncore.py python_classes = Test_* testpaths = tests -addopts = -W always --cov +addopts = --cov -W always -- cgit v1.2.1 From bdb0f369733618222ec85bbc6e14ca0f910d12f9 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 00:08:51 -0700 Subject: Add ENOTCONN as a valid error This may happen on macOS --- tests/test_functional.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_functional.py b/tests/test_functional.py index 968fbe6..c99876d 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1501,7 +1501,8 @@ if hasattr(socket, "AF_UNIX"): try: self.sock.send(to_send) except OSError as exc: - self.assertEqual(get_errno(exc), errno.EPIPE) + valid_errors = {errno.EPIPE, errno.ENOTCONN} + self.assertIn(get_errno(exc), valid_errors) class UnixEchoTests(EchoTests, UnixTests, unittest.TestCase): pass -- cgit v1.2.1 From d5b17e1912cc4df7c8f6ab4daf78d72ab4c01de1 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 00:24:19 -0700 Subject: Fixup deprecation warnings --- tests/test_adjustments.py | 3 +++ tests/test_runner.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 6347be3..6aafaa8 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -404,6 +404,9 @@ class TestCLI(unittest.TestCase): return Adjustments.parse_args(argv) + def assertDictContainsSubset(self, subset, dictionary): + self.assertTrue(set(subset.items()) <= set(dictionary.items())) + def test_noargs(self): opts, args = self.parse([]) self.assertDictEqual(opts, {"call": False, "help": False}) diff --git a/tests/test_runner.py b/tests/test_runner.py index 2ac302c..4cf6f6f 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -12,17 +12,17 @@ from waitress import runner class Test_match(unittest.TestCase): def test_empty(self): - self.assertRaisesRegexp( + self.assertRaisesRegex( ValueError, "^Malformed application ''$", runner.match, "" ) def test_module_only(self): - self.assertRaisesRegexp( + self.assertRaisesRegex( ValueError, r"^Malformed application 'foo\.bar'$", runner.match, "foo.bar" ) def test_bad_module(self): - self.assertRaisesRegexp( + self.assertRaisesRegex( ValueError, r"^Malformed application 'foo#bar:barney'$", runner.match, @@ -42,7 +42,7 @@ class Test_resolve(unittest.TestCase): ) def test_nonexistent_function(self): - self.assertRaisesRegexp( + self.assertRaisesRegex( AttributeError, r"has no attribute 'nonexistent_function'", runner.resolve, @@ -57,7 +57,7 @@ class Test_resolve(unittest.TestCase): def test_complex_happy_path(self): # Ensure we can recursively resolve object attributes if necessary. - self.assertEquals(runner.resolve("os.path", "exists.__name__"), "exists") + self.assertEqual(runner.resolve("os.path", "exists.__name__"), "exists") class Test_run(unittest.TestCase): @@ -65,7 +65,7 @@ class Test_run(unittest.TestCase): argv = ["waitress-serve"] + argv with capture() as captured: self.assertEqual(runner.run(argv=argv), code) - self.assertRegexpMatches(captured.getvalue(), regex) + self.assertRegex(captured.getvalue(), regex) captured.close() def test_bad(self): @@ -162,7 +162,7 @@ class Test_helper(unittest.TestCase): raise ImportError("My reason") except ImportError: self.assertEqual(show_exception(sys.stderr), None) - self.assertRegexpMatches(captured.getvalue(), regex) + self.assertRegex(captured.getvalue(), regex) captured.close() regex = ( @@ -175,7 +175,7 @@ class Test_helper(unittest.TestCase): raise ImportError except ImportError: self.assertEqual(show_exception(sys.stderr), None) - self.assertRegexpMatches(captured.getvalue(), regex) + self.assertRegex(captured.getvalue(), regex) captured.close() -- cgit v1.2.1 From 15dc1e82356fd0b0dc763480d42d6542261f28e6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 01:52:29 -0700 Subject: Remove useless try/finally wrapper This looks to be an artifact that was left behind from the original codebase Waitress was forked from and no longer servces a purpose. --- src/waitress/task.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/waitress/task.py b/src/waitress/task.py index 779f01e..1bcc540 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -165,16 +165,13 @@ class Task: def service(self): try: - try: - self.start() - self.execute() - self.finish() - except OSError: - self.close_on_finish = True - if self.channel.adj.log_socket_errors: - raise - finally: - pass + self.start() + self.execute() + self.finish() + except OSError: + self.close_on_finish = True + if self.channel.adj.log_socket_errors: + raise @property def has_body(self): -- cgit v1.2.1 From 4d2b2867933dc2f73c0755e137184fe0ea21f203 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 01:59:25 -0700 Subject: Stop marking socket as readable when flushing data We no longer mark the socket as readable if we are attempting to flush whatever remaining data we have and are trying to shut down the channel. Whatever data is ready to be read, it's no longer our concern. We don't want to spend time reading data we don't care about. --- src/waitress/channel.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/waitress/channel.py b/src/waitress/channel.py index 7332e40..d756b96 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -76,16 +76,20 @@ class HTTPChannel(wasyncore.dispatcher): # if there's data in the out buffer or we've been instructed to close # the channel (possibly by our server maintenance logic), run # handle_write + return self.total_outbufs_len or self.will_close or self.close_when_flushed def handle_write(self): # Precondition: there's data in the out buffer to be sent, or # there's a pending will_close request + if not self.connected: # we dont want to close the channel twice + return # try to flush any pending output + if not self.requests: # 1. There are no running tasks, so we don't need to try to lock # the outbuf before sending @@ -125,10 +129,18 @@ class HTTPChannel(wasyncore.dispatcher): def readable(self): # We might want to create a new task. We can only do this if: # 1. We're not already about to close the connection. - # 2. There's no already currently running task(s). - # 3. There's no data in the output buffer that needs to be sent + # 2. We're not waiting to flush remaining data before closing the + # connection + # 3. There's no already currently running task(s). + # 4. There's no data in the output buffer that needs to be sent # before we potentially create a new task. - return not (self.will_close or self.requests or self.total_outbufs_len) + + return not ( + self.will_close + or self.close_when_flushed + or self.requests + or self.total_outbufs_len + ) def handle_read(self): try: @@ -137,7 +149,9 @@ class HTTPChannel(wasyncore.dispatcher): if self.adj.log_socket_errors: self.logger.exception("Socket error") self.handle_close() + return + if data: self.last_activity = time.time() self.received(data) @@ -158,9 +172,11 @@ class HTTPChannel(wasyncore.dispatcher): if request is None: request = self.parser_class(self.adj) n = request.received(data) + if request.expect_continue and request.headers_finished: # guaranteed by parser to be a 1.1 request request.expect_continue = False + if not self.sent_continue: # there's no current task, so we don't need to try to # lock the outbuf to append to it. @@ -172,14 +188,17 @@ class HTTPChannel(wasyncore.dispatcher): self.sent_continue = True self._flush_some() request.completed = False + if request.completed: # The request (with the body) is ready to use. self.request = None + if not request.empty: requests.append(request) request = None else: self.request = request + if n >= len(data): break data = data[n:] @@ -193,6 +212,7 @@ class HTTPChannel(wasyncore.dispatcher): def _flush_some_if_lockable(self): # Since our task may be appending to the outbuf, we try to acquire # the lock, but we don't block if we can't. + if self.outbuf_lock.acquire(False): try: self._flush_some() @@ -213,9 +233,11 @@ class HTTPChannel(wasyncore.dispatcher): # use outbuf.__len__ rather than len(outbuf) FBO of not getting # OverflowError on 32-bit Python outbuflen = outbuf.__len__() + while outbuflen > 0: chunk = outbuf.get(self.sendbuf_len) num_sent = self.send(chunk) + if num_sent: outbuf.skip(num_sent, True) outbuflen -= num_sent @@ -224,9 +246,11 @@ class HTTPChannel(wasyncore.dispatcher): else: # failed to write anything, break out entirely dobreak = True + break else: # self.outbufs[-1] must always be a writable outbuf + if len(self.outbufs) > 1: toclose = self.outbufs.pop(0) try: @@ -242,6 +266,7 @@ class HTTPChannel(wasyncore.dispatcher): if sent: self.last_activity = time.time() + return True return False @@ -276,6 +301,7 @@ class HTTPChannel(wasyncore.dispatcher): fd = self._fileno # next line sets this to None wasyncore.dispatcher.del_channel(self, map) ac = self.server.active_channels + if fd in ac: del ac[fd] @@ -288,14 +314,17 @@ class HTTPChannel(wasyncore.dispatcher): # if the socket is closed then interrupt the task so that it # can cleanup possibly before the app_iter is exhausted raise ClientDisconnected + if data: # the async mainloop might be popping data off outbuf; we can # block here waiting for it because we're in a task thread with self.outbuf_lock: self._flush_outbufs_below_high_watermark() + if not self.connected: raise ClientDisconnected num_bytes = len(data) + if data.__class__ is ReadOnlyFileBasedBuffer: # they used wsgi.file_wrapper self.outbufs.append(data) @@ -312,13 +341,17 @@ class HTTPChannel(wasyncore.dispatcher): self.outbufs[-1].append(data) self.current_outbuf_count += num_bytes self.total_outbufs_len += num_bytes + if self.total_outbufs_len >= self.adj.send_bytes: self.server.pull_trigger() + return num_bytes + return 0 def _flush_outbufs_below_high_watermark(self): # check first to avoid locking if possible + if self.total_outbufs_len > self.adj.outbuf_high_watermark: with self.outbuf_lock: while ( @@ -333,6 +366,7 @@ class HTTPChannel(wasyncore.dispatcher): with self.task_lock: while self.requests: request = self.requests[0] + if request.error: task = self.error_task_class(self, request) else: @@ -348,6 +382,7 @@ class HTTPChannel(wasyncore.dispatcher): self.logger.exception( "Exception while serving %s" % task.request.path ) + if not task.wrote_header: if self.adj.expose_tracebacks: body = traceback.format_exc() @@ -376,8 +411,10 @@ class HTTPChannel(wasyncore.dispatcher): task.close_on_finish = True # we cannot allow self.requests to drop to empty til # here; otherwise the mainloop gets confused + if task.close_on_finish: self.close_when_flushed = True + for request in self.requests: request.close() self.requests = [] @@ -389,6 +426,7 @@ class HTTPChannel(wasyncore.dispatcher): # that we need to account for, otherwise it'd be better # to do this check at the start of the request instead of # at the end to account for consecutive service() calls + if len(self.requests) > 1: self._flush_outbufs_below_high_watermark() @@ -397,6 +435,7 @@ class HTTPChannel(wasyncore.dispatcher): # outbufs across requests which can cause outbufs to # not be deallocated regularly when a connection is open # for a long time + if self.current_outbuf_count > 0: self.current_outbuf_count = self.adj.outbuf_high_watermark -- cgit v1.2.1 From 0d0163fc78917cbc8bbc9c3cf646c3392d38275a Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 16 Aug 2020 16:55:05 -0700 Subject: Fixup comment, this is a Windows issue --- tests/test_adjustments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 6aafaa8..420ee4c 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -218,7 +218,7 @@ class TestAdjustments(unittest.TestCase): def test_service_port(self): if WIN: # pragma: no cover - # On Windows and Python 2 this is broken, so we raise a ValueError + # On Windows this is broken, so we raise a ValueError self.assertRaises( ValueError, self._makeOne, listen="127.0.0.1:http", ) -- cgit v1.2.1