Skip to content

Commit bef2159

Browse files
committed
Drop support for Python 2
1 parent 522b004 commit bef2159

21 files changed

+40
-115
lines changed

.github/workflows/ci.yml

-2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ jobs:
1313
max-parallel: 5
1414
matrix:
1515
python-version:
16-
- 2.7
1716
- 3.6
1817
- 3.7
1918
- 3.8
20-
- pypy2
2119
- pypy3
2220

2321
steps:

CONTRIBUTING.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ other hand, the following are all very welcome:
7070
tox
7171
```
7272

73-
But note that: (1) this will print slightly misleading coverage
73+
But note that: (1) this might print slightly misleading coverage
7474
statistics, because it only shows coverage for individual python
75-
versions, and there are some lines that are only executed on python
76-
2 or only executed on python 3, and (2) the full test suite will
75+
versions, and there might be some lines that are only executed on some
76+
python versions or implementations, and (2) the full test suite will
7777
automatically get run when you submit a pull request, so you don't
7878
need to worry too much about tracking down a version of cpython 3.3
7979
or whatever just to run the tests.

README.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ library.
112112
It has a test suite with 100.0% coverage for both statements and
113113
branches.
114114

115-
Currently it supports Python 3 (testing on 3.5-3.8), Python 2.7, and PyPy.
115+
Currently it supports Python 3 (testing on 3.5-3.8) and PyPy 3.
116+
The last Python 2-compatible version was h11 0.11.x.
116117
(Originally it had a Cython wrapper for `http-parser
117118
<https://github.com/nodejs/http-parser>`_ and a beautiful nested state
118119
machine implemented with ``yield from`` to postprocess the output. But

bench/asv.conf.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
// The Pythons you'd like to test against. If not provided, defaults
3838
// to the current version of Python used to run `asv`.
39-
"pythons": ["2.7", "3.5", "pypy"],
39+
"pythons": ["3.8", "pypy3"],
4040

4141
// The matrix of dependencies to test. Each key is the name of a
4242
// package (in PyPI) and the values are version numbers. An empty

docs/source/index.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ whatever. But h11 makes it much easier to implement something like
4444
Vital statistics
4545
----------------
4646

47-
* Requirements: Python 2.7 or Python 3.5+ (PyPy works great)
47+
* Requirements: Python 3.5+ (PyPy works great)
48+
49+
The last Python 2-compatible version was h11 0.11.x.
4850

4951
* Install: ``pip install h11``
5052

fuzz/afl-server.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99

1010
import h11
1111

12-
if sys.version_info[0] >= 3:
13-
in_file = sys.stdin.detach()
14-
else:
15-
in_file = sys.stdin
16-
1712

1813
def process_all(c):
1914
while True:
@@ -26,7 +21,7 @@ def process_all(c):
2621

2722
afl.init()
2823

29-
data = in_file.read()
24+
data = sys.stdin.detach().read()
3025

3126
# one big chunk
3227
server1 = h11.Connection(h11.SERVER)

h11/_connection.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _body_framing(request_method, event):
109109
################################################################
110110

111111

