diff --git a/.gitignore b/.gitignore index b2abf8c2..19ebfd79 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ nosetests.xml junit-report.xml pylint.txt toy.py +.cache/ cover/ build/ docs/_build diff --git a/HISTORY.rst b/HISTORY.rst index 6582073b..45111752 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,11 @@ Release History dev +++ +**Improvements** + +- ``Response`` is now a context manager, so can be used directly in a `with` statement + without first having to be wrapped by ``contextlib.closing()``. + **Bugfixes** - Resolve installation failure if multiprocessing is not available diff --git a/Makefile b/Makefile index f90248f7..92d1c344 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci: py.test -n 8 --boxed --junitxml=report.xml test-readme: - python setup.py check -r -s + @python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.rst and HISTORY.rst ok") || echo "Invalid markup in README.rst or HISTORY.rst!" flake8: flake8 --ignore=E501,F401,E128,E402,E731,F821 requests diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 2aac434c..a1b68707 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -301,15 +301,11 @@ release the connection back to the pool unless you consume all the data or call :meth:`Response.close `. This can lead to inefficiency with connections. If you find yourself partially reading request bodies (or not reading them at all) while using ``stream=True``, you should -consider using ``contextlib.closing`` (`documented here`_), like this:: +make the request within a ``with`` statement to ensure it's always closed:: - from contextlib import closing - - with closing(requests.get('http://httpbin.org/get', stream=True)) as r: + with requests.get('http://httpbin.org/get', stream=True) as r: # Do things with the response here. -.. _`documented here`: http://docs.python.org/2/library/contextlib.html#contextlib.closing - .. _keep-alive: Keep-Alive diff --git a/requests/__init__.py b/requests/__init__.py index d4461ec9..90a0d11a 100644 --- a/requests/__init__.py +++ b/requests/__init__.py @@ -40,34 +40,44 @@ is at . :license: Apache 2.0, see LICENSE for more details. """ -# Check urllib3 for compatibility. import urllib3 -urllib3_version = urllib3.__version__.split('.') -# Sometimes, urllib3 only reports its version as 16.1. -if len(urllib3_version) == 2: - urllib3_version.append('0') -major, minor, patch = urllib3_version -major, minor, patch = int(major), int(minor), int(patch) -# urllib3 >= 1.21.1, < 1.22 -try: +import chardet +import warnings +from .exceptions import RequestsDependencyWarning + + +def check_compatibility(urllib3_version, chardet_version): + urllib3_version = urllib3_version.split('.') + assert urllib3_version != ['dev'] # Verify urllib3 isn't installed from git. + + # Sometimes, urllib3 only reports its version as 16.1. + if len(urllib3_version) == 2: + urllib3_version.append('0') + + # Check urllib3 for compatibility. + major, minor, patch = urllib3_version # noqa: F811 + major, minor, patch = int(major), int(minor), int(patch) + # urllib3 >= 1.21.1, < 1.22 assert major == 1 assert minor >= 21 assert minor <= 22 -except AssertionError: - raise RuntimeError('Requests dependency \'urllib3\' must be version >= 1.21.1, < 1.22!') - -# Check chardet for compatibility. -import chardet -major, minor, patch = chardet.__version__.split('.')[:3] -major, minor, patch = int(major), int(minor), int(patch) -# chardet >= 3.0.2, < 3.1.0 -try: + # Check chardet for compatibility. + major, minor, patch = chardet_version.split('.')[:3] + major, minor, patch = int(major), int(minor), int(patch) + # chardet >= 3.0.2, < 3.1.0 assert major == 3 assert minor < 1 assert patch >= 2 -except AssertionError: - raise RuntimeError('Requests dependency \'chardet\' must be version >= 3.0.2, < 3.1.0!') + + +# Check imported dependencies for compatibility. +try: + check_compatibility(urllib3.__version__, chardet.__version__) +except (AssertionError, ValueError): + warnings.warn("urllib3 ({0}) or chardet ({1}) doesn't match a supported " + "version!".format(urllib3.__version__, chardet.__version__), + RequestsDependencyWarning) # Attempt to enable urllib3's SNI support, if possible try: @@ -76,8 +86,6 @@ try: except ImportError: pass -import warnings - # urllib3's DependencyWarnings should be silenced. from urllib3.exceptions import DependencyWarning warnings.simplefilter('ignore', DependencyWarning) diff --git a/requests/api.py b/requests/api.py index b2ce1a96..bc2115c1 100644 --- a/requests/api.py +++ b/requests/api.py @@ -73,7 +73,7 @@ def get(url, params=None, **kwargs): def options(url, **kwargs): - r"""Sends a OPTIONS request. + r"""Sends an OPTIONS request. :param url: URL for the new :class:`Request` object. :param \*\*kwargs: Optional arguments that ``request`` takes. diff --git a/requests/exceptions.py b/requests/exceptions.py index da5af10b..be7eaed6 100644 --- a/requests/exceptions.py +++ b/requests/exceptions.py @@ -115,3 +115,8 @@ class RequestsWarning(Warning): class FileModeWarning(RequestsWarning, DeprecationWarning): """A file was opened in text mode, but Requests determined its binary length.""" pass + + +class RequestsDependencyWarning(RequestsWarning): + """An imported dependency doesn't match the expected version range.""" + pass diff --git a/requests/models.py b/requests/models.py index 1375a3af..2148024f 100644 --- a/requests/models.py +++ b/requests/models.py @@ -634,6 +634,12 @@ class Response(object): #: is a response. self.request = None + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + def __getstate__(self): # Consume everything; accessing the content attribute makes # sure the content has been fully read. diff --git a/tests/test_requests.py b/tests/test_requests.py index b8350cb7..365ffcb2 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1668,6 +1668,12 @@ class TestRequests: next(it) assert len(list(it)) == 3 + def test_response_context_manager(self, httpbin): + with requests.get(httpbin('stream/4'), stream=True) as response: + assert isinstance(response, requests.Response) + + assert response.raw.closed + def test_unconsumed_session_response_closes_connection(self, httpbin): s = requests.session()