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/AUTHORS.rst b/AUTHORS.rst old mode 100755 new mode 100644 index 7e0bddf0..c5218c6a --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -183,3 +183,5 @@ Patches and Suggestions - Shmuel Amar (`@shmuelamar `_) - Gary Wu (`@garywu `_) - Ryan Pineo (`@ryanpineo `_) +- Ed Morley (`@edmorley `_) +- Matt Liu (`@mlcrazy `_) diff --git a/HISTORY.rst b/HISTORY.rst index c7f82e03..45111752 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,21 @@ 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 +- Resolve tests crash if multiprocessing is not able to determine the number of CPU cores +- Resolve error swallowing in utils set_environ generator + + 2.17.3 (2017-05-29) +++++++++++++++++++ @@ -1467,4 +1482,3 @@ This is not a backwards compatible change. * Frustration * Conception - 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/help.py b/requests/help.py index ac0691e3..695b5ee2 100644 --- a/requests/help.py +++ b/requests/help.py @@ -23,7 +23,7 @@ else: def _implementation(): - """Return a dict with the Python implementation and verison. + """Return a dict with the Python implementation and version. Provide both the name and the version of the Python implementation currently running. For example, on CPython 2.7.5 it will return 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/requests/sessions.py b/requests/sessions.py old mode 100755 new mode 100644 diff --git a/requests/utils.py b/requests/utils.py index 25af9923..1e4960d7 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -612,18 +612,18 @@ def set_environ(env_name, value): the environment variable 'env_name'. If 'value' is None, do nothing""" - if value is not None: + value_changed = value is not None + if value_changed: old_value = os.environ.get(env_name) os.environ[env_name] = value try: yield finally: - if value is None: - return - if old_value is None: - del os.environ[env_name] - else: - os.environ[env_name] = old_value + if value_changed: + if old_value is None: + del os.environ[env_name] + else: + os.environ[env_name] = old_value def should_bypass_proxies(url, no_proxy): diff --git a/setup.py b/setup.py index 193c8877..93a85077 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ from codecs import open from setuptools import setup from setuptools.command.test import test as TestCommand -from multiprocessing import cpu_count here = os.path.abspath(os.path.dirname(__file__)) @@ -17,7 +16,11 @@ class PyTest(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) - self.pytest_args = ['-n', str(cpu_count()), '--boxed'] + try: + from multiprocessing import cpu_count + self.pytest_args = ['-n', str(cpu_count()), '--boxed'] + except (ImportError, NotImplementedError): + self.pytest_args = ['-n', '1', '--boxed'] def finalize_options(self): TestCommand.finalize_options(self) diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py old mode 100755 new mode 100644 diff --git a/tests/test_requests.py b/tests/test_requests.py old mode 100755 new mode 100644 index b8350cb7..cfafc6e4 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """Tests for Requests.""" @@ -1668,6 +1667,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() @@ -2364,7 +2369,7 @@ class TestPreparingURLs(object): ) def test_parameters_for_nonstandard_schemes(self, input, params, expected): """ - Setting paramters for nonstandard schemes is allowed if those schemes + Setting parameters for nonstandard schemes is allowed if those schemes begin with "http", and is forbidden otherwise. """ r = requests.Request('GET', url=input, params=params) diff --git a/tests/test_utils.py b/tests/test_utils.py index 41858b37..b3f398ee 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import copy from io import BytesIO import pytest @@ -17,7 +18,7 @@ from requests.utils import ( requote_uri, select_proxy, should_bypass_proxies, super_len, to_key_val_list, to_native_string, unquote_header_value, unquote_unreserved, - urldefragauth, add_dict_to_cookiejar) + urldefragauth, add_dict_to_cookiejar, set_environ) from requests._internal_utils import unicode_is_ascii from .compat import StringIO, cStringIO @@ -651,4 +652,29 @@ def test_should_bypass_proxies_win_registry(url, expected, override, monkeypatch.setenv('NO_PROXY', '') monkeypatch.setattr(winreg, 'OpenKey', OpenKey) monkeypatch.setattr(winreg, 'QueryValueEx', QueryValueEx) - assert should_bypass_proxies(url, no_proxy=None) == expected + + +@pytest.mark.parametrize( + 'env_name, value', ( + ('no_proxy', '192.168.0.0/24,127.0.0.1,localhost.localdomain'), + ('no_proxy', None), + ('a_new_key', '192.168.0.0/24,127.0.0.1,localhost.localdomain'), + ('a_new_key', None), + )) +def test_set_environ(env_name, value): + """Tests set_environ will set environ values and will restore the environ.""" + environ_copy = copy.deepcopy(os.environ) + with set_environ(env_name, value): + assert os.environ.get(env_name) == value + + assert os.environ == environ_copy + + +def test_set_environ_raises_exception(): + """Tests set_environ will raise exceptions in context when the + value parameter is None.""" + with pytest.raises(Exception) as exception: + with set_environ('test1', None): + raise Exception('Expected exception') + + assert 'Expected exception' in str(exception.value)