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

WIP: Make Brotli support enabled by default, if appropriate urllib3 version is available #5554

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 14 additions & 3 deletions .travis.yml
Expand Up @@ -8,19 +8,30 @@ script:
cache: pip
jobs:
include:
- stage: test
python: 'pypy3'
dist: xenial
before_script:
- pipenv install importlib_metadata
- stage: test
python: '2.7'
before_script:
- pipenv install importlib_metadata
- stage: test
python: '3.5'
before_script:
- pipenv install importlib_metadata
- stage: test
python: '3.6'
- stage: test
python: '3.7'
python: '3.7'
- stage: test
python: 'pypy3'
dist: xenial
python: '3.8'
- stage: test
python: '3.8'
name: 'Python: 3.8 without Brotli'
before_script:
- pipenv uninstall brotli
- stage: coverage
python: '3.6'
script: codecov
6 changes: 3 additions & 3 deletions Pipfile
Expand Up @@ -12,14 +12,14 @@ detox = "*"
httpbin = ">=0.7.0"
more-itertools = "<6.0"
pysocks = "*"
pytest = ">=2.8.0,<=3.10.1"
pytest = "<4.7"
pytest-httpbin = ">=0.0.7,<1.0"
pytest-mock = "*"
pytest-mock = "<3"
pytest-cov = "*"
pytest-xdist = "<=1.25"
readme-renderer = "*"
sphinx = "<=1.5.5"
tox = "*"

[packages]
"requests" = {path = ".", editable = true, extras = ["socks"]}
"requests" = {path = ".", editable = true, extras = ["socks", "brotli"]}
554 changes: 266 additions & 288 deletions Pipfile.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion docs/community/faq.rst
Expand Up @@ -10,7 +10,7 @@ This part of the documentation answers common questions about Requests.
Encoded Data?
-------------

