diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1125da4..c31669a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,9 @@ jobs: max-parallel: 5 matrix: python-version: - - 2.7 - 3.6 - 3.7 - 3.8 - - pypy2 - pypy3 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b9eccd9..45a1ebf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,10 +70,10 @@ other hand, the following are all very welcome: tox ``` - But note that: (1) this will print slightly misleading coverage + But note that: (1) this might print slightly misleading coverage statistics, because it only shows coverage for individual python - versions, and there are some lines that are only executed on python - 2 or only executed on python 3, and (2) the full test suite will + versions, and there might be some lines that are only executed on some + python versions or implementations, and (2) the full test suite will automatically get run when you submit a pull request, so you don't need to worry too much about tracking down a version of cpython 3.3 or whatever just to run the tests. diff --git a/README.rst b/README.rst index 74bb182..f998b01 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,8 @@ library. It has a test suite with 100.0% coverage for both statements and branches. -Currently it supports Python 3 (testing on 3.5-3.8), Python 2.7, and PyPy. +Currently it supports Python 3 (testing on 3.5-3.8) and PyPy 3. +The last Python 2-compatible version was h11 0.11.x. (Originally it had a Cython wrapper for `http-parser `_ and a beautiful nested state machine implemented with ``yield from`` to postprocess the output. But diff --git a/bench/asv.conf.json b/bench/asv.conf.json index f65e4dd..0a07c42 100644 --- a/bench/asv.conf.json +++ b/bench/asv.conf.json @@ -36,7 +36,7 @@ // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. - "pythons": ["2.7", "3.5", "pypy"], + "pythons": ["3.8", "pypy3"], // The matrix of dependencies to test. Each key is the name of a // package (in PyPI) and the values are version numbers. An empty diff --git a/docs/source/index.rst b/docs/source/index.rst index c08b76d..617638f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,7 +44,9 @@ whatever. But h11 makes it much easier to implement something like Vital statistics ---------------- -* Requirements: Python 2.7 or Python 3.5+ (PyPy works great) +* Requirements: Python 3.5+ (PyPy works great) + + The last Python 2-compatible version was h11 0.11.x. * Install: ``pip install h11`` diff --git a/fuzz/afl-server.py b/fuzz/afl-server.py index 450c68b..0ff1947 100644 --- a/fuzz/afl-server.py +++ b/fuzz/afl-server.py @@ -9,11 +9,6 @@ import h11 -if sys.version_info[0] >= 3: - in_file = sys.stdin.detach() -else: - in_file = sys.stdin - def process_all(c): while True: @@ -26,7 +21,7 @@ def process_all(c): afl.init() -data = in_file.read() +data = sys.stdin.detach().read() # one big chunk server1 = h11.Connection(h11.SERVER) diff --git a/h11/_connection.py b/h11/_connection.py index 410c4e9..b6f8760 100644 --- a/h11/_connection.py +++ b/h11/_connection.py @@ -109,7 +109,7 @@ def _body_framing(request_method, event): ################################################################ -class Connection(object): +class Connection: """An object encapsulating the state of an HTTP connection. Args: diff --git a/h11/_events.py b/h11/_events.py index c11d838..1827930 100644 --- a/h11/_events.py +++ b/h11/_events.py @@ -24,7 +24,7 @@ request_target_re = re.compile(request_target.encode("ascii")) -class _EventBundle(object): +class _EventBundle: _fields = [] _defaults = {} @@ -85,9 +85,6 @@ def __repr__(self): def __eq__(self, other): return self.__class__ == other.__class__ and self.__dict__ == other.__dict__ - def __ne__(self, other): - return not self.__eq__(other) - # This is an unhashable type. __hash__ = None diff --git a/h11/_headers.py b/h11/_headers.py index 5229ac4..7ed39bc 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -132,7 +132,7 @@ def normalize_and_validate(headers, _parsed=False): raw_name = name name = name.lower() if name == b"content-length": - lengths = set(length.strip() for length in value.split(b",")) + lengths = {length.strip() for length in value.split(b",")} if len(lengths) != 1: raise LocalProtocolError("conflicting Content-Length headers") value = lengths.pop() diff --git a/h11/_readers.py b/h11/_readers.py index cc86bff..75f00bc 100644 --- a/h11/_readers.py +++ b/h11/_readers.py @@ -54,13 +54,7 @@ def _obsolete_line_fold(lines): def _decode_header_lines(lines): for line in _obsolete_line_fold(lines): - # _obsolete_line_fold yields either bytearray or bytes objects. On - # Python 3, validate() takes either and returns matches as bytes. But - # on Python 2, validate can return matches as bytearrays, so we have - # to explicitly cast back. - matches = validate( - header_field_re, bytes(line), "illegal header line: {!r}", bytes(line) - ) + matches = validate(header_field_re, line, "illegal header line: {!r}", line) yield (matches["field_name"], matches["field_value"]) @@ -127,7 +121,7 @@ def read_eof(self): chunk_header_re = re.compile(chunk_header.encode("ascii")) -class ChunkedReader(object): +class ChunkedReader: def __init__(self): self._bytes_in_chunk = 0 # After reading a chunk, we have to throw away the trailing \r\n; if @@ -163,9 +157,7 @@ def __call__(self, buf): chunk_header, ) # XX FIXME: we discard chunk extensions. Does anyone care? - # We convert to bytes because Python 2's `int()` function doesn't - # work properly on bytearray objects. - self._bytes_in_chunk = int(bytes(matches["chunk_size"]), base=16) + self._bytes_in_chunk = int(matches["chunk_size"], base=16) if self._bytes_in_chunk == 0: self._reading_trailer = True return self(buf) @@ -191,7 +183,7 @@ def read_eof(self): ) -class Http10Reader(object): +class Http10Reader: def __call__(self, buf): data = buf.maybe_extract_at_most(999999999) if data is None: diff --git a/h11/_receivebuffer.py b/h11/_receivebuffer.py index c56749a..8b709df 100644 --- a/h11/_receivebuffer.py +++ b/h11/_receivebuffer.py @@ -1,5 +1,3 @@ -import sys - __all__ = ["ReceiveBuffer"] @@ -38,7 +36,7 @@ # slightly clever thing where we delay calling compress() until we've # processed a whole event, which could in theory be slightly more efficient # than the internal bytearray support.) -class ReceiveBuffer(object): +class ReceiveBuffer: def __init__(self): self._data = bytearray() # These are both absolute offsets into self._data: @@ -53,10 +51,6 @@ def __bool__(self): def __bytes__(self): return bytes(self._data[self._start :]) - if sys.version_info[0] < 3: # version specific: Python 2 - __str__ = __bytes__ - __nonzero__ = __bool__ - def __len__(self): return len(self._data) - self._start diff --git a/h11/_state.py b/h11/_state.py index 70a5e04..0f08a09 100644 --- a/h11/_state.py +++ b/h11/_state.py @@ -197,7 +197,7 @@ } -class ConnectionState(object): +class ConnectionState: def __init__(self): # Extra bits of state that don't quite fit into the state model. diff --git a/h11/_util.py b/h11/_util.py index 0a2c28e..eb1a5cd 100644 --- a/h11/_util.py +++ b/h11/_util.py @@ -1,6 +1,3 @@ -import re -import sys - __all__ = [ "ProtocolError", "LocalProtocolError", @@ -74,34 +71,17 @@ def _reraise_as_remote_protocol_error(self): # (exc_info[0]) separately from the exception object (exc_info[1]), # and we only modified the latter. So we really do need to re-raise # the new type explicitly. - if sys.version_info[0] >= 3: - # On py3, the traceback is part of the exception object, so our - # in-place modification preserved it and we can just re-raise: - raise self - else: - # On py2, preserving the traceback requires 3-argument - # raise... but on py3 this is a syntax error, so we have to hide - # it inside an exec - exec("raise RemoteProtocolError, self, sys.exc_info()[2]") + # On py3, the traceback is part of the exception object, so our + # in-place modification preserved it and we can just re-raise: + raise self class RemoteProtocolError(ProtocolError): pass -try: - _fullmatch = type(re.compile("")).fullmatch -except AttributeError: - - def _fullmatch(regex, data): # version specific: Python < 3.4 - match = regex.match(data) - if match and match.end() != len(data): - match = None - return match - - def validate(regex, data, msg="malformed data", *format_args): - match = _fullmatch(regex, data) + match = regex.fullmatch(data) if not match: if format_args: msg = msg.format(*format_args) diff --git a/h11/_writers.py b/h11/_writers.py index 7531579..cb5e8a8 100644 --- a/h11/_writers.py +++ b/h11/_writers.py @@ -7,32 +7,12 @@ # - a writer # - or, for body writers, a dict of framin-dependent writer factories -import sys - from ._events import Data, EndOfMessage from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER from ._util import LocalProtocolError __all__ = ["WRITERS"] -# Equivalent of bstr % values, that works on python 3.x for x < 5 -if (3, 0) <= sys.version_info < (3, 5): - - def bytesmod(bstr, values): - decoded_values = [] - for value in values: - if isinstance(value, bytes): - decoded_values.append(value.decode("ascii")) - else: - decoded_values.append(value) - return (bstr.decode("ascii") % tuple(decoded_values)).encode("ascii") - - -else: - - def bytesmod(bstr, values): - return bstr % values - def write_headers(headers, write): # "Since the Host field-value is critical information for handling a @@ -41,17 +21,17 @@ def write_headers(headers, write): raw_items = headers._full_items for raw_name, name, value in raw_items: if name == b"host": - write(bytesmod(b"%s: %s\r\n", (raw_name, value))) + write(b"%s: %s\r\n" % (raw_name, value)) for raw_name, name, value in raw_items: if name != b"host": - write(bytesmod(b"%s: %s\r\n", (raw_name, value))) + write(b"%s: %s\r\n" % (raw_name, value)) write(b"\r\n") def write_request(request, write): if request.http_version != b"1.1": raise LocalProtocolError("I only send HTTP/1.1") - write(bytesmod(b"%s %s HTTP/1.1\r\n", (request.method, request.target))) + write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target)) write_headers(request.headers, write) @@ -68,11 +48,11 @@ def write_any_response(response, write): # from stdlib's http.HTTPStatus table. Or maybe just steal their enums # (either by import or copy/paste). We already accept them as status codes # since they're of type IntEnum < int. - write(bytesmod(b"HTTP/1.1 %s %s\r\n", (status_bytes, response.reason))) + write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason)) write_headers(response.headers, write) -class BodyWriter(object): +class BodyWriter: def __call__(self, event, write): if type(event) is Data: self.send_data(event.data, write) @@ -111,7 +91,7 @@ def send_data(self, data, write): # end-of-message. if not data: return - write(bytesmod(b"%x\r\n", (len(data),))) + write(b"%x\r\n" % len(data)) write(data) write(b"\r\n") diff --git a/h11/tests/test_against_stdlib_http.py b/h11/tests/test_against_stdlib_http.py index b4219ff..e6c5db4 100644 --- a/h11/tests/test_against_stdlib_http.py +++ b/h11/tests/test_against_stdlib_http.py @@ -1,26 +1,14 @@ import json import os.path import socket +import socketserver import threading from contextlib import closing, contextmanager +from http.server import SimpleHTTPRequestHandler +from urllib.request import urlopen import h11 -try: - from urllib.request import urlopen -except ImportError: # version specific: Python 2 - from urllib2 import urlopen - -try: - import socketserver -except ImportError: # version specific: Python 2 - import SocketServer as socketserver - -try: - from http.server import SimpleHTTPRequestHandler -except ImportError: # version specific: Python 2 - from SimpleHTTPServer import SimpleHTTPRequestHandler - @contextmanager def socket_server(handler): diff --git a/h11/tests/test_events.py b/h11/tests/test_events.py index 07ffc13..e20f741 100644 --- a/h11/tests/test_events.py +++ b/h11/tests/test_events.py @@ -1,3 +1,5 @@ +from http import HTTPStatus + import pytest from .. import _events @@ -154,10 +156,6 @@ def test_events(): def test_intenum_status_code(): # https://github.com/python-hyper/h11/issues/72 - try: - from http import HTTPStatus - except ImportError: - pytest.skip("Only affects Python 3") r = Response(status_code=HTTPStatus.OK, headers=[], http_version="1.0") assert r.status_code == HTTPStatus.OK diff --git a/h11/tests/test_util.py b/h11/tests/test_util.py index 74ab33b..d851bdc 100644 --- a/h11/tests/test_util.py +++ b/h11/tests/test_util.py @@ -93,7 +93,7 @@ def test_bytesify(): assert bytesify("123") == b"123" with pytest.raises(UnicodeEncodeError): - bytesify(u"\u1234") + bytesify("\u1234") with pytest.raises(TypeError): bytesify(10) diff --git a/newsfragments/114.removal.rst b/newsfragments/114.removal.rst new file mode 100644 index 0000000..849b82c --- /dev/null +++ b/newsfragments/114.removal.rst @@ -0,0 +1,2 @@ +Python 2.7 and PyPy 2 support is removed. h11 now requires Python>=3.5 including PyPy 3. +Users running `pip install h11` on Python 2 will automatically get the last Python 2-compatible version. diff --git a/setup.cfg b/setup.cfg index bda6834..0bd1262 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal=1 - [isort] combine_as_imports=True force_grid_wrap=0 diff --git a/setup.py b/setup.py index 25cbbe8..024b9a3 100644 --- a/setup.py +++ b/setup.py @@ -17,15 +17,15 @@ # This means, just install *everything* you see under h11/, even if it # doesn't look like a source file, so long as it appears in MANIFEST.in: include_package_data=True, + python_requires=">=3.5", classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", diff --git a/tox.ini b/tox.ini index de9b566..c2c748e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] -envlist = format, py27, py36, py37, py38, pypy, pypy3 +envlist = format, py36, py37, py38, pypy3 [gh-actions] python = - 2.7: py27 3.6: py36 3.7: py37 3.8: py38, format - pypy2: pypy pypy3: pypy3 [testenv]