112-
class Connection(object):
112+
class Connection:
113113
"""An object encapsulating the state of an HTTP connection.
114114
115115
Args:

h11/_events.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
request_target_re = re.compile(request_target.encode("ascii"))
2525

2626

27-
class _EventBundle(object):
27+
class _EventBundle:
2828
_fields = []
2929
_defaults = {}
3030

h11/_headers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def normalize_and_validate(headers, _parsed=False):
132132
raw_name = name
133133
name = name.lower()
134134
if name == b"content-length":
135-
lengths = set(length.strip() for length in value.split(b","))
135+
lengths = {length.strip() for length in value.split(b",")}
136136
if len(lengths) != 1:
137137
raise LocalProtocolError("conflicting Content-Length headers")
138138
value = lengths.pop()

h11/_readers.py

+4-12
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,7 @@ def _obsolete_line_fold(lines):
5454

5555
def _decode_header_lines(lines):
5656
for line in _obsolete_line_fold(lines):
57-
# _obsolete_line_fold yields either bytearray or bytes objects. On
58-
# Python 3, validate() takes either and returns matches as bytes. But
59-
# on Python 2, validate can return matches as bytearrays, so we have
60-
# to explicitly cast back.
61-
matches = validate(
62-
header_field_re, bytes(line), "illegal header line: {!r}", bytes(line)
63-
)
57+
matches = validate(header_field_re, line, "illegal header line: {!r}", line)
6458
yield (matches["field_name"], matches["field_value"])
6559

6660

@@ -127,7 +121,7 @@ def read_eof(self):
127121
chunk_header_re = re.compile(chunk_header.encode("ascii"))
128122

129123

130-
class ChunkedReader(object):
124+
class ChunkedReader:
131125
def __init__(self):
132126
self._bytes_in_chunk = 0
133127
# After reading a chunk, we have to throw away the trailing \r\n; if
@@ -163,9 +157,7 @@ def __call__(self, buf):
163157
chunk_header,
164158
)
165159
# XX FIXME: we discard chunk extensions. Does anyone care?
166-
# We convert to bytes because Python 2's `int()` function doesn't
167-
# work properly on bytearray objects.
168-
self._bytes_in_chunk = int(bytes(matches["chunk_size"]), base=16)
160+
self._bytes_in_chunk = int(matches["chunk_size"], base=16)
169161
if self._bytes_in_chunk == 0:
170162
self._reading_trailer = True
171163
return self(buf)
@@ -191,7 +183,7 @@ def read_eof(self):
191183
)
192184

193185

194-
class Http10Reader(object):
186+
class Http10Reader:
195187
def __call__(self, buf):
196188
data = buf.maybe_extract_at_most(999999999)
197189
if data is None:

h11/_receivebuffer.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import sys
2-
31
__all__ = ["ReceiveBuffer"]
42

53

@@ -38,7 +36,7 @@
3836
# slightly clever thing where we delay calling compress() until we've
3937
# processed a whole event, which could in theory be slightly more efficient
4038
# than the internal bytearray support.)
41-
class ReceiveBuffer(object):
39+
class ReceiveBuffer:
4240
def __init__(self):
4341
self._data = bytearray()
4442
# These are both absolute offsets into self._data:
@@ -53,10 +51,6 @@ def __bool__(self):
5351
def __bytes__(self):
5452
return bytes(self._data[self._start :])
5553

56-
if sys.version_info[0] < 3: # version specific: Python 2
57-
__str__ = __bytes__
58-
__nonzero__ = __bool__
59-
6054
def __len__(self):
6155
return len(self._data) - self._start
6256

h11/_state.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
}
198198

199199

200-
class ConnectionState(object):
200+
class ConnectionState:
201201
def __init__(self):
202202
# Extra bits of state that don't quite fit into the state model.
203203

h11/_util.py

+4-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import re
2-
import sys
3-
41
__all__ = [
52
"ProtocolError",
63
"LocalProtocolError",
@@ -74,34 +71,17 @@ def _reraise_as_remote_protocol_error(self):
7471
# (exc_info[0]) separately from the exception object (exc_info[1]),
7572
# and we only modified the latter. So we really do need to re-raise
7673
# the new type explicitly.
77-
if sys.version_info[0] >= 3:
78-
# On py3, the traceback is part of the exception object, so our
79-
# in-place modification preserved it and we can just re-raise:
80-
raise self
81-
else:
82-
# On py2, preserving the traceback requires 3-argument
83-
# raise... but on py3 this is a syntax error, so we have to hide
84-
# it inside an exec
85-
exec("raise RemoteProtocolError, self, sys.exc_info()[2]")
74+
# On py3, the traceback is part of the exception object, so our
75+
# in-place modification preserved it and we can just re-raise:
76+
raise self
8677

8778

8879
class RemoteProtocolError(ProtocolError):
8980
pass
9081

9182

92-
try:
93-
_fullmatch = type(re.compile("")).fullmatch
94-
except AttributeError:
95-
96-
def _fullmatch(regex, data): # version specific: Python < 3.4
97-
match = regex.match(data)
98-
if match and match.end() != len(data):
99-
match = None
100-
return match
101-
102-
10383
def validate(regex, data, msg="malformed data", *format_args):
104-
match = _fullmatch(regex, data)
84+
match = regex.fullmatch(data)
10585
if not match:
10686
if format_args:
10787
msg = msg.format(*format_args)

h11/_writers.py

+6-26
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,12 @@
77
# - a writer
88
# - or, for body writers, a dict of framin-dependent writer factories
99

10-
import sys
11-
1210
from ._events import Data, EndOfMessage
1311
from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER
1412
from ._util import LocalProtocolError
1513

1614
__all__ = ["WRITERS"]
1715

18-
# Equivalent of bstr % values, that works on python 3.x for x < 5
19-
if (3, 0) <= sys.version_info < (3, 5):
20-
21-
def bytesmod(bstr, values):
22-
decoded_values = []
23-
for value in values:
24-
if isinstance(value, bytes):
25-
decoded_values.append(value.decode("ascii"))
26-
else:
27-
decoded_values.append(value)
28-
return (bstr.decode("ascii") % tuple(decoded_values)).encode("ascii")
29-
30-
31-
else:
32-
33-
def bytesmod(bstr, values):
34-
return bstr % values
35-
3616

3717
def write_headers(headers, write):
3818
# "Since the Host field-value is critical information for handling a
@@ -41,17 +21,17 @@ def write_headers(headers, write):
4121
raw_items = headers._full_items
4222
for raw_name, name, value in raw_items:
4323
if name == b"host":
44-
write(bytesmod(b"%s: %s\r\n", (raw_name, value)))
24+
write(b"%s: %s\r\n" % (raw_name, value))
4525
for raw_name, name, value in raw_items:
4626
if name != b"host":
47-
write(bytesmod(b"%s: %s\r\n", (raw_name, value)))
27+
write(b"%s: %s\r\n" % (raw_name, value))
4828
write(b"\r\n")
4929

5030

5131
def write_request(request, write):
5232
if request.http_version != b"1.1":
5333
raise LocalProtocolError("I only send HTTP/1.1")
54-
write(bytesmod(b"%s %s HTTP/1.1\r\n", (request.method, request.target)))
34+
write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target))
5535
write_headers(request.headers, write)
5636

5737

@@ -68,11 +48,11 @@ def write_any_response(response, write):
6848
# from stdlib's http.HTTPStatus table. Or maybe just steal their enums
6949
# (either by import or copy/paste). We already accept them as status codes
7050
# since they're of type IntEnum < int.
71-
write(bytesmod(b"HTTP/1.1 %s %s\r\n", (status_bytes, response.reason)))
51+
write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason))
7252
write_headers(response.headers, write)
7353

7454

75-
class BodyWriter(object):
55+
class BodyWriter:
7656
def __call__(self, event, write):
7757
if type(event) is Data:
7858
self.send_data(event.data, write)
@@ -111,7 +91,7 @@ def send_data(self, data, write):
11191
# end-of-message.
11292
if not data:
11393
return
114-
write(bytesmod(b"%x\r\n", (len(data),)))
94+
write(b"%x\r\n" % len(data))
11595
write(data)
11696
write(b"\r\n")
11797

h11/tests/test_against_stdlib_http.py

+3-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
11
import json
22
import os.path
33
import socket
4+
import socketserver
45
import threading
56
from contextlib import closing, contextmanager
7+
from http.server import SimpleHTTPRequestHandler
8+
from urllib.request import urlopen
69

710
import h11
811

9-
try:
10-
from urllib.request import urlopen
11-
except ImportError: # version specific: Python 2
12-
from urllib2 import urlopen
13-
14-
try:
15-
import socketserver
16-
except ImportError: # version specific: Python 2
17-
import SocketServer as socketserver
18-
19-
try:
20-
from http.server import SimpleHTTPRequestHandler
21-
except ImportError: # version specific: Python 2
22-
from SimpleHTTPServer import SimpleHTTPRequestHandler
23-
2412

2513
@contextmanager
2614
def socket_server(handler):

h11/tests/test_events.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from http import HTTPStatus
2+
13
import pytest
24

35
from .. import _events
@@ -154,10 +156,6 @@ def test_events():
154156

155157
def test_intenum_status_code():
156158
# https://github.com/python-hyper/h11/issues/72
157-
try:
158-
from http import HTTPStatus
159-
except ImportError:
160-
pytest.skip("Only affects Python 3")
161159

162160
r = Response(status_code=HTTPStatus.OK, headers=[], http_version="1.0")
163161
assert r.status_code == HTTPStatus.OK

h11/tests/test_util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_bytesify():
9393
assert bytesify("123") == b"123"
9494

9595
with pytest.raises(UnicodeEncodeError):
96-
bytesify(u"\u1234")
96+
bytesify("\u1234")
9797

9898
with pytest.raises(TypeError):
9999
bytesify(10)

newsfragments/114.removal.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Python 2.7 and PyPy 2 support is removed. h11 now requires Python>=3.5 including PyPy 3.
2+
Users running `pip install h11` on Python 2 will automatically get the last Python 2-compatible version.

setup.cfg

-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
[bdist_wheel]
2-
universal=1
3-
41
[isort]
52
combine_as_imports=True
63
force_grid_wrap=0

setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
# This means, just install *everything* you see under h11/, even if it
1818
# doesn't look like a source file, so long as it appears in MANIFEST.in:
1919
include_package_data=True,
20+
python_requires=">=3.5",
2021
classifiers=[
2122
"Development Status :: 3 - Alpha",
2223
"Intended Audience :: Developers",
2324
"License :: OSI Approved :: MIT License",
2425
"Programming Language :: Python :: Implementation :: CPython",
2526
"Programming Language :: Python :: Implementation :: PyPy",
26-
"Programming Language :: Python :: 2",
27-
"Programming Language :: Python :: 2.7",
2827
"Programming Language :: Python :: 3",
28+
"Programming Language :: Python :: 3 :: Only",
2929
"Programming Language :: Python :: 3.5",
3030
"Programming Language :: Python :: 3.6",
3131
"Programming Language :: Python :: 3.7",

0 commit comments

Comments
 (0)