Merge branch 'master' into proposed/3.0.0

This commit is contained in:
Cory Benfield
2016-12-08 09:16:51 +00:00
20 changed files with 8421 additions and 2157 deletions
+2
View File
@@ -175,3 +175,5 @@ Patches and Suggestions
- Brian Bamsch <bbamsch32@gmail.com> (`@bbamsch <https://github.com/bbamsch>`_)
- Om Prakash Kumar <omprakash070@gmail.com> (`@iamprakashom <https://github.com/iamprakashom>`_)
- Philipp Konrad <gardiac2002@gmail.com> (`@gardiac2002 <https://github.com/gardiac2002>`_)
- Hussain Tamboli <hussaintamboli18@gmail.com> (`@hussaintamboli <https://github.com/hussaintamboli>`_)
- Casey Davidson (`@davidsoncasey <https://github.com/davidsoncasey>`_)
+43
View File
@@ -3,6 +3,49 @@
Release History
---------------
2.12.3 (2016-12-01)
+++++++++++++++++++
**Bugfixes**
- Fixed regression from v2.12.1 for URLs with schemes that begin with "http".
These URLs have historically been processed as though they were HTTP-schemed
URLs, and so have had parameters added. This was removed in v2.12.2 in an
overzealous attempt to resolve problems with IDNA-encoding those URLs. This
change was reverted: the other fixes for IDNA-encoding have been judged to
be sufficient to return to the behaviour Requests had before v2.12.0.
2.12.2 (2016-11-30)
+++++++++++++++++++
**Bugfixes**
- Fixed several issues with IDNA-encoding URLs that are technically invalid but
which are widely accepted. Requests will now attempt to IDNA-encode a URL if
it can but, if it fails, and the host contains only ASCII characters, it will
be passed through optimistically. This will allow users to opt-in to using
IDNA2003 themselves if they want to, and will also allow technically invalid
but still common hostnames.
- Fixed an issue where URLs with leading whitespace would raise
``InvalidSchema`` errors.
- Fixed an issue where some URLs without the HTTP or HTTPS schemes would still
have HTTP URL preparation applied to them.
- Fixed an issue where Unicode strings could not be used in basic auth.
- Fixed an issue encountered by some Requests plugins where constructing a
Response object would cause ``Response.content`` to raise an
``AttributeError``.
2.12.1 (2016-11-16)
+++++++++++++++++++
**Bugfixes**
- Updated setuptools 'security' extra for the new PyOpenSSL backend in urllib3.
**Miscellaneous**
- Updated bundled urllib3 to 1.19.1.
2.12.0 (2016-11-15)
+++++++++++++++++++
+5 -1
View File
@@ -3,7 +3,7 @@ Requests: HTTP for Humans
.. image:: https://img.shields.io/pypi/v/requests.svg
:target: https://pypi.python.org/pypi/requests
Requests is the only *Non-GMO* HTTP library for Python, safe for human
consumption.
@@ -29,6 +29,10 @@ Behold, the power of Requests:
See `the similar code, sans Requests <https://gist.github.com/973705>`_.
.. image:: http://docs.python-requests.org/en/master/_static/requests-sidebar.png
:target: http://docs.python-requests.org/
Requests allows you to send *organic, grass-fed* HTTP/1.1 requests, without the
need for manual labor. There's no need to manually add query strings to your
URLs, or to form-encode your POST data. Keep-alive and HTTP connection pooling
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 299 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" title="Rezzy the Requests Sea Turtle"/>
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" title="https://kennethreitz.org/tattoos"/>
</a>
</p>
@@ -21,7 +21,7 @@
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow @kennethreitz</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></p>
<p><a href="https://saythanks.io/to/kennethreitz">Say Thanks!</a></p>
<p><a href="http://tinyletter.com/kennethreitz">Join Mailing List</a>.</p>
<h3>Other Projects</h3>
+5 -1
View File
@@ -1,6 +1,6 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" title="Rezzy the Requests Sea Turtle"/>
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" title="https://kennethreitz.org/tattoos"/>
</a>
</p>
<p>
@@ -19,6 +19,10 @@
<p><a href="http://tinyletter.com/kennethreitz">Join Mailing List</a>.</p>
<hr/>
<p>If you enjoy using this project, <a href="https://saythanks.io/to/kennethreitz">Say Thanks!</a></p>
<p><iframe src="http://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=false"
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
+1 -1
View File
@@ -42,7 +42,7 @@ A hotfix release will only include bug fixes that were missed when the project
released the previous version. If the previous version of Requests released
``v10.2.7`` the hotfix release would be versioned as ``v10.2.8``.
Hotfixes will **not** include upgrades to vendored dependences after
Hotfixes will **not** include upgrades to vendored dependencies after
``v2.6.2``
Reasoning
+5 -4
View File
@@ -178,13 +178,14 @@ In general, however, you should use a pattern like this to save what is being
streamed to a file::
with open(filename, 'wb') as fd:
for chunk in r.iter_content(chunk_size):
for chunk in r.iter_content(chunk_size=128):
fd.write(chunk)
Using ``Response.iter_content`` will handle a lot of what you would otherwise
have to handle when using ``Response.raw`` directly. When streaming a
download, the above is the preferred and recommended way to retrieve the
content.
content. Note that ``chunk_size`` can be freely adjusted to a number that
may better fit your use cases.
Custom Headers
@@ -423,8 +424,8 @@ suitable for use over multiple domains or paths. Cookie jars can
also be passed in to requests::
>>> jar = requests.cookies.RequestsCookieJar()
>>> jar.set('tasty_cookie', 'yum', site='httpbin.org', path='/cookies')
>>> jar.set('gross_cookie', 'blech', site='httpbin.org', path='/elsewhere')
>>> jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies')
>>> jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere')
>>> url = 'http://httpbin.org/cookies'
>>> r = requests.get(url, cookies=jar)
>>> r.text
+8149 -1644
View File
File diff suppressed because one or more lines are too long
+1 -487
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 863 KiB