Requests automatically decompresses gzip-encoded responses, and does
Requests automatically decompresses gzip-, deflate- and brotli-encoded[#]_ responses, and does
its best to decode response content to unicode when possible.

You can get direct access to the raw response (and even the socket),
Expand Down Expand Up @@ -91,3 +91,6 @@ For information on using SNI with Requests on Python < 2.7.9 refer to this
.. _`Server-Name-Indication`: https://en.wikipedia.org/wiki/Server_Name_Indication
.. _`virtual hosting`: https://en.wikipedia.org/wiki/Virtual_hosting
.. _`Stack Overflow answer`: https://stackoverflow.com/questions/18578439/using-requests-with-tls-doesnt-give-sni-support/18579484#18579484


[#] for Brotli support you need to have `urllib3` >= 1.25.1 and `brotli` packages installed
4 changes: 2 additions & 2 deletions docs/user/advanced.rst
Expand Up @@ -113,8 +113,8 @@ However, if we want to get the headers we sent the server, we simply access the
request, and then the request's headers::

>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'}
{'Accept-Encoding': 'br, gzip, deflate',
'Accept': '*/*', 'User-Agent': 'python-requests/2.25.0'}

.. _prepared-requests:

Expand Down
5 changes: 3 additions & 2 deletions docs/user/quickstart.rst
Expand Up @@ -128,7 +128,7 @@ You can also access the response body as bytes, for non-text requests::
>>> r.content
b'[{"repository":{"open_issues":0,"url":"https://github.com/...

The ``gzip`` and ``deflate`` transfer-encodings are automatically decoded for you.
The ``gzip``, ``deflate`` and ``brotli``[#]_ transfer-encodings are automatically decoded for you.

For example, to create an image from binary data returned by a request, you can
use the following code::
Expand Down Expand Up @@ -559,7 +559,8 @@ All exceptions that Requests explicitly raises inherit from

-----------------------

Ready for more? Check out the :ref:`advanced <advanced>` section.
[#] for Brotli support you need to have `urllib3` >= 1.25.1 and `brotli` packages installed

Ready for more? Check out the :ref:`advanced <advanced>` section.

If you're on the job market, consider taking `this programming quiz <https://triplebyte.com/a/b1i2FB8/requests-docs-1>`_. A substantial donation will be made to this project, if you find a job through this platform.
34 changes: 33 additions & 1 deletion requests/utils.py
Expand Up @@ -20,6 +20,7 @@
import warnings
import zipfile
from collections import OrderedDict
import urllib3

from .__version__ import __version__
from . import certs
Expand Down Expand Up @@ -804,13 +805,44 @@ def default_user_agent(name="python-requests"):
return '%s/%s' % (name, __version__)


def brotli_supported():
"""
Returns whether Brotli compression is supported

:rtype: bool
"""

# urllib >= 1.25.1 includes brotli support
major, minor, patch = urllib3.__version__.split('.') # noqa: F811
major, minor, patch = int(major), int(minor), int(patch)
urllib3_with_brotli = (major == 1 and ((minor == 25 and patch >= 1) or (minor >= 26))) \
or major >= 2

# pybrotli is an extra package required by urllib3 for brotli support
try:
import brotli
except ImportError:
brotli = None

return urllib3_with_brotli and brotli


BROTLI_SUPPORTED = brotli_supported()


def default_headers():
"""
:rtype: requests.structures.CaseInsensitiveDict
"""

if BROTLI_SUPPORTED:
accepted_encodings = ('br', 'gzip', 'deflate')
else:
accepted_encodings = ('gzip', 'deflate')

return CaseInsensitiveDict({
'User-Agent': default_user_agent(),
'Accept-Encoding': ', '.join(('gzip', 'deflate')),
'Accept-Encoding': ', '.join(accepted_encodings),
'Accept': '*/*',
'Connection': 'keep-alive',
})
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Expand Up @@ -54,7 +54,8 @@ def run_tests(self):
'pytest-mock',
'pytest-xdist',
'PySocks>=1.5.6, !=1.5.7',
'pytest>=3'
'brotli',
'pytest<4.7'
]

about = {}
Expand Down Expand Up @@ -105,6 +106,7 @@ def run_tests(self):
'security': ['pyOpenSSL >= 0.14', 'cryptography>=1.3.4'],
'socks': ['PySocks>=1.5.6, !=1.5.7'],
'socks:sys_platform == "win32" and python_version == "2.7"': ['win_inet_pton'],
'brotli': ['urllib3 >= 1.25.1', 'brotli'],
},
project_urls={
'Documentation': 'https://requests.readthedocs.io',
Expand Down
37 changes: 37 additions & 0 deletions tests/test_lowlevel.py
Expand Up @@ -3,7 +3,9 @@
import pytest
import threading
import requests
import brotli

from requests.utils import brotli_supported
from tests.testserver.server import Server, consume_socket_content

from .utils import override_environ
Expand Down Expand Up @@ -307,3 +309,38 @@ def response_handler(sock):
assert r.url == 'http://{}:{}/final-url/#relevant-section'.format(host, port)

close_server.set()

@pytest.mark.skipif(not brotli_supported(), reason='run only if Brotli is supported')
def test_brotli():
"""Verify that if Requests supports Brotli, then a request to a server serving
content in Brotli will result with a response with the content correctly decompressed.
"""

some_text = '<html><html><html>'
br_compressed_text = brotli.compress(some_text.encode('utf-8'))

assert len(some_text) != len(br_compressed_text)
assert some_text != br_compressed_text

def response_handler(sock):
consume_socket_content(sock, timeout=0.5)

sock.send(
b'HTTP/1.1 200 OK\r\n'
b'Content-Length: ' + bytes(len(br_compressed_text)) + b'\r\n'
b'Content-Encoding: br\r\n'
b'\r\n' +
br_compressed_text
)

close_server = threading.Event()
server = Server(response_handler, wait_to_close_event=close_server)

with server as (host, port):
url = 'http://{}:{}/'.format(host, port)
r = requests.get(url)

assert len(r.text) == len(some_text)
assert r.text == some_text

close_server.set()
21 changes: 21 additions & 0 deletions tests/test_requests.py
Expand Up @@ -31,6 +31,7 @@
from requests.models import urlencode
from requests.hooks import default_hooks
from requests.compat import MutableMapping
from requests.utils import brotli_supported

from .compat import StringIO, u
from .utils import override_environ
Expand Down Expand Up @@ -2527,3 +2528,23 @@ def test_parameters_for_nonstandard_schemes(self, input, params, expected):
r = requests.Request('GET', url=input, params=params)
p = r.prepare()
assert p.url == expected


@pytest.mark.skipif(not brotli_supported(), reason='run only if Brotli is supported')
def test_brotli_default_accept_encoding(self, httpbin):
"""Verify that Requests with Brotli support by default sends Accept-Encoding with 'br',
as the first accepted encoding as the most effecient
"""
r = requests.request('GET', httpbin('get'))

assert 'br' in r.request.headers['Accept-Encoding']
assert r.request.headers['Accept-Encoding'].startswith('br')


@pytest.mark.skipif(brotli_supported(), reason='run only if Brotli is NOT supported')
def test_no_brotli_default_accept_encoding(self, httpbin):
"""Verify that Requests without Brotli support by default sends Accept-Encoding without 'br'
"""
r = requests.request('GET', httpbin('get'))

assert 'br' not in r.request.headers['Accept-Encoding']