Skip to content

Drop support for Python 2 #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ jobs:
max-parallel: 5
matrix:
python-version:
- 2.7
- 3.6
- 3.7
- 3.8
- pypy2
- pypy3

steps:
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://github.com/nodejs/http-parser>`_ and a beautiful nested state
machine implemented with ``yield from`` to postprocess the output. But
Expand Down
2 changes: 1 addition & 1 deletion bench/asv.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.6+


The last Python 2-compatible version was h11 0.11.x.

* Install: ``pip install h11``

Expand Down
7 changes: 1 addition & 6 deletions fuzz/afl-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion h11/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 1 addition & 4 deletions h11/_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
request_target_re = re.compile(request_target.encode("ascii"))


class _EventBundle(object):
class _EventBundle:
_fields = []
_defaults = {}

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion h11/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 4 additions & 12 deletions h11/_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
8 changes: 1 addition & 7 deletions h11/_receivebuffer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import sys

__all__ = ["ReceiveBuffer"]


Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion h11/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
28 changes: 4 additions & 24 deletions h11/_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import re
import sys

__all__ = [
"ProtocolError",
"LocalProtocolError",
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 6 additions & 26 deletions h11/_writers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)


Expand All @@ -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)
Expand Down Expand Up @@ -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")

Expand Down
18 changes: 3 additions & 15 deletions h11/tests/test_against_stdlib_http.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
6 changes: 2 additions & 4 deletions h11/tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from http import HTTPStatus

import pytest

from .. import _events
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion h11/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions newsfragments/114.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Python 2.7 and PyPy 2 support is removed. h11 now requires Python>=3.5 including PyPy 3.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.6+

Users running `pip install h11` on Python 2 will automatically get the last Python 2-compatible version.
3 changes: 0 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[bdist_wheel]
universal=1

[isort]
combine_as_imports=True
force_grid_wrap=0
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.6

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also remove this?

"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
Expand Down
Loading