From 0b3c8030fb776a3ef7f00d7b2deade5bcf8f52e4 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 22:32:17 +0000 Subject: New upstream release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 78a0a64..5ad9464 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-urllib3 (1.9-1) UNRELEASED; urgency=medium + + * New upstream release + + -- Daniele Tricoli Mon, 01 Sep 2014 00:27:23 +0200 + python-urllib3 (1.8.3-1) unstable; urgency=medium * New upstream release (Closes: #754090) -- cgit v1.2.3 From 4dc005d9567d988b37c751ebfb65b18ecd4514ce Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 22:43:19 +0000 Subject: Refresh 01_do-not-use-embedded-python-six.patch --- debian/changelog | 4 ++- .../01_do-not-use-embedded-python-six.patch | 34 +++++++++++----------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/debian/changelog b/debian/changelog index 5ad9464..ba7b433 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,10 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium * New upstream release + * debian/patches/01_do-not-use-embedded-python-six.patch + - Refresh - -- Daniele Tricoli Mon, 01 Sep 2014 00:27:23 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 00:42:17 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/patches/01_do-not-use-embedded-python-six.patch b/debian/patches/01_do-not-use-embedded-python-six.patch index 024180f..6a6f7f9 100644 --- a/debian/patches/01_do-not-use-embedded-python-six.patch +++ b/debian/patches/01_do-not-use-embedded-python-six.patch @@ -1,7 +1,7 @@ Description: Do not use embedded copy of python-six. Author: Daniele Tricoli Forwarded: not-needed -Last-Update: 2014-07-7 +Last-Update: 2014-09-01 --- a/test/test_collections.py +++ b/test/test_collections.py @@ -16,8 +16,8 @@ Last-Update: 2014-07-7 --- a/urllib3/connectionpool.py +++ b/urllib3/connectionpool.py -@@ -31,7 +31,7 @@ - ProxyError, +@@ -27,7 +27,7 @@ + InsecureRequestWarning, ) from .packages.ssl_match_hostname import CertificateError -from .packages import six @@ -27,7 +27,7 @@ Last-Update: 2014-07-7 DummyConnection, --- a/urllib3/filepost.py +++ b/urllib3/filepost.py -@@ -9,8 +9,8 @@ +@@ -3,8 +3,8 @@ from uuid import uuid4 from io import BytesIO @@ -40,14 +40,14 @@ Last-Update: 2014-07-7 writer = codecs.lookup('utf-8')[3] --- a/urllib3/response.py +++ b/urllib3/response.py -@@ -11,7 +11,7 @@ +@@ -4,7 +4,7 @@ from ._collections import HTTPHeaderDict - from .exceptions import DecodeError, ReadTimeoutError + from .exceptions import ProtocolError, DecodeError, ReadTimeoutError -from .packages.six import string_types as basestring, binary_type +from six import string_types as basestring, binary_type - from .util import is_fp_closed - + from .connection import HTTPException, BaseSSLError + from .util.response import is_fp_closed --- a/test/test_filepost.py +++ b/test/test_filepost.py @@ -62,7 +62,7 @@ Last-Update: 2014-07-7 BOUNDARY = '!! test boundary !!' --- a/dummyserver/handlers.py +++ b/dummyserver/handlers.py -@@ -190,7 +190,7 @@ +@@ -211,7 +211,7 @@ """ import tornado.httputil import email.utils @@ -73,7 +73,7 @@ Last-Update: 2014-07-7 parts = tornado.httputil._parseparam(';' + line) --- a/urllib3/fields.py +++ b/urllib3/fields.py -@@ -7,7 +7,7 @@ +@@ -1,7 +1,7 @@ import email.utils import mimetypes @@ -95,7 +95,7 @@ Last-Update: 2014-07-7 class TestRequestField(unittest.TestCase): --- a/urllib3/_collections.py +++ b/urllib3/_collections.py -@@ -20,7 +20,7 @@ +@@ -14,7 +14,7 @@ from collections import OrderedDict except ImportError: from .packages.ordered_dict import OrderedDict @@ -106,22 +106,22 @@ Last-Update: 2014-07-7 __all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] --- a/urllib3/connection.py +++ b/urllib3/connection.py -@@ -34,7 +34,7 @@ +@@ -28,7 +28,7 @@ ConnectTimeoutError, ) from .packages.ssl_match_hostname import match_hostname -from .packages import six +import six - from .util import ( - assert_fingerprint, + + from .util.ssl_ import ( resolve_cert_reqs, --- a/urllib3/util/request.py +++ b/urllib3/util/request.py @@ -1,6 +1,6 @@ from base64 import b64encode --from ..packages import six -+import six - +-from ..packages.six import b ++from six import b ACCEPT_ENCODING = 'gzip,deflate' + -- cgit v1.2.3 From 5074da0fb36b78402877d7626982a70aafe2e5dd Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 22:48:53 +0000 Subject: Refresh 02_require-cert-verification.patch --- debian/changelog | 4 +++- debian/patches/02_require-cert-verification.patch | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/debian/changelog b/debian/changelog index ba7b433..792afaf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,8 +3,10 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium * New upstream release * debian/patches/01_do-not-use-embedded-python-six.patch - Refresh + * debian/patches/02_require-cert-verification.patch + - Refresh - -- Daniele Tricoli Mon, 01 Sep 2014 00:42:17 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 00:48:18 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/patches/02_require-cert-verification.patch b/debian/patches/02_require-cert-verification.patch index 1c09012..1b5992c 100644 --- a/debian/patches/02_require-cert-verification.patch +++ b/debian/patches/02_require-cert-verification.patch @@ -3,11 +3,11 @@ Description: require SSL certificate validation by default by using CERT_REQUIRED and using the system /etc/ssl/certs/ca-certificates.crt Bug-Ubuntu: https://launchpad.net/bugs/1047054 Bug-Debian: http://bugs.debian.org/686872 -Last-Update: 2014-05-24 +Last-Update: 2014-09-01 --- a/urllib3/connectionpool.py +++ b/urllib3/connectionpool.py -@@ -591,6 +591,8 @@ +@@ -628,6 +628,8 @@ ``ssl_version`` are only used if :mod:`ssl` is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade the connection socket into an SSL socket. @@ -16,9 +16,9 @@ Last-Update: 2014-05-24 """ scheme = 'https' -@@ -600,8 +602,8 @@ - strict=False, timeout=None, maxsize=1, - block=False, headers=None, +@@ -637,8 +639,8 @@ + strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, + block=False, headers=None, retries=None, _proxy=None, _proxy_headers=None, - key_file=None, cert_file=None, cert_reqs=None, - ca_certs=None, ssl_version=None, -- cgit v1.2.3 From 696eb71f386cc0c21cde533fd2572b69823bddad Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 23:02:07 +0000 Subject: Refresh 05_do-not-use-embedded-ssl-match-hostname.patch --- debian/changelog | 4 +++- .../05_do-not-use-embedded-ssl-match-hostname.patch | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/debian/changelog b/debian/changelog index 792afaf..10eed10 100644 --- a/debian/changelog +++ b/debian/changelog @@ -5,8 +5,10 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium - Refresh * debian/patches/02_require-cert-verification.patch - Refresh + * debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch + - Refresh - -- Daniele Tricoli Mon, 01 Sep 2014 00:48:18 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 01:00:03 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch b/debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch index 9ee4cb9..17c858d 100644 --- a/debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch +++ b/debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch @@ -1,14 +1,14 @@ Description: Do not use embedded copy of ssl.match_hostname. Author: Daniele Tricoli Forwarded: not-needed -Last-Update: 2014-05-25 +Last-Update: 2014-09-01 --- a/test/test_connectionpool.py +++ b/test/test_connectionpool.py @@ -6,7 +6,7 @@ HTTPConnectionPool, ) - from urllib3.util import Timeout + from urllib3.util.timeout import Timeout -from urllib3.packages.ssl_match_hostname import CertificateError +from ssl import CertificateError from urllib3.exceptions import ( @@ -16,20 +16,20 @@ Last-Update: 2014-05-25 EmptyPoolError, --- a/urllib3/connection.py +++ b/urllib3/connection.py -@@ -38,7 +38,7 @@ +@@ -27,7 +27,7 @@ from .exceptions import ( ConnectTimeoutError, ) -from .packages.ssl_match_hostname import match_hostname +from ssl import match_hostname import six - from .util import ( - assert_fingerprint, + + from .util.ssl_ import ( --- a/urllib3/connectionpool.py +++ b/urllib3/connectionpool.py -@@ -31,7 +31,7 @@ - ReadTimeoutError, - ProxyError, +@@ -26,7 +26,7 @@ + TimeoutError, + InsecureRequestWarning, ) -from .packages.ssl_match_hostname import CertificateError +from ssl import CertificateError @@ -45,7 +45,7 @@ Last-Update: 2014-05-25 --- a/setup.py +++ b/setup.py -@@ -45,7 +45,7 @@ +@@ -42,7 +42,7 @@ url='http://urllib3.readthedocs.org/', license='MIT', packages=['urllib3', @@ -53,4 +53,4 @@ Last-Update: 2014-05-25 + 'urllib3.packages', 'urllib3.contrib', 'urllib3.util', ], - requires=requirements, + requires=[], -- cgit v1.2.3 From 491940f4268d7350b072abd9428ad854dc9719ef Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 23:12:10 +0000 Subject: Remove since upstream now does not specify version of packages needed for testing inside setup.py --- debian/changelog | 5 ++++- debian/patches/06_relax-test-requirements.patch | 16 ---------------- 2 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 debian/patches/06_relax-test-requirements.patch diff --git a/debian/changelog b/debian/changelog index 10eed10..24735f2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -7,8 +7,11 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium - Refresh * debian/patches/05_do-not-use-embedded-ssl-match-hostname.patch - Refresh + * debian/patches/06_relax-test-requirements.patch + - Remove since upstream now does not specify version of packages needed + for testing inside setup.py - -- Daniele Tricoli Mon, 01 Sep 2014 01:00:03 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 01:07:27 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/patches/06_relax-test-requirements.patch b/debian/patches/06_relax-test-requirements.patch deleted file mode 100644 index e0fe46c..0000000 --- a/debian/patches/06_relax-test-requirements.patch +++ /dev/null @@ -1,16 +0,0 @@ -Description: Relax version of packages needed for testing. -Author: Daniele Tricoli -Forwarded: not-needed -Last-Update: 2014-05-25 - ---- a/test-requirements.txt -+++ b/test-requirements.txt -@@ -1,4 +1,4 @@ --nose==1.3 --mock==1.0.1 --tornado==3.1.1 --coverage==3.6 -+nose>=1.3 -+mock>=1.0.1 -+tornado>=3.1.1 -+coverage>=3.6 -- cgit v1.2.3 From e2a0148455ca86805c5ed1fe17dc777a04dec9fb Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 23:14:29 +0000 Subject: Remove 06_relax-test-requirements.patch from debian/patches/series --- debian/patches/series | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/patches/series b/debian/patches/series index 6bb1581..cddf757 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -3,4 +3,3 @@ 03_force_setuptools.patch 04_relax_nosetests_options.patch 05_do-not-use-embedded-ssl-match-hostname.patch -06_relax-test-requirements.patch -- cgit v1.2.3 From b9e530268e3856d9bf310958eda83ee894f34532 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 23:22:11 +0000 Subject: Fix a forgotten six import --- debian/patches/01_do-not-use-embedded-python-six.patch | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/patches/01_do-not-use-embedded-python-six.patch b/debian/patches/01_do-not-use-embedded-python-six.patch index 6a6f7f9..40fa594 100644 --- a/debian/patches/01_do-not-use-embedded-python-six.patch +++ b/debian/patches/01_do-not-use-embedded-python-six.patch @@ -125,3 +125,14 @@ Last-Update: 2014-09-01 ACCEPT_ENCODING = 'gzip,deflate' +--- a/urllib3/util/retry.py ++++ b/urllib3/util/retry.py +@@ -7,7 +7,7 @@ + ReadTimeoutError, + MaxRetryError, + ) +-from ..packages import six ++import six + + + log = logging.getLogger(__name__) -- cgit v1.2.3 From 4c62daa1059e5972f158ee38bb9360fee96e92b3 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sun, 31 Aug 2014 23:51:26 +0000 Subject: Refresh --- debian/patches/01_do-not-use-embedded-python-six.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/patches/01_do-not-use-embedded-python-six.patch b/debian/patches/01_do-not-use-embedded-python-six.patch index 40fa594..0774786 100644 --- a/debian/patches/01_do-not-use-embedded-python-six.patch +++ b/debian/patches/01_do-not-use-embedded-python-six.patch @@ -136,3 +136,13 @@ Last-Update: 2014-09-01 log = logging.getLogger(__name__) +--- a/test/test_retry.py ++++ b/test/test_retry.py +@@ -1,6 +1,6 @@ + import unittest + +-from urllib3.packages.six.moves import xrange ++from six.moves import xrange + from urllib3.util.retry import Retry + from urllib3.exceptions import ( + ConnectTimeoutError, -- cgit v1.2.3 From 726f01eb2b27c81ad5f5df01bbabaa912d0fafe5 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Mon, 1 Sep 2014 00:07:27 +0000 Subject: * debian/patches/06_add-test-init-py.patch - Add needed test/__init__.py file not shipped in sdist --- debian/changelog | 4 +- debian/patches/06_add-test-init-py.patch | 99 ++++++++++++++++++++++++++++++++ debian/patches/series | 1 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 debian/patches/06_add-test-init-py.patch diff --git a/debian/changelog b/debian/changelog index 24735f2..869893b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -10,8 +10,10 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium * debian/patches/06_relax-test-requirements.patch - Remove since upstream now does not specify version of packages needed for testing inside setup.py + * debian/patches/06_add-test-init-py.patch + - Add needed test/__init__.py file not shipped in sdist - -- Daniele Tricoli Mon, 01 Sep 2014 01:07:27 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 02:03:47 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/patches/06_add-test-init-py.patch b/debian/patches/06_add-test-init-py.patch new file mode 100644 index 0000000..2cbbc02 --- /dev/null +++ b/debian/patches/06_add-test-init-py.patch @@ -0,0 +1,99 @@ +Description: Add needed test/__init__.py file not shipped in sdist. +Origin: https://raw.githubusercontent.com/shazow/urllib3/1.9/test/__init__.py +Bug: https://github.com/shazow/urllib3/issues/440 + +--- /dev/null ++++ b/test/__init__.py +@@ -0,0 +1,92 @@ ++import warnings ++import sys ++import errno ++import functools ++import socket ++ ++from nose.plugins.skip import SkipTest ++ ++from urllib3.exceptions import MaxRetryError, HTTPWarning ++import six ++ ++# We need a host that will not immediately close the connection with a TCP ++# Reset. SO suggests this hostname ++TARPIT_HOST = '10.255.255.1' ++ ++VALID_SOURCE_ADDRESSES = [('::1', 0), ('127.0.0.1', 0)] ++# RFC 5737: 192.0.2.0/24 is for testing only. ++# RFC 3849: 2001:db8::/32 is for documentation only. ++INVALID_SOURCE_ADDRESSES = [('192.0.2.255', 0), ('2001:db8::1', 0)] ++ ++ ++def clear_warnings(cls=HTTPWarning): ++ new_filters = [] ++ for f in warnings.filters: ++ if issubclass(f[2], cls): ++ continue ++ new_filters.append(f) ++ warnings.filters[:] = new_filters ++ ++def setUp(): ++ clear_warnings() ++ warnings.simplefilter('ignore', HTTPWarning) ++ ++ ++def onlyPy26OrOlder(test): ++ """Skips this test unless you are on Python2.6.x or earlier.""" ++ ++ @functools.wraps(test) ++ def wrapper(*args, **kwargs): ++ msg = "{name} only runs on Python2.6.x or older".format(name=test.__name__) ++ if sys.version_info >= (2, 7): ++ raise SkipTest(msg) ++ return test(*args, **kwargs) ++ return wrapper ++ ++def onlyPy27OrNewer(test): ++ """Skips this test unless you are on Python 2.7.x or later.""" ++ ++ @functools.wraps(test) ++ def wrapper(*args, **kwargs): ++ msg = "{name} requires Python 2.7.x+ to run".format(name=test.__name__) ++ if sys.version_info < (2, 7): ++ raise SkipTest(msg) ++ return test(*args, **kwargs) ++ return wrapper ++ ++def onlyPy3(test): ++ """Skips this test unless you are on Python3.x""" ++ ++ @functools.wraps(test) ++ def wrapper(*args, **kwargs): ++ msg = "{name} requires Python3.x to run".format(name=test.__name__) ++ if not six.PY3: ++ raise SkipTest(msg) ++ return test(*args, **kwargs) ++ return wrapper ++ ++def requires_network(test): ++ """Helps you skip tests that require the network""" ++ ++ def _is_unreachable_err(err): ++ return getattr(err, 'errno', None) in (errno.ENETUNREACH, ++ errno.EHOSTUNREACH) # For OSX ++ ++ @functools.wraps(test) ++ def wrapper(*args, **kwargs): ++ msg = "Can't run {name} because the network is unreachable".format( ++ name=test.__name__) ++ try: ++ return test(*args, **kwargs) ++ except socket.error as e: ++ # This test needs an initial network connection to attempt the ++ # connection to the TARPIT_HOST. This fails if you are in a place ++ # without an Internet connection, so we skip the test in that case. ++ if _is_unreachable_err(e): ++ raise SkipTest(msg) ++ raise ++ except MaxRetryError as e: ++ if _is_unreachable_err(e.reason): ++ raise SkipTest(msg) ++ raise ++ return wrapper diff --git a/debian/patches/series b/debian/patches/series index cddf757..f8be250 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -3,3 +3,4 @@ 03_force_setuptools.patch 04_relax_nosetests_options.patch 05_do-not-use-embedded-ssl-match-hostname.patch +06_add-test-init-py.patch -- cgit v1.2.3 From a13add89b1d3ab63524fda763fe2cd75578adf57 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Mon, 1 Sep 2014 01:07:53 +0000 Subject: * debian/control - Add python-ndg-httpsclient, python-openssl and python-pyasn1 into python-urllib3's Recomends to ensure that SNI works as expected and to prevent CRIME attack - Add python3-ndg-httpsclient, python3-openssl and python3-pyasn1 into python3-urllib3's Suggests since Python 3 already support SNI and and SSL compression can be disabled using OP_NO_COMPRESSION --- debian/changelog | 9 ++++++++- debian/control | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 869893b..1da9b1f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,13 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium * New upstream release + * debian/control + - Add python-ndg-httpsclient, python-openssl and python-pyasn1 into + python-urllib3's Recomends to ensure that SNI works as expected and to + prevent CRIME attack + - Add python3-ndg-httpsclient, python3-openssl and python3-pyasn1 into + python3-urllib3's Suggests since Python 3 already support SNI and + and SSL compression can be disabled using OP_NO_COMPRESSION * debian/patches/01_do-not-use-embedded-python-six.patch - Refresh * debian/patches/02_require-cert-verification.patch @@ -13,7 +20,7 @@ python-urllib3 (1.9-1) UNRELEASED; urgency=medium * debian/patches/06_add-test-init-py.patch - Add needed test/__init__.py file not shipped in sdist - -- Daniele Tricoli Mon, 01 Sep 2014 02:03:47 +0200 + -- Daniele Tricoli Mon, 01 Sep 2014 02:56:44 +0200 python-urllib3 (1.8.3-1) unstable; urgency=medium diff --git a/debian/control b/debian/control index 87415bb..d11c0b3 100644 --- a/debian/control +++ b/debian/control @@ -33,7 +33,10 @@ Depends: ${python:Depends}, python-six Recommends: - ca-certificates + ca-certificates, + python-ndg-httpsclient, + python-openssl, + python-pyasn1 Description: HTTP library with thread-safe connection pooling for Python urllib3 supports features left out of urllib and urllib2 libraries. . @@ -55,6 +58,10 @@ Depends: python3-six Recommends: ca-certificates +Suggests: + python3-ndg-httpsclient, + python3-openssl, + python3-pyasn1 Description: HTTP library with thread-safe connection pooling for Python3 urllib3 supports features left out of urllib and urllib2 libraries. . -- cgit v1.2.3 From 331904bfea8957d9e6353fc96c1ab53eb1c95d8d Mon Sep 17 00:00:00 2001 From: Piotr Ożarowski Date: Mon, 8 Sep 2014 19:54:32 +0000 Subject: s/UNRELEASED/unstable/ --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 1da9b1f..93324ca 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python-urllib3 (1.9-1) UNRELEASED; urgency=medium +python-urllib3 (1.9-1) unstable; urgency=medium * New upstream release * debian/control -- cgit v1.2.3 From 0f393d00b51bc54c5075447e4a8b21f0bed6acd8 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 13:19:39 -0700 Subject: Imported Upstream version 1.9 --- CHANGES.rst | 37 ++++++ LICENSE.txt | 2 +- MANIFEST.in | 3 +- Makefile | 47 +++++++ PKG-INFO | 85 ++++++++++-- README.rst | 46 +++++-- dev-requirements.txt | 5 + dummyserver/handlers.py | 28 +++- dummyserver/server.py | 2 +- dummyserver/testcase.py | 7 + setup.py | 13 +- test-requirements.txt | 4 - test/test_connectionpool.py | 11 +- test/test_poolmanager.py | 5 +- test/test_response.py | 5 + test/test_retry.py | 156 ++++++++++++++++++++++ test/test_util.py | 30 ++++- urllib3.egg-info/PKG-INFO | 85 ++++++++++-- urllib3.egg-info/SOURCES.txt | 5 +- urllib3/__init__.py | 26 ++-- urllib3/_collections.py | 6 - urllib3/connection.py | 51 ++++--- urllib3/connectionpool.py | 182 ++++++++++++++++--------- urllib3/contrib/ntlmpool.py | 6 - urllib3/contrib/pyopenssl.py | 202 +++------------------------- urllib3/exceptions.py | 37 ++++-- urllib3/fields.py | 8 +- urllib3/filepost.py | 6 - urllib3/packages/ordered_dict.py | 1 - urllib3/poolmanager.py | 41 +++--- urllib3/request.py | 8 +- urllib3/response.py | 31 +++-- urllib3/util/__init__.py | 9 +- urllib3/util/connection.py | 56 +++++++- urllib3/util/request.py | 13 +- urllib3/util/retry.py | 279 +++++++++++++++++++++++++++++++++++++++ urllib3/util/ssl_.py | 5 +- urllib3/util/timeout.py | 67 +++++----- urllib3/util/url.py | 9 +- 39 files changed, 1151 insertions(+), 468 deletions(-) create mode 100644 Makefile create mode 100644 dev-requirements.txt delete mode 100644 test-requirements.txt create mode 100644 test/test_retry.py create mode 100644 urllib3/util/retry.py diff --git a/CHANGES.rst b/CHANGES.rst index 4d90ce2..9ada9c2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,43 @@ Changes ======= +1.9 (2014-07-04) +++++++++++++++++ + +* Shuffled around development-related files. If you're maintaining a distro + package of urllib3, you may need to tweak things. (Issue #415) + +* Unverified HTTPS requests will trigger a warning on the first request. See + our new `security documentation + `_ for details. + (Issue #426) + +* New retry logic and ``urllib3.util.retry.Retry`` configuration object. + (Issue #326) + +* All raised exceptions should now wrapped in a + ``urllib3.exceptions.HTTPException``-extending exception. (Issue #326) + +* All errors during a retry-enabled request should be wrapped in + ``urllib3.exceptions.MaxRetryError``, including timeout-related exceptions + which were previously exempt. Underlying error is accessible from the + ``.reason`` propery. (Issue #326) + +* ``urllib3.exceptions.ConnectionError`` renamed to + ``urllib3.exceptions.ProtocolError``. (Issue #326) + +* Errors during response read (such as IncompleteRead) are now wrapped in + ``urllib3.exceptions.ProtocolError``. (Issue #418) + +* Requesting an empty host will raise ``urllib3.exceptions.LocationValueError``. + (Issue #417) + +* Catch read timeouts over SSL connections as + ``urllib3.exceptions.ReadTimeoutError``. (Issue #419) + +* Apply socket arguments before connecting. (Issue #427) + + 1.8.3 (2014-06-23) ++++++++++++++++++ diff --git a/LICENSE.txt b/LICENSE.txt index 31f0b6c..2a02593 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ This is the MIT license: http://www.opensource.org/licenses/mit-license.php -Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software diff --git a/MANIFEST.in b/MANIFEST.in index 3c2189a..6b37d64 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ -include README.rst CHANGES.rst LICENSE.txt CONTRIBUTORS.txt test-requirements.txt +include README.rst CHANGES.rst LICENSE.txt CONTRIBUTORS.txt dev-requirements.txt Makefile recursive-include dummyserver *.* -prune *.pyc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6cdcfb --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +REQUIREMENTS_FILE=dev-requirements.txt +REQUIREMENTS_OUT=dev-requirements.txt.log +SETUP_OUT=*.egg-info + + +all: setup requirements + +virtualenv: +ifndef VIRTUAL_ENV + $(error Must be run inside of a virtualenv) +endif + +setup: virtualenv $(SETUP_OUT) + +$(SETUP_OUT): setup.py setup.cfg + python setup.py develop + touch $(SETUP_OUT) + +requirements: setup $(REQUIREMENTS_OUT) + +piprot: setup + pip install piprot + piprot -x $(REQUIREMENTS_FILE) + +$(REQUIREMENTS_OUT): $(REQUIREMENTS_FILE) + pip install -r $(REQUIREMENTS_FILE) | tee -a $(REQUIREMENTS_OUT) + python setup.py develop + +clean: + find . -name "*.py[oc]" -delete + find . -name "__pycache__" -delete + rm -f $(REQUIREMENTS_OUT) + +test: requirements + nosetests + +test-all: requirements + tox + +docs: + cd docs && pip install -r doc-requirements.txt && make html + +release: + ./release.sh + + +.PHONY: docs diff --git a/PKG-INFO b/PKG-INFO index 8e4fc2f..168944c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: urllib3 -Version: 1.8.3 +Version: 1.9 Summary: HTTP library with thread-safe connection pooling, file post, and more. Home-page: http://urllib3.readthedocs.org/ Author: Andrey Petrov @@ -25,16 +25,18 @@ Description: ======= - Supports gzip and deflate decoding. - Thread-safe and sanity-safe. - Works with AppEngine, gevent, and eventlib. - - Tested on Python 2.6+ and Python 3.2+, 100% unit test coverage. + - Tested on Python 2.6+, Python 3.2+, and PyPy, with 100% unit test coverage. - Small and easy to understand codebase perfect for extending and building upon. For a more comprehensive solution, have a look at `Requests `_ which is also powered by ``urllib3``. - + + You might already be using urllib3! =================================== - ``urllib3`` powers `many great Python libraries `_, - including ``pip`` and ``requests``. + ``urllib3`` powers `many great Python libraries + `_, including ``pip`` and + ``requests``. What's wrong with urllib and urllib2? @@ -50,6 +52,7 @@ Description: ======= solving a different scope of problems, and ``urllib3`` follows in a similar vein. + Why do I want to reuse connections? =================================== @@ -71,6 +74,7 @@ Description: ======= retrying is useful. It's relatively lightweight, so it can be used for anything! + Examples ======== @@ -89,26 +93,39 @@ Description: ======= The ``PoolManager`` will take care of reusing connections for you whenever you request the same host. For more fine-grained control of your connection - pools, you should look at - `ConnectionPool `_. + pools, you should look at `ConnectionPool + `_. Run the tests ============= We use some external dependencies, multiple interpreters and code coverage - analysis while running test suite. Easiest way to run the tests is thusly the - ``tox`` utility: :: + analysis while running test suite. Our ``Makefile`` handles much of this for + you as long as you're running it `inside of a virtualenv + `_:: + + $ make test + [... magically installs dependencies and runs tests on your virtualenv] + Ran 182 tests in 1.633s + + OK (SKIP=6) - $ tox - # [..] + Note that code coverage less than 100% is regarded as a failing run. Some + platform-specific tests are skipped unless run in that platform. To make sure + the code works in all of urllib3's supported platforms, you can run our ``tox`` + suite:: + + $ make test-all + [... tox creates a virtualenv for every platform and runs tests inside of each] py26: commands succeeded py27: commands succeeded py32: commands succeeded py33: commands succeeded py34: commands succeeded - Note that code coverage less than 100% is regarded as a failing run. + Our test suite `runs continuously on Travis CI + `_ with every pull request. Contributing @@ -126,9 +143,53 @@ Description: ======= :) Make sure to add yourself to ``CONTRIBUTORS.txt``. + Sponsorship + =========== + + If your company benefits from this library, please consider `sponsoring its + development `_. + + Changes ======= + 1.9 (2014-07-04) + ++++++++++++++++ + + * Shuffled around development-related files. If you're maintaining a distro + package of urllib3, you may need to tweak things. (Issue #415) + + * Unverified HTTPS requests will trigger a warning on the first request. See + our new `security documentation + `_ for details. + (Issue #426) + + * New retry logic and ``urllib3.util.retry.Retry`` configuration object. + (Issue #326) + + * All raised exceptions should now wrapped in a + ``urllib3.exceptions.HTTPException``-extending exception. (Issue #326) + + * All errors during a retry-enabled request should be wrapped in + ``urllib3.exceptions.MaxRetryError``, including timeout-related exceptions + which were previously exempt. Underlying error is accessible from the + ``.reason`` propery. (Issue #326) + + * ``urllib3.exceptions.ConnectionError`` renamed to + ``urllib3.exceptions.ProtocolError``. (Issue #326) + + * Errors during response read (such as IncompleteRead) are now wrapped in + ``urllib3.exceptions.ProtocolError``. (Issue #418) + + * Requesting an empty host will raise ``urllib3.exceptions.LocationValueError``. + (Issue #417) + + * Catch read timeouts over SSL connections as + ``urllib3.exceptions.ReadTimeoutError``. (Issue #419) + + * Apply socket arguments before connecting. (Issue #427) + + 1.8.3 (2014-06-23) ++++++++++++++++++ diff --git a/README.rst b/README.rst index e76c261..6a81759 100644 --- a/README.rst +++ b/README.rst @@ -17,16 +17,18 @@ Highlights - Supports gzip and deflate decoding. - Thread-safe and sanity-safe. - Works with AppEngine, gevent, and eventlib. -- Tested on Python 2.6+ and Python 3.2+, 100% unit test coverage. +- Tested on Python 2.6+, Python 3.2+, and PyPy, with 100% unit test coverage. - Small and easy to understand codebase perfect for extending and building upon. For a more comprehensive solution, have a look at `Requests `_ which is also powered by ``urllib3``. - + + You might already be using urllib3! =================================== -``urllib3`` powers `many great Python libraries `_, -including ``pip`` and ``requests``. +``urllib3`` powers `many great Python libraries +`_, including ``pip`` and +``requests``. What's wrong with urllib and urllib2? @@ -42,6 +44,7 @@ with each other. They were designed to be independent and standalone, each solving a different scope of problems, and ``urllib3`` follows in a similar vein. + Why do I want to reuse connections? =================================== @@ -63,6 +66,7 @@ This library is perfect for: retrying is useful. It's relatively lightweight, so it can be used for anything! + Examples ======== @@ -81,26 +85,39 @@ But, long story short:: The ``PoolManager`` will take care of reusing connections for you whenever you request the same host. For more fine-grained control of your connection -pools, you should look at -`ConnectionPool `_. +pools, you should look at `ConnectionPool +`_. Run the tests ============= We use some external dependencies, multiple interpreters and code coverage -analysis while running test suite. Easiest way to run the tests is thusly the -``tox`` utility: :: +analysis while running test suite. Our ``Makefile`` handles much of this for +you as long as you're running it `inside of a virtualenv +`_:: + + $ make test + [... magically installs dependencies and runs tests on your virtualenv] + Ran 182 tests in 1.633s - $ tox - # [..] + OK (SKIP=6) + +Note that code coverage less than 100% is regarded as a failing run. Some +platform-specific tests are skipped unless run in that platform. To make sure +the code works in all of urllib3's supported platforms, you can run our ``tox`` +suite:: + + $ make test-all + [... tox creates a virtualenv for every platform and runs tests inside of each] py26: commands succeeded py27: commands succeeded py32: commands succeeded py33: commands succeeded py34: commands succeeded -Note that code coverage less than 100% is regarded as a failing run. +Our test suite `runs continuously on Travis CI +`_ with every pull request. Contributing @@ -116,3 +133,10 @@ Contributing as expected. #. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to ``CONTRIBUTORS.txt``. + + +Sponsorship +=========== + +If your company benefits from this library, please consider `sponsoring its +development `_. diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..6de0e09 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +nose==1.3.3 +mock==1.0.1 +tornado==3.2.2 +coverage==3.7.1 +tox==1.7.1 diff --git a/dummyserver/handlers.py b/dummyserver/handlers.py index 5d6e2e6..72faa1a 100644 --- a/dummyserver/handlers.py +++ b/dummyserver/handlers.py @@ -1,5 +1,6 @@ from __future__ import print_function +import collections import gzip import json import logging @@ -41,11 +42,12 @@ class TestingApp(WSGIHandler): Simple app that performs various operations, useful for testing an HTTP library. - Given any path, it will attempt to convert it will load a corresponding - local method if it exists. Status code 200 indicates success, 400 indicates - failure. Each method has its own conditions for success/failure. + Given any path, it will attempt to load a corresponding local method if + it exists. Status code 200 indicates success, 400 indicates failure. Each + method has its own conditions for success/failure. """ def __call__(self, environ, start_response): + """ Call the correct method in this class based on the incoming URI """ req = HTTPRequest(environ) req.params = {} @@ -172,6 +174,25 @@ class TestingApp(WSGIHandler): def headers(self, request): return Response(json.dumps(request.headers)) + def successful_retry(self, request): + """ Handler which will return an error and then success + + It's not currently very flexible as the number of retries is hard-coded. + """ + test_name = request.headers.get('test-name', None) + if not test_name: + return Response("test-name header not set", + status="400 Bad Request") + + if not hasattr(self, 'retry_test_names'): + self.retry_test_names = collections.defaultdict(int) + self.retry_test_names[test_name] += 1 + + if self.retry_test_names[test_name] >= 2: + return Response("Retry successful!") + else: + return Response("need to keep retrying!", status="418 I'm A Teapot") + def shutdown(self, request): sys.exit() @@ -207,7 +228,6 @@ def _parse_header(line): params.pop(0) # get rid of the dummy again pdict = {} for name, value in params: - print(repr(value)) value = email.utils.collapse_rfc2231_value(value) if len(value) >= 2 and value[0] == '"' and value[-1] == '"': value = value[1:-1] diff --git a/dummyserver/server.py b/dummyserver/server.py index 22de456..99f0835 100755 --- a/dummyserver/server.py +++ b/dummyserver/server.py @@ -58,7 +58,7 @@ class SocketServerThread(threading.Thread): self.port = sock.getsockname()[1] # Once listen() returns, the server socket is ready - sock.listen(1) + sock.listen(0) if self.ready_event: self.ready_event.set() diff --git a/dummyserver/testcase.py b/dummyserver/testcase.py index 35769ef..335b2f2 100644 --- a/dummyserver/testcase.py +++ b/dummyserver/testcase.py @@ -40,6 +40,13 @@ class SocketDummyServerTestCase(unittest.TestCase): class HTTPDummyServerTestCase(unittest.TestCase): + """ A simple HTTP server that runs when your test class runs + + Have your unittest class inherit from this one, and then a simple server + will start when your tests run, and automatically shut down when they + complete. For examples of what test requests you can send to the server, + see the TestingApp in dummyserver/handlers.py. + """ scheme = 'http' host = 'localhost' host_alt = '127.0.0.1' # Some tests need two hosts diff --git a/setup.py b/setup.py index 92fad33..f638377 100644 --- a/setup.py +++ b/setup.py @@ -21,9 +21,6 @@ fp.close() version = VERSION -requirements = [] -tests_requirements = requirements + open('test-requirements.txt').readlines() - setup(name='urllib3', version=version, description="HTTP library with thread-safe connection pooling, file post, and more.", @@ -48,7 +45,13 @@ setup(name='urllib3', 'urllib3.packages', 'urllib3.packages.ssl_match_hostname', 'urllib3.contrib', 'urllib3.util', ], - requires=requirements, - tests_require=tests_requirements, + requires=[], + tests_require=[ + # These are a less-specific subset of dev-requirements.txt, for the + # convenience of distro package maintainers. + 'nose', + 'mock', + 'tornado', + ], test_suite='test', ) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 02d70f4..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nose==1.3 -mock==1.0.1 -tornado==3.1.1 -coverage==3.6 diff --git a/test/test_connectionpool.py b/test/test_connectionpool.py index 02229cf..28fb89b 100644 --- a/test/test_connectionpool.py +++ b/test/test_connectionpool.py @@ -5,13 +5,15 @@ from urllib3.connectionpool import ( HTTPConnection, HTTPConnectionPool, ) -from urllib3.util import Timeout +from urllib3.util.timeout import Timeout from urllib3.packages.ssl_match_hostname import CertificateError from urllib3.exceptions import ( ClosedPoolError, EmptyPoolError, HostChangedError, + LocationValueError, MaxRetryError, + ProtocolError, SSLError, ) @@ -127,7 +129,7 @@ class TestConnectionPool(unittest.TestCase): HTTPConnectionPool(host='localhost'), "Test.", err)), "HTTPConnectionPool(host='localhost', port=None): " "Max retries exceeded with url: Test. " - "(Caused by {0}: Test)".format(str(err.__class__))) + "(Caused by %r)" % err) def test_pool_size(self): @@ -186,7 +188,6 @@ class TestConnectionPool(unittest.TestCase): self.assertRaises(Empty, old_pool_queue.get, block=False) - def test_pool_timeouts(self): pool = HTTPConnectionPool(host='localhost') conn = pool._new_conn() @@ -201,6 +202,10 @@ class TestConnectionPool(unittest.TestCase): self.assertEqual(pool.timeout._connect, 3) self.assertEqual(pool.timeout.total, None) + def test_no_host(self): + self.assertRaises(LocationValueError, HTTPConnectionPool, None) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py index 759b5e3..754ee8a 100644 --- a/test/test_poolmanager.py +++ b/test/test_poolmanager.py @@ -4,7 +4,7 @@ from urllib3.poolmanager import PoolManager from urllib3 import connection_from_url from urllib3.exceptions import ( ClosedPoolError, - LocationParseError, + LocationValueError, ) @@ -68,7 +68,8 @@ class TestPoolManager(unittest.TestCase): def test_nohost(self): p = PoolManager(5) - self.assertRaises(LocationParseError, p.connection_from_url, 'http://@') + self.assertRaises(LocationValueError, p.connection_from_url, 'http://@') + self.assertRaises(LocationValueError, p.connection_from_url, None) if __name__ == '__main__': diff --git a/test/test_response.py b/test/test_response.py index ecfcbee..ad134ee 100644 --- a/test/test_response.py +++ b/test/test_response.py @@ -131,6 +131,11 @@ class TestResponse(unittest.TestCase): self.assertEqual(r.read(1), b'f') self.assertEqual(r.read(2), b'oo') + def test_body_blob(self): + resp = HTTPResponse(b'foo') + self.assertEqual(resp.data, b'foo') + self.assertTrue(resp.closed) + def test_io(self): import socket try: diff --git a/test/test_retry.py b/test/test_retry.py new file mode 100644 index 0000000..7a3aa40 --- /dev/null +++ b/test/test_retry.py @@ -0,0 +1,156 @@ +import unittest + +from urllib3.packages.six.moves import xrange +from urllib3.util.retry import Retry +from urllib3.exceptions import ( + ConnectTimeoutError, + ReadTimeoutError, + MaxRetryError +) + + +class RetryTest(unittest.TestCase): + + def test_string(self): + """ Retry string representation looks the way we expect """ + retry = Retry() + self.assertEqual(str(retry), 'Retry(total=10, connect=None, read=None, redirect=None)') + for _ in range(3): + retry = retry.increment() + self.assertEqual(str(retry), 'Retry(total=7, connect=None, read=None, redirect=None)') + + def test_retry_both_specified(self): + """Total can win if it's lower than the connect value""" + error = ConnectTimeoutError() + retry = Retry(connect=3, total=2) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + try: + retry.increment(error=error) + self.fail("Failed to raise error.") + except MaxRetryError as e: + self.assertEqual(e.reason, error) + + def test_retry_higher_total_loses(self): + """ A lower connect timeout than the total is honored """ + error = ConnectTimeoutError() + retry = Retry(connect=2, total=3) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + self.assertRaises(MaxRetryError, retry.increment, error=error) + + def test_retry_higher_total_loses_vs_read(self): + """ A lower read timeout than the total is honored """ + error = ReadTimeoutError(None, "/", "read timed out") + retry = Retry(read=2, total=3) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + self.assertRaises(MaxRetryError, retry.increment, error=error) + + def test_retry_total_none(self): + """ if Total is none, connect error should take precedence """ + error = ConnectTimeoutError() + retry = Retry(connect=2, total=None) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + try: + retry.increment(error=error) + self.fail("Failed to raise error.") + except MaxRetryError as e: + self.assertEqual(e.reason, error) + + error = ReadTimeoutError(None, "/", "read timed out") + retry = Retry(connect=2, total=None) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + self.assertFalse(retry.is_exhausted()) + + def test_retry_default(self): + """ If no value is specified, should retry connects 3 times """ + retry = Retry() + self.assertEqual(retry.total, 10) + self.assertEqual(retry.connect, None) + self.assertEqual(retry.read, None) + self.assertEqual(retry.redirect, None) + + error = ConnectTimeoutError() + retry = Retry(connect=1) + retry = retry.increment(error=error) + self.assertRaises(MaxRetryError, retry.increment, error=error) + + retry = Retry(connect=1) + retry = retry.increment(error=error) + self.assertFalse(retry.is_exhausted()) + + self.assertTrue(Retry(0).raise_on_redirect) + self.assertFalse(Retry(False).raise_on_redirect) + + def test_retry_read_zero(self): + """ No second chances on read timeouts, by default """ + error = ReadTimeoutError(None, "/", "read timed out") + retry = Retry(read=0) + try: + retry.increment(error=error) + self.fail("Failed to raise error.") + except MaxRetryError as e: + self.assertEqual(e.reason, error) + + def test_backoff(self): + """ Backoff is computed correctly """ + max_backoff = Retry.BACKOFF_MAX + + retry = Retry(total=100, backoff_factor=0.2) + self.assertEqual(retry.get_backoff_time(), 0) # First request + + retry = retry.increment() + self.assertEqual(retry.get_backoff_time(), 0) # First retry + + retry = retry.increment() + self.assertEqual(retry.backoff_factor, 0.2) + self.assertEqual(retry.total, 98) + self.assertEqual(retry.get_backoff_time(), 0.4) # Start backoff + + retry = retry.increment() + self.assertEqual(retry.get_backoff_time(), 0.8) + + retry = retry.increment() + self.assertEqual(retry.get_backoff_time(), 1.6) + + for i in xrange(10): + retry = retry.increment() + + self.assertEqual(retry.get_backoff_time(), max_backoff) + + def test_zero_backoff(self): + retry = Retry() + self.assertEqual(retry.get_backoff_time(), 0) + retry = retry.increment() + retry = retry.increment() + self.assertEqual(retry.get_backoff_time(), 0) + + def test_sleep(self): + # sleep a very small amount of time so our code coverage is happy + retry = Retry(backoff_factor=0.0001) + retry = retry.increment() + retry = retry.increment() + retry.sleep() + + def test_status_forcelist(self): + retry = Retry(status_forcelist=xrange(500,600)) + self.assertFalse(retry.is_forced_retry('GET', status_code=200)) + self.assertFalse(retry.is_forced_retry('GET', status_code=400)) + self.assertTrue(retry.is_forced_retry('GET', status_code=500)) + + retry = Retry(total=1, status_forcelist=[418]) + self.assertFalse(retry.is_forced_retry('GET', status_code=400)) + self.assertTrue(retry.is_forced_retry('GET', status_code=418)) + + def test_exhausted(self): + self.assertFalse(Retry(0).is_exhausted()) + self.assertTrue(Retry(-1).is_exhausted()) + self.assertEqual(Retry(1).increment().total, 0) + + def test_disabled(self): + self.assertRaises(MaxRetryError, Retry(-1).increment) + self.assertRaises(MaxRetryError, Retry(0).increment) diff --git a/test/test_util.py b/test/test_util.py index 944d90f..388d877 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,20 +1,27 @@ +import warnings import logging import unittest import ssl from mock import patch -from urllib3 import add_stderr_logger -from urllib3.util import ( +from urllib3 import add_stderr_logger, disable_warnings +from urllib3.util.request import make_headers +from urllib3.util.timeout import Timeout +from urllib3.util.url import ( get_host, - make_headers, - split_first, parse_url, - Timeout, + split_first, Url, - resolve_cert_reqs, ) -from urllib3.exceptions import LocationParseError, TimeoutStateError +from urllib3.util.ssl_ import resolve_cert_reqs +from urllib3.exceptions import ( + LocationParseError, + TimeoutStateError, + InsecureRequestWarning, +) + +from . import clear_warnings # This number represents a time in seconds, it doesn't mean anything in # isolation. Setting to a high-ish value to avoid conflicts with the smaller @@ -203,6 +210,15 @@ class TestUtil(unittest.TestCase): logger.debug('Testing add_stderr_logger') logger.removeHandler(handler) + def test_disable_warnings(self): + with warnings.catch_warnings(record=True) as w: + clear_warnings() + warnings.warn('This is a test.', InsecureRequestWarning) + self.assertEqual(len(w), 1) + disable_warnings() + warnings.warn('This is a test.', InsecureRequestWarning) + self.assertEqual(len(w), 1) + def _make_time_pass(self, seconds, timeout, time_mock): """ Make some time pass for the timeout object """ time_mock.return_value = TIMEOUT_EPOCH diff --git a/urllib3.egg-info/PKG-INFO b/urllib3.egg-info/PKG-INFO index 8e4fc2f..168944c 100644 --- a/urllib3.egg-info/PKG-INFO +++ b/urllib3.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: urllib3 -Version: 1.8.3 +Version: 1.9 Summary: HTTP library with thread-safe connection pooling, file post, and more. Home-page: http://urllib3.readthedocs.org/ Author: Andrey Petrov @@ -25,16 +25,18 @@ Description: ======= - Supports gzip and deflate decoding. - Thread-safe and sanity-safe. - Works with AppEngine, gevent, and eventlib. - - Tested on Python 2.6+ and Python 3.2+, 100% unit test coverage. + - Tested on Python 2.6+, Python 3.2+, and PyPy, with 100% unit test coverage. - Small and easy to understand codebase perfect for extending and building upon. For a more comprehensive solution, have a look at `Requests `_ which is also powered by ``urllib3``. - + + You might already be using urllib3! =================================== - ``urllib3`` powers `many great Python libraries `_, - including ``pip`` and ``requests``. + ``urllib3`` powers `many great Python libraries + `_, including ``pip`` and + ``requests``. What's wrong with urllib and urllib2? @@ -50,6 +52,7 @@ Description: ======= solving a different scope of problems, and ``urllib3`` follows in a similar vein. + Why do I want to reuse connections? =================================== @@ -71,6 +74,7 @@ Description: ======= retrying is useful. It's relatively lightweight, so it can be used for anything! + Examples ======== @@ -89,26 +93,39 @@ Description: ======= The ``PoolManager`` will take care of reusing connections for you whenever you request the same host. For more fine-grained control of your connection - pools, you should look at - `ConnectionPool `_. + pools, you should look at `ConnectionPool + `_. Run the tests ============= We use some external dependencies, multiple interpreters and code coverage - analysis while running test suite. Easiest way to run the tests is thusly the - ``tox`` utility: :: + analysis while running test suite. Our ``Makefile`` handles much of this for + you as long as you're running it `inside of a virtualenv + `_:: + + $ make test + [... magically installs dependencies and runs tests on your virtualenv] + Ran 182 tests in 1.633s + + OK (SKIP=6) - $ tox - # [..] + Note that code coverage less than 100% is regarded as a failing run. Some + platform-specific tests are skipped unless run in that platform. To make sure + the code works in all of urllib3's supported platforms, you can run our ``tox`` + suite:: + + $ make test-all + [... tox creates a virtualenv for every platform and runs tests inside of each] py26: commands succeeded py27: commands succeeded py32: commands succeeded py33: commands succeeded py34: commands succeeded - Note that code coverage less than 100% is regarded as a failing run. + Our test suite `runs continuously on Travis CI + `_ with every pull request. Contributing @@ -126,9 +143,53 @@ Description: ======= :) Make sure to add yourself to ``CONTRIBUTORS.txt``. + Sponsorship + =========== + + If your company benefits from this library, please consider `sponsoring its + development `_. + + Changes ======= + 1.9 (2014-07-04) + ++++++++++++++++ + + * Shuffled around development-related files. If you're maintaining a distro + package of urllib3, you may need to tweak things. (Issue #415) + + * Unverified HTTPS requests will trigger a warning on the first request. See + our new `security documentation + `_ for details. + (Issue #426) + + * New retry logic and ``urllib3.util.retry.Retry`` configuration object. + (Issue #326) + + * All raised exceptions should now wrapped in a + ``urllib3.exceptions.HTTPException``-extending exception. (Issue #326) + + * All errors during a retry-enabled request should be wrapped in + ``urllib3.exceptions.MaxRetryError``, including timeout-related exceptions + which were previously exempt. Underlying error is accessible from the + ``.reason`` propery. (Issue #326) + + * ``urllib3.exceptions.ConnectionError`` renamed to + ``urllib3.exceptions.ProtocolError``. (Issue #326) + + * Errors during response read (such as IncompleteRead) are now wrapped in + ``urllib3.exceptions.ProtocolError``. (Issue #418) + + * Requesting an empty host will raise ``urllib3.exceptions.LocationValueError``. + (Issue #417) + + * Catch read timeouts over SSL connections as + ``urllib3.exceptions.ReadTimeoutError``. (Issue #419) + + * Apply socket arguments before connecting. (Issue #427) + + 1.8.3 (2014-06-23) ++++++++++++++++++ diff --git a/urllib3.egg-info/SOURCES.txt b/urllib3.egg-info/SOURCES.txt index fb93e5b..e0b9ddd 100644 --- a/urllib3.egg-info/SOURCES.txt +++ b/urllib3.egg-info/SOURCES.txt @@ -2,10 +2,11 @@ CHANGES.rst CONTRIBUTORS.txt LICENSE.txt MANIFEST.in +Makefile README.rst +dev-requirements.txt setup.cfg setup.py -test-requirements.txt dummyserver/__init__.py dummyserver/handlers.py dummyserver/proxy.py @@ -30,6 +31,7 @@ test/test_filepost.py test/test_poolmanager.py test/test_proxymanager.py test/test_response.py +test/test_retry.py test/test_util.py urllib3/__init__.py urllib3/_collections.py @@ -57,6 +59,7 @@ urllib3/util/__init__.py urllib3/util/connection.py urllib3/util/request.py urllib3/util/response.py +urllib3/util/retry.py urllib3/util/ssl_.py urllib3/util/timeout.py urllib3/util/url.py \ No newline at end of file diff --git a/urllib3/__init__.py b/urllib3/__init__.py index c80d5da..56f5bf4 100644 --- a/urllib3/__init__.py +++ b/urllib3/__init__.py @@ -1,16 +1,10 @@ -# urllib3/__init__.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - """ urllib3 - Thread-safe connection pooling and re-using. """ __author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' __license__ = 'MIT' -__version__ = '1.8.3' +__version__ = '1.9' from .connectionpool import ( @@ -23,7 +17,10 @@ from . import exceptions from .filepost import encode_multipart_formdata from .poolmanager import PoolManager, ProxyManager, proxy_from_url from .response import HTTPResponse -from .util import make_headers, get_host, Timeout +from .util.request import make_headers +from .util.url import get_host +from .util.timeout import Timeout +from .util.retry import Retry # Set default logging handler to avoid "No handler found" warnings. @@ -51,8 +48,19 @@ def add_stderr_logger(level=logging.DEBUG): handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) logger.addHandler(handler) logger.setLevel(level) - logger.debug('Added an stderr logging handler to logger: %s' % __name__) + logger.debug('Added a stderr logging handler to logger: %s' % __name__) return handler # ... Clean up. del NullHandler + + +# Set security warning to only go off once by default. +import warnings +warnings.simplefilter('module', exceptions.InsecureRequestWarning) + +def disable_warnings(category=exceptions.HTTPWarning): + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter('ignore', category) diff --git a/urllib3/_collections.py b/urllib3/_collections.py index ccf0d5f..d77ebb8 100644 --- a/urllib3/_collections.py +++ b/urllib3/_collections.py @@ -1,9 +1,3 @@ -# urllib3/_collections.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - from collections import Mapping, MutableMapping try: from threading import RLock diff --git a/urllib3/connection.py b/urllib3/connection.py index fbb63ed..0d578d7 100644 --- a/urllib3/connection.py +++ b/urllib3/connection.py @@ -1,9 +1,3 @@ -# urllib3/connection.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - import sys import socket from socket import timeout as SocketTimeout @@ -35,13 +29,16 @@ from .exceptions import ( ) from .packages.ssl_match_hostname import match_hostname from .packages import six -from .util import ( - assert_fingerprint, + +from .util.ssl_ import ( resolve_cert_reqs, resolve_ssl_version, ssl_wrap_socket, + assert_fingerprint, ) +from .util import connection + port_by_scheme = { 'http': 80, @@ -82,15 +79,23 @@ class HTTPConnection(_HTTPConnection, object): #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] + #: Whether this connection verifies the host's certificate. + is_verified = False + def __init__(self, *args, **kw): if six.PY3: # Python 3 kw.pop('strict', None) - if sys.version_info < (2, 7): # Python 2.6 and older - kw.pop('source_address', None) # Pre-set source_address in case we have an older Python like 2.6. self.source_address = kw.get('source_address') + if sys.version_info < (2, 7): # Python 2.6 + # _HTTPConnection on Python 2.6 will balk at this keyword arg, but + # not newer versions. We can still use it when creating a + # connection though, so we pop it *after* we have saved it as + # self.source_address. + kw.pop('source_address', None) + #: The socket options provided by the user. If no options are #: provided, we use the default options. self.socket_options = kw.pop('socket_options', self.default_socket_options) @@ -103,22 +108,22 @@ class HTTPConnection(_HTTPConnection, object): :return: New socket connection. """ - extra_args = [] - if self.source_address: # Python 2.7+ - extra_args.append(self.source_address) + extra_kw = {} + if self.source_address: + extra_kw['source_address'] = self.source_address + + if self.socket_options: + extra_kw['socket_options'] = self.socket_options try: - conn = socket.create_connection( - (self.host, self.port), self.timeout, *extra_args) + conn = connection.create_connection( + (self.host, self.port), self.timeout, **extra_kw) except SocketTimeout: raise ConnectTimeoutError( self, "Connection to %s timed out. (connect timeout=%s)" % (self.host, self.timeout)) - # Set options on the socket. - self._set_options_on(conn) - return conn def _prepare_conn(self, conn): @@ -132,14 +137,6 @@ class HTTPConnection(_HTTPConnection, object): # Mark this connection as not reusable self.auto_open = 0 - def _set_options_on(self, conn): - # Disable all socket options if the user passes ``socket_options=None`` - if self.socket_options is None: - return - - for opt in self.socket_options: - conn.setsockopt(*opt) - def connect(self): conn = self._new_conn() self._prepare_conn(conn) @@ -225,6 +222,8 @@ class VerifiedHTTPSConnection(HTTPSConnection): match_hostname(self.sock.getpeercert(), self.assert_hostname or hostname) + self.is_verified = resolved_cert_reqs == ssl.CERT_REQUIRED + if ssl: # Make a copy for testing. diff --git a/urllib3/connectionpool.py b/urllib3/connectionpool.py index ab205fa..9317fdc 100644 --- a/urllib3/connectionpool.py +++ b/urllib3/connectionpool.py @@ -1,12 +1,7 @@ -# urllib3/connectionpool.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -import sys import errno import logging +import sys +import warnings from socket import error as SocketError, timeout as SocketTimeout import socket @@ -20,15 +15,16 @@ except ImportError: from .exceptions import ( ClosedPoolError, - ConnectionError, + ProtocolError, EmptyPoolError, HostChangedError, - LocationParseError, + LocationValueError, MaxRetryError, + ProxyError, + ReadTimeoutError, SSLError, TimeoutError, - ReadTimeoutError, - ProxyError, + InsecureRequestWarning, ) from .packages.ssl_match_hostname import CertificateError from .packages import six @@ -40,11 +36,11 @@ from .connection import ( ) from .request import RequestMethods from .response import HTTPResponse -from .util import ( - get_host, - is_connection_dropped, - Timeout, -) + +from .util.connection import is_connection_dropped +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import get_host xrange = six.moves.xrange @@ -65,13 +61,11 @@ class ConnectionPool(object): QueueCls = LifoQueue def __init__(self, host, port=None): - if host is None: - raise LocationParseError(host) + if not host: + raise LocationValueError("No host specified.") # httplib doesn't like it when we include brackets in ipv6 addresses - host = host.strip('[]') - - self.host = host + self.host = host.strip('[]') self.port = port def __str__(self): @@ -126,6 +120,9 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): Headers to include with all requests, unless other headers are given explicitly. + :param retries: + Retry configuration to use by default with requests in this pool. + :param _proxy: Parsed proxy URL, should not be used directly, instead, see :class:`urllib3.connectionpool.ProxyManager`" @@ -144,18 +141,22 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): def __init__(self, host, port=None, strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, - headers=None, _proxy=None, _proxy_headers=None, **conn_kw): + headers=None, retries=None, + _proxy=None, _proxy_headers=None, + **conn_kw): ConnectionPool.__init__(self, host, port) RequestMethods.__init__(self, headers) self.strict = strict - # This is for backwards compatibility and can be removed once a timeout - # can only be set to a Timeout object if not isinstance(timeout, Timeout): timeout = Timeout.from_float(timeout) + if retries is None: + retries = Retry.DEFAULT + self.timeout = timeout + self.retries = retries self.pool = self.QueueCls(maxsize) self.block = block @@ -259,6 +260,12 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if conn: conn.close() + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + pass + def _get_timeout(self, timeout): """ Helper that always returns a :class:`urllib3.util.Timeout` """ if timeout is _Default: @@ -290,9 +297,12 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): self.num_requests += 1 timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() conn.timeout = timeout_obj.connect_timeout + + # Trigger any extra validation we need to do. + self._validate_conn(conn) + # conn.request() calls httplib.*.request, not the method in # urllib3.request. It also calls makefile (recv) on the socket. conn.request(method, url, **httplib_request_kw) @@ -301,7 +311,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): read_timeout = timeout_obj.read_timeout # App Engine doesn't have a sock attr - if hasattr(conn, 'sock'): + if getattr(conn, 'sock', None): # In Python 3 socket.py will catch EAGAIN and return None when you # try and read into the file pointer created by http.client, which # instead raises a BadStatusLine exception. Instead of catching @@ -309,8 +319,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # timeouts, check for a zero timeout before making the request. if read_timeout == 0: raise ReadTimeoutError( - self, url, - "Read timed out. (read timeout=%s)" % read_timeout) + self, url, "Read timed out. (read timeout=%s)" % read_timeout) if read_timeout is Timeout.DEFAULT_TIMEOUT: conn.sock.settimeout(socket.getdefaulttimeout()) else: # None or a value @@ -332,7 +341,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # http://bugs.python.org/issue10272 if 'timed out' in str(e) or \ 'did not complete (read)' in str(e): # Python 2.6 - raise ReadTimeoutError(self, url, "Read timed out.") + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % read_timeout) raise @@ -341,8 +351,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # have to specifically catch it and throw the timeout error if e.errno in _blocking_errnos: raise ReadTimeoutError( - self, url, - "Read timed out. (read timeout=%s)" % read_timeout) + self, url, "Read timed out. (read timeout=%s)" % read_timeout) raise @@ -388,7 +397,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): return (scheme, host, port) == (self.scheme, self.host, self.port) - def urlopen(self, method, url, body=None, headers=None, retries=3, + def urlopen(self, method, url, body=None, headers=None, retries=None, redirect=True, assert_same_host=True, timeout=_Default, pool_timeout=None, release_conn=None, **response_kw): """ @@ -422,9 +431,20 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): these headers completely replace any pool-specific headers. :param retries: - Number of retries to allow before raising a MaxRetryError exception. - If `False`, then retries are disabled and any exception is raised - immediately. + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. :param redirect: If True, automatically handle redirects (status codes 301, 302, @@ -463,15 +483,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if headers is None: headers = self.headers - if retries < 0 and retries is not False: - raise MaxRetryError(self, url) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) if release_conn is None: release_conn = response_kw.get('preload_content', True) # Check host if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries - 1) + raise HostChangedError(self, url, retries) conn = None @@ -487,10 +507,10 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): err = None try: - # Request a connection from the queue + # Request a connection from the queue. conn = self._get_conn(timeout=pool_timeout) - # Make the request on the httplib connection object + # Make the request on the httplib connection object. httplib_response = self._make_request(conn, method, url, timeout=timeout, body=body, headers=headers) @@ -529,21 +549,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): conn.close() conn = None - if not retries: - if isinstance(e, TimeoutError): - # TimeoutError is exempt from MaxRetryError-wrapping. - # FIXME: ... Not sure why. Add a reason here. - raise - - # Wrap unexpected exceptions with the most appropriate - # module-level exception and re-raise. - if isinstance(e, SocketError) and self.proxy: - raise ProxyError('Cannot connect to proxy.', e) - - if retries is False: - raise ConnectionError('Connection failed.', e) + stacktrace = sys.exc_info()[2] + if isinstance(e, SocketError) and self.proxy: + e = ProxyError('Cannot connect to proxy.', e) + elif isinstance(e, (SocketError, HTTPException)): + e = ProtocolError('Connection aborted.', e) - raise MaxRetryError(self, url, e) + retries = retries.increment(method, url, error=e, + _pool=self, _stacktrace=stacktrace) + retries.sleep() # Keep track of the error for the retry warning. err = e @@ -557,23 +571,43 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if not conn: # Try again - log.warning("Retrying (%d attempts remain) after connection " + log.warning("Retrying (%r) after connection " "broken by '%r': %s" % (retries, err, url)) - return self.urlopen(method, url, body, headers, retries - 1, + return self.urlopen(method, url, body, headers, retries, redirect, assert_same_host, timeout=timeout, pool_timeout=pool_timeout, release_conn=release_conn, **response_kw) # Handle redirect? redirect_location = redirect and response.get_redirect_location() - if redirect_location and retries is not False: + if redirect_location: if response.status == 303: method = 'GET' + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + raise + return response + log.info("Redirecting %s -> %s" % (url, redirect_location)) return self.urlopen(method, redirect_location, body, headers, - retries - 1, redirect, assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, **response_kw) + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + # Check if we should retry the HTTP response. + if retries.is_forced_retry(method, status_code=response.status): + retries = retries.increment(method, url, response=response, _pool=self) + retries.sleep() + log.info("Forced retry: %s" % url) + return self.urlopen(method, url, body, headers, + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) return response @@ -600,8 +634,8 @@ class HTTPSConnectionPool(HTTPConnectionPool): ConnectionCls = HTTPSConnection def __init__(self, host, port=None, - strict=False, timeout=None, maxsize=1, - block=False, headers=None, + strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, + block=False, headers=None, retries=None, _proxy=None, _proxy_headers=None, key_file=None, cert_file=None, cert_reqs=None, ca_certs=None, ssl_version=None, @@ -609,7 +643,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): **conn_kw): HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, - block, headers, _proxy, _proxy_headers, + block, headers, retries, _proxy, _proxy_headers, **conn_kw) self.key_file = key_file self.cert_file = cert_file @@ -640,7 +674,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): set_tunnel = conn.set_tunnel except AttributeError: # Platform-specific: Python 2.6 set_tunnel = conn._set_tunnel - + if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older set_tunnel(self.host, self.port) else: @@ -677,6 +711,24 @@ class HTTPSConnectionPool(HTTPConnectionPool): return self._prepare_conn(conn) + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + super(HTTPSConnectionPool, self)._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if not conn.sock: + conn.connect() + + if not conn.is_verified: + warnings.warn(( + 'Unverified HTTPS request is being made. ' + 'Adding certificate verification is strongly advised. See: ' + 'https://urllib3.readthedocs.org/en/latest/security.html ' + '(This warning will only appear once by default.)'), + InsecureRequestWarning) + def connection_from_url(url, **kw): """ @@ -693,7 +745,7 @@ def connection_from_url(url, **kw): :class:`.ConnectionPool`. Useful for specifying things like timeout, maxsize, headers, etc. - Example: :: + Example:: >>> conn = connection_from_url('http://google.com/') >>> r = conn.request('GET', '/') diff --git a/urllib3/contrib/ntlmpool.py b/urllib3/contrib/ntlmpool.py index b8cd933..c6b266f 100644 --- a/urllib3/contrib/ntlmpool.py +++ b/urllib3/contrib/ntlmpool.py @@ -1,9 +1,3 @@ -# urllib3/contrib/ntlmpool.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - """ NTLM authenticating pool, contributed by erikcederstran diff --git a/urllib3/contrib/pyopenssl.py b/urllib3/contrib/pyopenssl.py index 21a12c6..7a9ea2e 100644 --- a/urllib3/contrib/pyopenssl.py +++ b/urllib3/contrib/pyopenssl.py @@ -54,7 +54,6 @@ from pyasn1.type import univ, constraint from socket import _fileobject, timeout import ssl import select -from cStringIO import StringIO from .. import connection from .. import util @@ -155,196 +154,37 @@ def get_subj_alt_name(peer_cert): return dns_name -class fileobject(_fileobject): - - def _wait_for_sock(self): - rd, wd, ed = select.select([self._sock], [], [], - self._sock.gettimeout()) - if not rd: - raise timeout() - - - def read(self, size=-1): - # Use max, disallow tiny reads in a loop as they are very inefficient. - # We never leave read() with any leftover data from a new recv() call - # in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned by - # recv() minimizes memory usage and fragmentation that occurs when - # rbufsize is large compared to the typical return value of recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - if size < 0: - # Read until EOF - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. - while True: - try: - data = self._sock.recv(rbufsize) - except OpenSSL.SSL.WantReadError: - self._wait_for_sock() - continue - if not data: - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO() - self._rbuf.write(buf.read()) - return rv - - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. - while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - try: - data = self._sock.recv(left) - except OpenSSL.SSL.WantReadError: - self._wait_for_sock() - continue - if not data: - break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, "recv(%d) returned %d bytes" % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - #assert buf_len == buf.tell() - return buf.getvalue() - - def readline(self, size=-1): - buf = self._rbuf - buf.seek(0, 2) # seek end - if buf.tell() > 0: - # check if we already have it in our buffer - buf.seek(0) - bline = buf.readline(size) - if bline.endswith('\n') or len(bline) == size: - self._rbuf = StringIO() - self._rbuf.write(buf.read()) - return bline - del bline - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - buf.seek(0) - buffers = [buf.read()] - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. - data = None - recv = self._sock.recv - while True: - try: - while data != "\n": - data = recv(1) - if not data: - break - buffers.append(data) - except OpenSSL.SSL.WantReadError: - self._wait_for_sock() - continue - break - return "".join(buffers) - - buf.seek(0, 2) # seek end - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. - while True: - try: - data = self._sock.recv(self._rbufsize) - except OpenSSL.SSL.WantReadError: - self._wait_for_sock() - continue - if not data: - break - nl = data.find('\n') - if nl >= 0: - nl += 1 - buf.write(data[:nl]) - self._rbuf.write(data[nl:]) - del data - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or \n or EOF seen, whichever comes first - buf.seek(0, 2) # seek end - buf_len = buf.tell() - if buf_len >= size: - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO() - self._rbuf.write(buf.read()) - return rv - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. - while True: - try: - data = self._sock.recv(self._rbufsize) - except OpenSSL.SSL.WantReadError: - self._wait_for_sock() - continue - if not data: - break - left = size - buf_len - # did we just receive a newline? - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - # save the excess data to _rbuf - self._rbuf.write(data[nl:]) - if buf_len: - buf.write(data[:nl]) - break - else: - # Shortcut. Avoid data copy through buf when returning - # a substring of our first recv(). - return data[:nl] - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid data copy through buf when - # returning exactly all of our first recv(). - return data - if n >= left: - buf.write(data[:left]) - self._rbuf.write(data[left:]) - break - buf.write(data) - buf_len += n - #assert buf_len == buf.tell() - return buf.getvalue() - - class WrappedSocket(object): '''API-compatibility wrapper for Python OpenSSL's Connection-class.''' - def __init__(self, connection, socket): + def __init__(self, connection, socket, suppress_ragged_eofs=True): self.connection = connection self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs def fileno(self): return self.socket.fileno() def makefile(self, mode, bufsize=-1): - return fileobject(self.connection, mode, bufsize) + return _fileobject(self, mode, bufsize) + + def recv(self, *args, **kwargs): + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + return b'' + else: + raise + except OpenSSL.SSL.WantReadError: + rd, wd, ed = select.select( + [self.socket], [], [], self.socket.gettimeout()) + if not rd: + raise timeout() + else: + return self.recv(*args, **kwargs) + else: + return data def settimeout(self, timeout): return self.socket.settimeout(timeout) diff --git a/urllib3/exceptions.py b/urllib3/exceptions.py index b4df831..fff8bfa 100644 --- a/urllib3/exceptions.py +++ b/urllib3/exceptions.py @@ -1,9 +1,3 @@ -# urllib3/exceptions.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - ## Base Exceptions @@ -11,6 +5,11 @@ class HTTPError(Exception): "Base exception used by this module." pass +class HTTPWarning(Warning): + "Base warning used by this module." + pass + + class PoolError(HTTPError): "Base exception for errors caused within a pool." @@ -44,16 +43,20 @@ class ProxyError(HTTPError): pass -class ConnectionError(HTTPError): - "Raised when a normal connection fails." +class DecodeError(HTTPError): + "Raised when automatic decoding based on Content-Type fails." pass -class DecodeError(HTTPError): - "Raised when automatic decoding based on Content-Type fails." +class ProtocolError(HTTPError): + "Raised when something unexpected happens mid-request/response." pass +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + ## Leaf Exceptions class MaxRetryError(RequestError): @@ -64,7 +67,7 @@ class MaxRetryError(RequestError): message = "Max retries exceeded with url: %s" % url if reason: - message += " (Caused by %s: %s)" % (type(reason), reason) + message += " (Caused by %r)" % reason else: message += " (Caused by redirect)" @@ -116,7 +119,12 @@ class ClosedPoolError(PoolError): pass -class LocationParseError(ValueError, HTTPError): +class LocationValueError(ValueError, HTTPError): + "Raised when there is something wrong with a given URL input." + pass + + +class LocationParseError(LocationValueError): "Raised when get_host or similar fails to parse the URL input." def __init__(self, location): @@ -124,3 +132,8 @@ class LocationParseError(ValueError, HTTPError): HTTPError.__init__(self, message) self.location = location + + +class InsecureRequestWarning(HTTPWarning): + "Warned when making an unverified HTTPS request." + pass diff --git a/urllib3/fields.py b/urllib3/fields.py index dceafb4..c853f8d 100644 --- a/urllib3/fields.py +++ b/urllib3/fields.py @@ -1,9 +1,3 @@ -# urllib3/fields.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - import email.utils import mimetypes @@ -81,7 +75,7 @@ class RequestField(object): Supports constructing :class:`~urllib3.fields.RequestField` from parameter of key/value strings AND key/filetuple. A filetuple is a (filename, data, MIME type) tuple where the MIME type is optional. - For example: :: + For example:: 'foo': 'bar', 'fakefile': ('foofile.txt', 'contents of foofile'), diff --git a/urllib3/filepost.py b/urllib3/filepost.py index c3db30c..0fbf488 100644 --- a/urllib3/filepost.py +++ b/urllib3/filepost.py @@ -1,9 +1,3 @@ -# urllib3/filepost.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - import codecs from uuid import uuid4 diff --git a/urllib3/packages/ordered_dict.py b/urllib3/packages/ordered_dict.py index 7f8ee15..4479363 100644 --- a/urllib3/packages/ordered_dict.py +++ b/urllib3/packages/ordered_dict.py @@ -2,7 +2,6 @@ # Passes Python2.7's test suite and incorporates all the latest updates. # Copyright 2009 Raymond Hettinger, released under the MIT License. # http://code.activestate.com/recipes/576693/ - try: from thread import get_ident as _get_ident except ImportError: diff --git a/urllib3/poolmanager.py b/urllib3/poolmanager.py index 3945f5d..515dc96 100644 --- a/urllib3/poolmanager.py +++ b/urllib3/poolmanager.py @@ -1,9 +1,3 @@ -# urllib3/poolmanager.py -# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - import logging try: # Python 3 @@ -14,8 +8,10 @@ except ImportError: from ._collections import RecentlyUsedContainer from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool from .connectionpool import port_by_scheme +from .exceptions import LocationValueError from .request import RequestMethods -from .util import parse_url +from .util.url import parse_url +from .util.retry import Retry __all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] @@ -49,7 +45,7 @@ class PoolManager(RequestMethods): Additional parameters are used to create fresh :class:`urllib3.connectionpool.ConnectionPool` instances. - Example: :: + Example:: >>> manager = PoolManager(num_pools=2) >>> r = manager.request('GET', 'http://google.com/') @@ -102,10 +98,11 @@ class PoolManager(RequestMethods): ``urllib3.connectionpool.port_by_scheme``. """ - scheme = scheme or 'http' + if not host: + raise LocationValueError("No host specified.") + scheme = scheme or 'http' port = port or port_by_scheme.get(scheme, 80) - pool_key = (scheme, host, port) with self.pools.lock: @@ -118,6 +115,7 @@ class PoolManager(RequestMethods): # Make a fresh ConnectionPool of the desired type pool = self._new_pool(scheme, host, port) self.pools[pool_key] = pool + return pool def connection_from_url(self, url): @@ -165,9 +163,14 @@ class PoolManager(RequestMethods): if response.status == 303: method = 'GET' - log.info("Redirecting %s -> %s" % (url, redirect_location)) - kw['retries'] = kw.get('retries', 3) - 1 # Persist retries countdown + retries = kw.get('retries') + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + kw['retries'] = retries.increment(method, redirect_location) kw['redirect'] = redirect + + log.info("Redirecting %s -> %s" % (url, redirect_location)) return self.urlopen(method, redirect_location, **kw) @@ -208,12 +211,16 @@ class ProxyManager(PoolManager): if not proxy.port: port = port_by_scheme.get(proxy.scheme, 80) proxy = proxy._replace(port=port) + + assert proxy.scheme in ("http", "https"), \ + 'Not supported proxy scheme %s' % proxy.scheme + self.proxy = proxy self.proxy_headers = proxy_headers or {} - assert self.proxy.scheme in ("http", "https"), \ - 'Not supported proxy scheme %s' % self.proxy.scheme + connection_pool_kw['_proxy'] = self.proxy connection_pool_kw['_proxy_headers'] = self.proxy_headers + super(ProxyManager, self).__init__( num_pools, headers, **connection_pool_kw) @@ -248,10 +255,10 @@ class ProxyManager(PoolManager): # For proxied HTTPS requests, httplib sets the necessary headers # on the CONNECT to the proxy. For HTTP, we'll definitely # need to set 'Host' at the very least. - kw['headers'] = self._set_proxy_headers(url, kw.get('headers', - self.headers)) + headers = kw.get('headers', self.headers) + kw['headers'] = self._set_proxy_headers(url, headers) - return super(ProxyManager, self).urlopen(method, url, redirect, **kw) + return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw) def proxy_from_url(url, **kw): diff --git a/urllib3/request.py b/urllib3/request.py index 7a46f1b..51fe238 100644 --- a/urllib3/request.py +++ b/urllib3/request.py @@ -1,9 +1,3 @@ -# urllib3/request.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - try: from urllib.parse import urlencode except ImportError: @@ -105,7 +99,7 @@ class RequestMethods(object): Supports an optional ``fields`` parameter of key/value strings AND key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example: :: + the MIME type is optional. For example:: fields = { 'foo': 'bar', diff --git a/urllib3/response.py b/urllib3/response.py index 13ffba4..7e0d47f 100644 --- a/urllib3/response.py +++ b/urllib3/response.py @@ -1,18 +1,13 @@ -# urllib3/response.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - import zlib import io from socket import timeout as SocketTimeout from ._collections import HTTPHeaderDict -from .exceptions import DecodeError, ReadTimeoutError +from .exceptions import ProtocolError, DecodeError, ReadTimeoutError from .packages.six import string_types as basestring, binary_type -from .util import is_fp_closed +from .connection import HTTPException, BaseSSLError +from .util.response import is_fp_closed + class DeflateDecoder(object): @@ -88,11 +83,14 @@ class HTTPResponse(io.IOBase): self.decode_content = decode_content self._decoder = None - self._body = body if body and isinstance(body, basestring) else None + self._body = None self._fp = None self._original_response = original_response self._fp_bytes_read = 0 + if body and isinstance(body, (basestring, binary_type)): + self._body = body + self._pool = pool self._connection = connection @@ -199,6 +197,19 @@ class HTTPResponse(io.IOBase): # there is yet no clean way to get at it from this context. raise ReadTimeoutError(self._pool, None, 'Read timed out.') + except BaseSSLError as e: + # FIXME: Is there a better way to differentiate between SSLErrors? + if not 'read operation timed out' in str(e): # Defensive: + # This shouldn't happen but just in case we're missing an edge + # case, let's avoid swallowing SSL errors. + raise + + raise ReadTimeoutError(self._pool, None, 'Read timed out.') + + except HTTPException as e: + # This includes IncompleteRead. + raise ProtocolError('Connection broken: %r' % e, e) + self._fp_bytes_read += len(data) try: diff --git a/urllib3/util/__init__.py b/urllib3/util/__init__.py index a40185e..8becc81 100644 --- a/urllib3/util/__init__.py +++ b/urllib3/util/__init__.py @@ -1,9 +1,4 @@ -# urllib3/util/__init__.py -# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - +# For backwards compatibility, provide imports that used to be here. from .connection import is_connection_dropped from .request import make_headers from .response import is_fp_closed @@ -19,6 +14,8 @@ from .timeout import ( current_time, Timeout, ) + +from .retry import Retry from .url import ( get_host, parse_url, diff --git a/urllib3/util/connection.py b/urllib3/util/connection.py index c67ef04..062ee9d 100644 --- a/urllib3/util/connection.py +++ b/urllib3/util/connection.py @@ -1,4 +1,4 @@ -from socket import error as SocketError +import socket try: from select import poll, POLLIN except ImportError: # `poll` doesn't exist on OSX and other platforms @@ -31,7 +31,7 @@ def is_connection_dropped(conn): # Platform-specific try: return select([sock], [], [], 0.0)[0] - except SocketError: + except socket.error: return True # This version is better on platforms that support it. @@ -41,3 +41,55 @@ def is_connection_dropped(conn): # Platform-specific if fno == sock.fileno(): # Either data is buffered (bad), or the connection is dropped. return True + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, socket_options=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + err = None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + # If provided, set socket level options before connecting. + # This is the only addition urllib3 makes to this function. + _set_socket_options(sock, socket_options) + sock.connect(sa) + return sock + + except socket.error as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise socket.error("getaddrinfo returns an empty list") + + +def _set_socket_options(sock, options): + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) diff --git a/urllib3/util/request.py b/urllib3/util/request.py index bfd7a98..bc64f6b 100644 --- a/urllib3/util/request.py +++ b/urllib3/util/request.py @@ -1,7 +1,6 @@ from base64 import b64encode -from ..packages import six - +from ..packages.six import b ACCEPT_ENCODING = 'gzip,deflate' @@ -29,13 +28,13 @@ def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, auth header. :param proxy_basic_auth: - Colon-separated username:password string for - 'proxy-authorization: basic ...' auth header. + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. :param disable_cache: If ``True``, adds 'cache-control: no-cache' header. - Example: :: + Example:: >>> make_headers(keep_alive=True, user_agent="Batman/1.0") {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} @@ -60,11 +59,11 @@ def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, if basic_auth: headers['authorization'] = 'Basic ' + \ - b64encode(six.b(basic_auth)).decode('utf-8') + b64encode(b(basic_auth)).decode('utf-8') if proxy_basic_auth: headers['proxy-authorization'] = 'Basic ' + \ - b64encode(six.b(proxy_basic_auth)).decode('utf-8') + b64encode(b(proxy_basic_auth)).decode('utf-8') if disable_cache: headers['cache-control'] = 'no-cache' diff --git a/urllib3/util/retry.py b/urllib3/util/retry.py new file mode 100644 index 0000000..9013197 --- /dev/null +++ b/urllib3/util/retry.py @@ -0,0 +1,279 @@ +import time +import logging + +from ..exceptions import ( + ProtocolError, + ConnectTimeoutError, + ReadTimeoutError, + MaxRetryError, +) +from ..packages import six + + +log = logging.getLogger(__name__) + + +class Retry(object): + """ Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool:: + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', retries=Retry(10)) + + Retries can be disabled by passing ``False``:: + + response = http.request('GET', 'http://example.com/', retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. It's a good idea to set this to some sensibly-high value to + account for unexpected edge cases and avoid infinite retry loops. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param iterable method_whitelist: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + indempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`. + + :param iterable status_forcelist: + A set of HTTP status codes that we should force a retry on. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts. urllib3 will sleep for:: + + {backoff factor} * (2 ^ ({number of total retries} - 1)) + + seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep + for [0.1s, 0.2s, 0.4s, ...] between retries. It will never be longer + than :attr:`Retry.MAX_BACKOFF`. + + By default, backoff is disabled (set to 0). + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + """ + + DEFAULT_METHOD_WHITELIST = frozenset([ + 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) + + #: Maximum backoff time. + BACKOFF_MAX = 120 + + def __init__(self, total=10, connect=None, read=None, redirect=None, + method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, + backoff_factor=0, raise_on_redirect=True, _observed_errors=0): + + self.total = total + self.connect = connect + self.read = read + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.method_whitelist = method_whitelist + self.backoff_factor = backoff_factor + self.raise_on_redirect = raise_on_redirect + self._observed_errors = _observed_errors # TODO: use .history instead? + + def new(self, **kw): + params = dict( + total=self.total, + connect=self.connect, read=self.read, redirect=self.redirect, + method_whitelist=self.method_whitelist, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + raise_on_redirect=self.raise_on_redirect, + _observed_errors=self._observed_errors, + ) + params.update(kw) + return type(self)(**params) + + @classmethod + def from_int(cls, retries, redirect=True, default=None): + """ Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r" % (retries, new_retries)) + return new_retries + + def get_backoff_time(self): + """ Formula for computing the current backoff + + :rtype: float + """ + if self._observed_errors <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (self._observed_errors - 1)) + return min(self.BACKOFF_MAX, backoff_value) + + def sleep(self): + """ Sleep between retry attempts using an exponential backoff. + + By default, the backoff factor is 0 and this method will return + immediately. + """ + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def _is_connection_error(self, err): + """ Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err): + """ Errors that occur after the request has been started, so we can't + assume that the server did not process any of it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def is_forced_retry(self, method, status_code): + """ Is this method/response retryable? (Based on method/codes whitelists) + """ + if self.method_whitelist and method.upper() not in self.method_whitelist: + return False + + return self.status_forcelist and status_code in self.status_forcelist + + def is_exhausted(self): + """ Are we out of retries? + """ + retry_counts = (self.total, self.connect, self.read, self.redirect) + retry_counts = list(filter(None, retry_counts)) + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment(self, method=None, url=None, response=None, error=None, _pool=None, _stacktrace=None): + """ Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.HTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise six.reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + _observed_errors = self._observed_errors + connect = self.connect + read = self.read + redirect = self.redirect + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise six.reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + _observed_errors += 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False: + raise six.reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + _observed_errors += 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + + else: + # FIXME: Nothing changed, scenario doesn't make sense. + _observed_errors += 1 + + new_retry = self.new( + total=total, + connect=connect, read=read, redirect=redirect, + _observed_errors=_observed_errors) + + if new_retry.is_exhausted(): + raise MaxRetryError(_pool, url, error) + + log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry)) + + return new_retry + + + def __repr__(self): + return ('{cls.__name__}(total={self.total}, connect={self.connect}, ' + 'read={self.read}, redirect={self.redirect})').format( + cls=type(self), self=self) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/urllib3/util/ssl_.py b/urllib3/util/ssl_.py index dee4b87..9cfe2d2 100644 --- a/urllib3/util/ssl_.py +++ b/urllib3/util/ssl_.py @@ -34,10 +34,9 @@ def assert_fingerprint(cert, fingerprint): } fingerprint = fingerprint.replace(':', '').lower() + digest_length, odd = divmod(len(fingerprint), 2) - digest_length, rest = divmod(len(fingerprint), 2) - - if rest or digest_length not in hashfunc_map: + if odd or digest_length not in hashfunc_map: raise SSLError('Fingerprint is of invalid length.') # We need encode() here for py32; works on py2 and p33. diff --git a/urllib3/util/timeout.py b/urllib3/util/timeout.py index aaadc12..ea7027f 100644 --- a/urllib3/util/timeout.py +++ b/urllib3/util/timeout.py @@ -1,32 +1,49 @@ +# The default socket timeout, used by httplib to indicate that no timeout was +# specified by the user from socket import _GLOBAL_DEFAULT_TIMEOUT import time from ..exceptions import TimeoutStateError +# A sentinel value to indicate that no timeout was specified by the user in +# urllib3 +_Default = object() def current_time(): """ - Retrieve the current time, this function is mocked out in unit testing. + Retrieve the current time. This function is mocked out in unit testing. """ return time.time() -_Default = object() -# The default timeout to use for socket connections. This is the attribute used -# by httplib to define the default timeout +class Timeout(object): + """ Timeout configuration. + Timeouts can be defined as a default for a pool:: -class Timeout(object): - """ - Utility object for storing timeout values. + timeout = Timeout(connect=2.0, read=7.0) + http = PoolManager(timeout=timeout) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) - Example usage: + Timeouts can be disabled by setting all the parameters to ``None``:: + + no_timeout = Timeout(connect=None, read=None) + response = http.request('GET', 'http://example.com/, timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. - .. code-block:: python + Defaults to None. - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout) - pool.request(...) # Etc, etc + :type total: integer, float, or None :param connect: The maximum amount of time to wait for a connection attempt to a server @@ -47,25 +64,15 @@ class Timeout(object): :type read: integer, float, or None - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: integer, float, or None - .. note:: Many factors can affect the total amount of time for urllib3 to return - an HTTP response. Specifically, Python's DNS resolver does not obey the - timeout specified on the socket. Other factors that can affect total - request time include high CPU load, high swap, the program running at a - low priority level, or other behaviors. The observed running time for - urllib3 to return a response may be greater than the value passed to - `total`. + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. In addition, the read and total timeouts only measure the time between read operations on the socket connecting the client and the server, @@ -73,8 +80,8 @@ class Timeout(object): response. For most requests, the timeout is raised because the server has not sent the first byte in the specified time. This is not always the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not ever trigger, even though the request will - take several minutes to complete. + of 20 seconds will not trigger, even though the request will take + several minutes to complete. If your goal is to cut off any request after a set amount of wall clock time, consider having a second "watcher" thread to cut off a slow diff --git a/urllib3/util/url.py b/urllib3/util/url.py index 122108b..487d456 100644 --- a/urllib3/util/url.py +++ b/urllib3/util/url.py @@ -2,6 +2,7 @@ from collections import namedtuple from ..exceptions import LocationParseError + url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] @@ -47,7 +48,7 @@ def split_first(s, delims): If not found, then the first part is the full input string. - Example: :: + Example:: >>> split_first('foo/bar?baz', '?/=') ('foo', 'bar?baz', '/') @@ -80,7 +81,7 @@ def parse_url(url): Partly backwards-compatible with :mod:`urlparse`. - Example: :: + Example:: >>> parse_url('http://google.com/mail/') Url(scheme='http', host='google.com', port=None, path='/', ...) @@ -95,6 +96,10 @@ def parse_url(url): # Additionally, this implementations does silly things to be optimal # on CPython. + if not url: + # Empty + return Url() + scheme = None auth = None host = None -- cgit v1.2.3