Skip to content
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

Drop support for Python 2.7 and 3.5 #320

Merged
merged 19 commits into from Jan 24, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/ci.yaml
Expand Up @@ -63,7 +63,7 @@ jobs:
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy-2.7", "pypy-3.7"]
python-version: ["3.6", "3.7", "3.8", "3.9", "pypy-3.7"]
twisted-version: ["lowest", "latest"]
experimental: [false]

Expand Down Expand Up @@ -97,13 +97,10 @@ jobs:
shell: python
run: |
table = {
"2.7": "py27",
"3.5": "py35",
"3.6": "py36",
"3.7": "py37",
"3.8": "py38",
"3.9": "py39",
"pypy-2.7": "pypy",
"pypy-3.7": "pypy3",
}
factor = table["${{ matrix.python-version }}"]
Expand Down
1 change: 1 addition & 0 deletions changelog.d/318.removal.rst
@@ -0,0 +1 @@
Support for Python 2.7 and 3.5 has been dropped. treq no longer depends on ``six`` or ``mock``.
6 changes: 4 additions & 2 deletions docs/testing.rst
Expand Up @@ -33,13 +33,15 @@ Download: :download:`testing_seq.py <examples/testing_seq.py>`.
Loosely matching the request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you don't care about certain parts of the request, you can pass :data:`mock.ANY`, which compares equal to anything.
If you don't care about certain parts of the request, you can pass :data:`unittest.mock.ANY`, which compares equal to anything.
This sequence matches a single GET request with any parameters or headers:

.. code-block:: python

from unittest.mock import ANY

RequestSequence([
((b'get', mock.ANY, mock.ANY, b''), (200, {}, b'ok'))
((b'get', ANY, ANY, b''), (200, {}, b'ok'))
])