+16 -1
View File
@@ -8,7 +8,7 @@ Provides utility functions that are consumed internally by Requests
which depend on extremely few external helpers (such as compat)
"""
from .compat import is_py2, builtin_str
from .compat import is_py2, builtin_str, str
def to_native_string(string, encoding='ascii'):
@@ -25,3 +25,18 @@ def to_native_string(string, encoding='ascii'):
out = string.decode(encoding)
return out
def unicode_is_ascii(u_string):
"""Determine if unicode string only contains ASCII characters.
:param str u_string: unicode string to check. Must be unicode
and not Python 2 `str`.
:rtype: bool
"""
assert isinstance(u_string, str)
try:
u_string.encode('ascii')
return True
except UnicodeEncodeError:
return False
+1 -1
View File
@@ -34,7 +34,7 @@ def request(method, url, session=None, **kwargs):
before giving up, as a float, or a :ref:`(connect timeout, read
timeout) <timeouts>` tuple.
:type timeout: float or tuple
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection.
:type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param verify: (optional) whether the SSL cert will be verified. A CA_BUNDLE path can also be provided. Defaults to ``True``.
+7 -1
View File
@@ -27,9 +27,15 @@ CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
def _basic_auth_str(username, password):
"""Returns a Basic Auth string."""
if isinstance(username, str):
username = username.encode('latin1')
if isinstance(password, str):
password = password.encode('latin1')
authstr = 'Basic ' + to_native_string(
b64encode(('%s:%s' % (username, password)).encode('latin1')).strip()
b64encode(b':'.join((username, password))).strip()
)
return authstr
+14 -8
View File
@@ -33,7 +33,7 @@ from .packages.urllib3.exceptions import (
from .exceptions import (
HTTPError, MissingScheme, InvalidURL, ChunkedEncodingError,
ContentDecodingError, ConnectionError, StreamConsumedError)
from ._internal_utils import to_native_string
from ._internal_utils import to_native_string, unicode_is_ascii
from .utils import (
guess_filename, get_auth_from_url, requote_uri,
stream_decode_response_unicode, to_key_val_list, parse_header_links,
@@ -372,11 +372,17 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
if not host:
raise InvalidURL("Invalid URL %r: No host supplied" % url)
# Only want to apply IDNA to the hostname
# In general, we want to try IDNA encoding every hostname, as that
# allows users to automatically get the correct behaviour. However,
# were quite strict about IDNA encoding, so certain valid hostnames
# may fail to encode. On failure, we verify the hostname meets a
# minimum standard of only containing ASCII characters, and not starting
# with a wildcard (*), before allowing the unencoded hostname through.
try:
host = idna.encode(host, uts46=True).decode('utf-8')
except (UnicodeError, idna.IDNAError):
raise InvalidURL('URL has an invalid label.')
if not unicode_is_ascii(host) or host.startswith(u'*'):
raise InvalidURL('URL has an invalid label.')
# Carefully reconstruct the network location
netloc = auth or ''
@@ -600,7 +606,7 @@ class Response(object):
#: Final URL location of Response.
self.url = None
#: Encoding to decode with when accessing r.text or
#: Encoding to decode with when accessing r.text or
#: r.iter_content(decode_unicode=True)
self.encoding = None
@@ -691,7 +697,7 @@ class Response(object):
chunks are received. If stream=False, data is returned as
a single chunk.
If using decode_unicode, the encoding must be set to a valid encoding
If using decode_unicode, the encoding must be set to a valid encoding
enumeration before invoking iter_content.
"""
@@ -737,11 +743,11 @@ class Response(object):
'encoding must be set before consuming streaming '
'responses'
)
# check encoding value here, don't wait for the generator to be
# consumed before raising an exception
codecs.lookup(self.encoding)
chunks = stream_decode_response_unicode(chunks, self)
return chunks
@@ -787,7 +793,7 @@ class Response(object):
raise RuntimeError(
'The content for this response was already consumed')
if self.status_code == 0:
if self.status_code == 0 or self.raw is None:
self._content = None
else:
self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes()
+2 -1
View File
@@ -32,7 +32,7 @@ except ImportError:
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
__license__ = 'MIT'
__version__ = '1.19'
__version__ = '1.19.1'
__all__ = (
'HTTPConnectionPool',
@@ -71,6 +71,7 @@ def add_stderr_logger(level=logging.DEBUG):
logger.debug('Added a stderr logging handler to logger: %s', __name__)
return handler
# ... Clean up.
del NullHandler
@@ -42,7 +42,7 @@ from __future__ import absolute_import
import logging
import os
import warnings
from urlparse import urljoin
from ..packages.six.moves.urllib.parse import urljoin
from ..exceptions import (
HTTPError,
@@ -141,4 +141,5 @@ def _has_ipv6(host):
sock.close()
return has_ipv6
HAS_IPV6 = _has_ipv6('::1')
+2 -1
View File
@@ -94,7 +94,8 @@ setup(
cmdclass={'test': PyTest},
tests_require=test_requirements,
extras_require={
'security': ['pyOpenSSL>=0.13', 'ndg-httpsclient', 'pyasn1'],
'security': ['pyOpenSSL>=0.14', 'cryptography>=1.3.4', 'idna>=2.0.0'],
'socks': ['PySocks>=1.5.6, !=1.5.7'],
},
)
+151 -3
View File
@@ -156,6 +156,11 @@ class TestRequests:
data=u"ööö".encode("utf-8")).prepare()
assert isinstance(request.body, bytes)
def test_whitespaces_are_removed_from_url(self):
# Test for issue #3696
request = requests.Request('GET', ' http://example.com').prepare()
assert request.url == 'http://example.com/'
@pytest.mark.parametrize('scheme', ('http://', 'HTTP://', 'hTTp://', 'HttP://'))
def test_mixed_case_scheme_acceptable(self, httpbin, scheme):
s = requests.Session()
@@ -505,6 +510,20 @@ class TestRequests:
r = s.get(url)
assert r.status_code == 200
@pytest.mark.parametrize(
'username, password', (
('user', 'pass'),
(u'имя'.encode('utf-8'), u'пароль'.encode('utf-8')),
))
def test_set_basicauth(self, httpbin, username, password):
auth = (username, password)
url = httpbin('get')
r = requests.Request('GET', url, auth=auth)
p = r.prepare()
assert p.headers['Authorization'] == _basic_auth_str(username, password)
@pytest.mark.parametrize(
'url, exception', (
# Connecting to an unknown domain should raise a ConnectionError
@@ -1105,6 +1124,10 @@ class TestRequests:
total_seconds = ((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6)
assert total_seconds > 0.0
def test_empty_response_has_content_none(self):
r = requests.Response()
assert r.content is None
def test_response_is_iterable(self):
r = requests.Response()
io = StringIO.StringIO('abc')
@@ -1613,10 +1636,15 @@ class TestRequests:
self._patch_adapter_gzipped_redirect(s, url)
s.get(url)
def test_basic_auth_str_is_always_native(self):
s = _basic_auth_str("test", "test")
@pytest.mark.parametrize(
'username, password, auth_str', (
('test', 'test', 'Basic dGVzdDp0ZXN0'),
(u'имя'.encode('utf-8'), u'пароль'.encode('utf-8'), 'Basic 0LjQvNGPOtC/0LDRgNC+0LvRjA=='),
))
def test_basic_auth_str_is_always_native(self, username, password, auth_str):
s = _basic_auth_str(username, password)
assert isinstance(s, builtin_str)
assert s == "Basic dGVzdDp0ZXN0"
assert s == auth_str
def test_requests_history_is_saved(self, httpbin):
r = requests.get(httpbin('redirect/5'))
@@ -1790,6 +1818,41 @@ class TestRequests:
assert resp_with_cert.raw._pool.key_file == key
assert resp.raw._pool is not resp_with_cert.raw._pool
def test_empty_stream_with_auth_does_not_set_content_length_header(self, httpbin):
"""Ensure that a byte stream with size 0 will not set both a Content-Length
and Transfer-Encoding header.
"""
auth = ('user', 'pass')
url = httpbin('post')
file_obj = io.BytesIO(b'')
r = requests.Request('POST', url, auth=auth, data=file_obj)
prepared_request = r.prepare()
assert 'Transfer-Encoding' in prepared_request.headers
assert 'Content-Length' not in prepared_request.headers
def test_stream_with_auth_does_not_set_transfer_encoding_header(self, httpbin):
"""Ensure that a byte stream with size > 0 will not set both a Content-Length
and Transfer-Encoding header.
"""
auth = ('user', 'pass')
url = httpbin('post')
file_obj = io.BytesIO(b'test data')
r = requests.Request('POST', url, auth=auth, data=file_obj)
prepared_request = r.prepare()
assert 'Transfer-Encoding' not in prepared_request.headers
assert 'Content-Length' in prepared_request.headers
def test_chunked_upload_does_not_set_content_length_header(self, httpbin):
"""Ensure that requests with a generator body stream using
Transfer-Encoding: chunked, not a Content-Length header.
"""
data = (i for i in [b'a', b'b', b'c'])
url = httpbin('post')
r = requests.Request('POST', url, data=data)
prepared_request = r.prepare()
assert 'Transfer-Encoding' in prepared_request.headers
assert 'Content-Length' not in prepared_request.headers
class TestCaseInsensitiveDict:
@@ -2242,6 +2305,7 @@ class TestPreparingURLs(object):
(
('http://google.com', 'http://google.com/'),
(u'http://ジェーピーニック.jp', u'http://xn--hckqz9bzb1cyrb.jp/'),
(u'http://xn--n3h.net/', u'http://xn--n3h.net/'),
(
u'http://ジェーピーニック.jp'.encode('utf-8'),
u'http://xn--hckqz9bzb1cyrb.jp/'
@@ -2262,6 +2326,18 @@ class TestPreparingURLs(object):
u'http://Königsgäßchen.de/straße'.encode('utf-8'),
u'http://xn--knigsgchen-b4a3dun.de/stra%C3%9Fe'
),
(
b'http://xn--n3h.net/',
u'http://xn--n3h.net/'
),
(
b'http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/',
u'http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/'
),
(
u'http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/',
u'http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/'
)
)
)
def test_preparing_url(self, url, expected):
@@ -2276,9 +2352,81 @@ class TestPreparingURLs(object):
b"http://*",
u"http://*.google.com",
u"http://*",
u"http://☃.net/"
)
)
def test_preparing_bad_url(self, url):
r = requests.Request('GET', url=url)
with pytest.raises(requests.exceptions.InvalidURL):
r.prepare()
@pytest.mark.parametrize(
'input, expected',
(
(
b"http+unix://%2Fvar%2Frun%2Fsocket/path",
u"http+unix://%2fvar%2frun%2fsocket/path",
),
(
u"http+unix://%2Fvar%2Frun%2Fsocket/path",
u"http+unix://%2fvar%2frun%2fsocket/path",
),
(
b"mailto:user@example.org",
u"mailto:user@example.org",
),
(
u"mailto:user@example.org",
u"mailto:user@example.org",
),
(
b"data:SSDimaUgUHl0aG9uIQ==",
u"data:SSDimaUgUHl0aG9uIQ==",
)
)
)
def test_url_mutation(self, input, expected):
"""
This test validates that we correctly exclude some URLs from
preparation, and that we handle others. Specifically, it tests that
any URL whose scheme doesn't begin with "http" is left alone, and
those whose scheme *does* begin with "http" are mutated.
"""
r = requests.Request('GET', url=input)
p = r.prepare()
assert p.url == expected
@pytest.mark.parametrize(
'input, params, expected',
(
(
b"http+unix://%2Fvar%2Frun%2Fsocket/path",
{"key": "value"},
u"http+unix://%2fvar%2frun%2fsocket/path?key=value",
),
(
u"http+unix://%2Fvar%2Frun%2Fsocket/path",
{"key": "value"},
u"http+unix://%2fvar%2frun%2fsocket/path?key=value",
),
(
b"mailto:user@example.org",
{"key": "value"},
u"mailto:user@example.org",
),
(
u"mailto:user@example.org",
{"key": "value"},
u"mailto:user@example.org",
),
)
)
def test_parameters_for_nonstandard_schemes(self, input, params, expected):
"""
Setting paramters for nonstandard schemes is allowed if those schemes
begin with "http", and is forbidden otherwise.
"""
r = requests.Request('GET', url=input, params=params)
p = r.prepare()
assert p.url == expected
+13
View File
@@ -17,6 +17,7 @@ from requests.utils import (
to_key_val_list, to_native_string,
unquote_header_value, unquote_unreserved,
urldefragauth, add_dict_to_cookiejar)
from requests._internal_utils import unicode_is_ascii
from .compat import StringIO, cStringIO
@@ -515,6 +516,7 @@ def test_should_bypass_proxies(url, expected, monkeypatch):
monkeypatch.setenv('NO_PROXY', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1')
assert should_bypass_proxies(url) == expected
@pytest.mark.parametrize(
'cookiejar', (
compat.cookielib.CookieJar(),
@@ -529,3 +531,14 @@ def test_add_dict_to_cookiejar(cookiejar):
cj = add_dict_to_cookiejar(cookiejar, cookiedict)
cookies = dict((cookie.name, cookie.value) for cookie in cj)
assert cookiedict == cookies
@pytest.mark.parametrize(
'value, expected', (
(u'test', True),
(u'æíöû', False),
(u'ジェーピーニック', False),
)
)
def test_unicode_is_ascii(value, expected):
assert unicode_is_ascii(value) is expected