Expand Down
6 changes: 1 addition & 5 deletions setup.py
Expand Up @@ -7,8 +7,6 @@
"Operating System :: OS Independent",
"Framework :: Twisted",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
Expand All @@ -28,18 +26,16 @@
package_dir={"": "src"},
setup_requires=["incremental"],
use_incremental=True,
python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
python_requires='>=3.5',
twm marked this conversation as resolved.
Show resolved Hide resolved
install_requires=[
"incremental",
"requests >= 2.1.0",
"hyperlink >= 21.0.0",
"six >= 1.13.0",
"Twisted[tls] >= 18.7.0",
"attrs",
],
extras_require={
"dev": [
"mock",
"pep8",
"pyflakes",
"sphinx",
Expand Down
33 changes: 13 additions & 20 deletions src/treq/client.py
@@ -1,15 +1,10 @@
from __future__ import absolute_import, division, print_function

import io
import mimetypes
import uuid
import warnings

import io

import six
from six.moves.collections_abc import Mapping
from six.moves.http_cookiejar import CookieJar
from six.moves.urllib.parse import quote_plus, urlencode as _urlencode
from collections.abc import Mapping
from http.cookiejar import CookieJar
from urllib.parse import quote_plus, urlencode as _urlencode

from twisted.internet.interfaces import IProtocol
from twisted.internet.defer import Deferred
Expand Down Expand Up @@ -42,7 +37,10 @@


def urlencode(query, doseq):
return six.ensure_binary(_urlencode(query, doseq), encoding='ascii')
s = _urlencode(query, doseq)
if not isinstance(s, bytes):
s = s.encode("ascii")
return s


class _BodyBufferingProtocol(proxyForInterface(IProtocol)):
Expand Down Expand Up @@ -156,7 +154,7 @@ def request(self, method, url, **kwargs):
parsed_url = url.encoded_url
elif isinstance(url, EncodedURL):
parsed_url = url
elif isinstance(url, six.text_type):
elif isinstance(url, str):
# We use hyperlink in lazy mode so that users can pass arbitrary
# bytes in the path and querystring.
parsed_url = EncodedURL.from_text(url)
Expand Down Expand Up @@ -250,7 +248,7 @@ def _request_headers(self, headers, stacklevel):
if isinstance(headers, dict):
h = Headers({})
for k, v in headers.items():
if isinstance(v, (bytes, six.text_type)):
if isinstance(v, (bytes, str)):
h.addRawHeader(k, v)
elif isinstance(v, list):
h.setRawHeaders(k, v)
Expand Down Expand Up @@ -432,7 +430,7 @@ def _query_quote(v):
a querystring (with space as ``+``).
"""
if not isinstance(v, (str, bytes)):
v = six.text_type(v)
v = str(v)
if not isinstance(v, bytes):
v = v.encode("utf-8")
q = quote_plus(v)
Expand Down Expand Up @@ -496,10 +494,5 @@ def _guess_content_type(filename):
registerAdapter(_from_bytes, bytes, IBodyProducer)
registerAdapter(_from_file, io.BytesIO, IBodyProducer)

if six.PY2:
registerAdapter(_from_file, six.StringIO, IBodyProducer)
# Suppress lint failure on Python 3.
registerAdapter(_from_file, file, IBodyProducer) # noqa: F821
else:
# file()/open() equiv on Py3
registerAdapter(_from_file, io.BufferedReader, IBodyProducer)
# file()/open() equiv on Py3
twm marked this conversation as resolved.
Show resolved Hide resolved
registerAdapter(_from_file, io.BufferedReader, IBodyProducer)
2 changes: 0 additions & 2 deletions src/treq/content.py
@@ -1,5 +1,3 @@
from __future__ import absolute_import, division, print_function

import cgi
import json

Expand Down
22 changes: 9 additions & 13 deletions src/treq/multipart.py
@@ -1,14 +1,10 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from __future__ import absolute_import, division, print_function

from uuid import uuid4
from io import BytesIO
from contextlib import closing

from six import integer_types, text_type

from twisted.internet import defer, task
from twisted.web.iweb import UNKNOWN_LENGTH, IBodyProducer

Expand Down Expand Up @@ -60,7 +56,7 @@ def __init__(self, fields, boundary=None, cooperator=task):

self.boundary = boundary or uuid4().hex

if isinstance(self.boundary, text_type):
if isinstance(self.boundary, str):
self.boundary = self.boundary.encode('ascii')

self.length = self._calculateLength()
Expand Down Expand Up @@ -169,7 +165,7 @@ def _writeLoop(self, consumer):
consumer.write(CRLF + self._getBoundary(final=True) + CRLF)

def _writeField(self, name, value, consumer):
if isinstance(value, text_type):
if isinstance(value, str):
self._writeString(name, value, consumer)
elif isinstance(value, tuple):
filename, content_type, producer = value
Expand Down Expand Up @@ -218,8 +214,8 @@ def _escape(value):
a newline in the file name parameter makes form-data request unreadable
for majority of parsers.
"""
if not isinstance(value, (bytes, text_type)):
value = text_type(value)
if not isinstance(value, (bytes, str)):
value = str(value)
if isinstance(value, bytes):
value = value.decode('utf-8')
return value.replace(u"\r", u"").replace(u"\n", u"").replace(u'"', u'\\"')
Expand All @@ -232,14 +228,14 @@ def _enforce_unicode(value):
If someone needs to pass the binary string, use BytesIO and wrap it with
`FileBodyProducer`.
"""
if isinstance(value, text_type):
if isinstance(value, str):
return value

elif isinstance(value, bytes):
# we got a byte string, and we have no ide what's the encoding of it
# we can only assume that it's something cool
try:
return text_type(value, "utf-8")
return str(value, "utf-8")
twm marked this conversation as resolved.
Show resolved Hide resolved
except UnicodeDecodeError:
raise ValueError(
"Supplied raw bytes that are not ascii/utf-8."
Expand Down Expand Up @@ -267,7 +263,7 @@ def _converted(fields):
filename = _enforce_unicode(filename) if filename else None
yield name, (filename, content_type, producer)

elif isinstance(value, (bytes, text_type)):
elif isinstance(value, (bytes, str)):
yield name, _enforce_unicode(value)
twm marked this conversation as resolved.
Show resolved Hide resolved

else:
Expand Down Expand Up @@ -300,7 +296,7 @@ def write(self, value):

if value is UNKNOWN_LENGTH:
self.length = value
elif isinstance(value, integer_types):
elif isinstance(value, int):
self.length += value
else:
self.length += len(value)
Expand Down Expand Up @@ -347,7 +343,7 @@ def _sorted_by_type(fields):
"""
def key(p):
key, val = p
if isinstance(val, (bytes, text_type)):
if isinstance(val, (bytes, str)):
return (0, key)
else:
return (1, key)
Expand Down
2 changes: 0 additions & 2 deletions src/treq/response.py
@@ -1,5 +1,3 @@
from __future__ import absolute_import, division, print_function

from twisted.python.components import proxyForInterface
from twisted.web.iweb import IResponse, UNKNOWN_LENGTH
from twisted.python import reflect
Expand Down
23 changes: 6 additions & 17 deletions src/treq/test/local_httpbin/child.py
Expand Up @@ -10,9 +10,7 @@

import httpbin

import six

from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
from twisted.internet.defer import Deferred, inlineCallbacks
from twisted.internet.endpoints import TCP4ServerEndpoint, SSL4ServerEndpoint
from twisted.internet.task import react
from twisted.internet.ssl import (Certificate,
Expand Down Expand Up @@ -158,11 +156,9 @@ def _serve_tls(reactor, host, port, site):
:return: A :py:class:`Deferred` that fires with a
:py:class:`_HTTPBinDescription`
"""
cert_host = host.decode('ascii') if six.PY2 else host

(
ca_cert, private_key, certificate,
) = _certificates_for_authority_and_server(cert_host)
) = _certificates_for_authority_and_server(host)

context_factory = CertificateOptions(privateKey=private_key,
certificate=certificate)
Expand All @@ -178,7 +174,7 @@ def _serve_tls(reactor, host, port, site):
port=port.getHost().port,
cacert=ca_cert.dumpPEM().decode('ascii'))

returnValue(description)
return description


@inlineCallbacks
Expand All @@ -202,7 +198,7 @@ def _serve_tcp(reactor, host, port, site):

description = _HTTPBinDescription(host=host, port=port.getHost().port)

returnValue(description)
return description


def _output_process_description(description, stdout=sys.stdout):
Expand All @@ -214,15 +210,8 @@ def _output_process_description(description, stdout=sys.stdout):

:param stdout: (optional) Standard out.
"""
if six.PY2:
write = stdout.write
flush = stdout.flush
else:
write = stdout.buffer.write
flush = stdout.buffer.flush

write(description.to_json_bytes() + b'\n')
flush()
stdout.buffer.write(description.to_json_bytes() + b'\n')
stdout.buffer.flush()


def _forever_httpbin(reactor, argv,
Expand Down
21 changes: 8 additions & 13 deletions src/treq/test/local_httpbin/test/test_child.py
Expand Up @@ -25,8 +25,6 @@

from service_identity.cryptography import verify_certificate_hostname

import six

from .. import child, shared


Expand Down Expand Up @@ -263,14 +261,13 @@ def flush(self):
self._state.flush_count += 1


if not six.PY2:
@attr.s
class BufferedStandardOut(object):
"""
A standard out that whose ``buffer`` is a
:py:class:`FlushableBytesIO` instance.
"""
buffer = attr.ib()
@attr.s
class BufferedStandardOut(object):
twm marked this conversation as resolved.
Show resolved Hide resolved
"""
A standard out that whose ``buffer`` is a
:py:class:`FlushableBytesIO` instance.
"""
buffer = attr.ib()


class OutputProcessDescriptionTests(SynchronousTestCase):
Expand All @@ -280,9 +277,7 @@ class OutputProcessDescriptionTests(SynchronousTestCase):

def setUp(self):
self.stdout_state = FlushableBytesIOState()
self.stdout = FlushableBytesIO(self.stdout_state)
if not six.PY2:
self.stdout = BufferedStandardOut(self.stdout)
self.stdout = BufferedStandardOut(FlushableBytesIO(self.stdout_state))

def test_description_written(self):
"""
Expand Down
3 changes: 1 addition & 2 deletions src/treq/test/test_client.py
@@ -1,8 +1,7 @@
# -*- encoding: utf-8 -*-
from collections import OrderedDict
from io import BytesIO

import mock
from unittest import mock

from hyperlink import DecodedURL, EncodedURL
from twisted.internet.defer import Deferred, succeed, CancelledError
Expand Down
3 changes: 1 addition & 2 deletions src/treq/test/test_content.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock

from twisted.python.failure import Failure

Expand Down