diff --git a/README.rst b/README.rst index c535d64..3cea98e 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,8 @@ threads to test your Web app. Supported Libaries ================== -``wsgi_intercept`` works with a variety of HTTP clients in Python 2.7, -3.5 and beyond, and in pypy. +``wsgi_intercept`` works with a variety of HTTP clients in Python 3.7 +and beyond, and in pypy. * urllib2 * urllib.request @@ -22,7 +22,7 @@ Supported Libaries * http.client * httplib2 * requests -* urllib3 (<2.0.0, urllib3 2 support is in progress) +* urllib3 How Does It Work? ================= @@ -88,14 +88,9 @@ Packages Intercepted Unfortunately each of the HTTP client libraries use their own specific mechanism for making HTTP call-outs, so individual implementations are needed. At this time there are implementations for ``httplib2``, -``urllib3`` (<2.0.0) and ``requests`` in both Python 2 and 3, ``urllib2`` and -``httplib`` in Python 2 and ``urllib.request`` and ``http.client`` +``urllib3``, ``requests``, ``urllib.request`` and ``http.client`` in Python 3. -If you are using Python 2 and need support for a different HTTP -client, require a version of ``wsgi_intercept<0.6``. Earlier versions -include support for ``webtest``, ``webunit`` and ``zope.testbrowser``. - The best way to figure out how to use interception is to inspect `the tests`_. More comprehensive documentation available upon request. @@ -115,9 +110,9 @@ it into all of the *other* Python Web testing frameworks. The Python 2 version of wsgi-intercept was the result. Kumar McMillan later took over maintenance. -The current version is tested with Python 2.7, 3.5-3.11, and pypy and pypy3. -It was assembled by `Chris Dent`_. Testing and documentation improvements -from `Sasha Hart`_. +The current version is tested with Python 3.7-3.12, and pypy3. It was +assembled by `Chris Dent`_. Testing and documentation improvements from +`Sasha Hart`_. .. _"best Web testing framework": http://blog.ianbicking.org/best-of-the-web-app-test-frameworks.html diff --git a/setup.py b/setup.py index 6a1c3b4..a185927 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,6 @@ Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent -Programming Language :: Python :: 2 -Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -36,15 +34,13 @@ 'license': 'MIT License', 'classifiers': CLASSIFIERS, 'packages': find_packages(), - 'install_requires': [ - 'six', - ], + 'python_requires': '>=3', 'extras_require': { 'testing': [ 'pytest>=2.4', 'httplib2', 'requests>=2.0.1', - 'urllib3>=1.11.0,<2.0.0', + 'urllib3>=2.0.0', ], 'docs': [ 'sphinx', diff --git a/tox.ini b/tox.ini index a7e7143..75ae60a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] minversion = 1.6 skipsdist = True -envlist = py27,py35,py36,py37,py38,py39,py310,py311,py312,pypy,pep8,docs,readme +envlist = py37,py38,py39,py310,py311,py312,pypy,pep8,docs,readme [testenv] deps = .[testing] -commands = py.test --tb=short wsgi_intercept/tests +commands = py.test --tb=short wsgi_intercept/tests {posargs} passenv = WSGI_INTERCEPT_* [testenv:pep8] diff --git a/wsgi_intercept/__init__.py b/wsgi_intercept/__init__.py index ec9397f..c32ce1e 100644 --- a/wsgi_intercept/__init__.py +++ b/wsgi_intercept/__init__.py @@ -13,8 +13,8 @@ Supported Libaries ================== -``wsgi_intercept`` works with a variety of HTTP clients in Python 2.7, -3.7 and beyond, and in pypy. +``wsgi_intercept`` works with a variety of HTTP clients in Python 3.7 +and beyond, and in pypy. * urllib2 * urllib.request @@ -22,7 +22,7 @@ * http.client * httplib2 * requests -* urllib3 (<2.0.0, urllib3 2 support is in progress) +* urllib3 How Does It Work? ================= @@ -88,14 +88,9 @@ def load_app(): Unfortunately each of the HTTP client libraries use their own specific mechanism for making HTTP call-outs, so individual implementations are needed. At this time there are implementations for ``httplib2``, -``urllib3`` (<2.0.0) and ``requests`` in both Python 2 and 3, ``urllib2`` and -``httplib`` in Python 2 and ``urllib.request`` and ``http.client`` +``urllib3``, ``requests``, ``urllib.request`` and ``http.client`` in Python 3. -If you are using Python 2 and need support for a different HTTP -client, require a version of ``wsgi_intercept<0.6``. Earlier versions -include support for ``webtest``, ``webunit`` and ``zope.testbrowser``. - The best way to figure out how to use interception is to inspect `the tests`_. More comprehensive documentation available upon request. @@ -115,9 +110,9 @@ def load_app(): The Python 2 version of wsgi-intercept was the result. Kumar McMillan later took over maintenance. -The current version is tested with Python 2.7, 3.5-3.11, and pypy and pypy3. -It was assembled by `Chris Dent`_. Testing and documentation improvements -from `Sasha Hart`_. +The current version is tested with Python 3.7-3.12, and pypy3. It was +assembled by `Chris Dent`_. Testing and documentation improvements from +`Sasha Hart`_. .. _"best Web testing framework": http://blog.ianbicking.org/best-of-the-web-app-test-frameworks.html @@ -144,15 +139,9 @@ def load_app(): import traceback from io import BytesIO -# Don't use six here because it is unquote_to_bytes that we want in -# Python 3. -try: - from urllib.parse import unquote_to_bytes as url_unquote -except ImportError: - from urllib import unquote as url_unquote +from urllib.parse import unquote_to_bytes as url_unquote -import six -from six.moves.http_client import HTTPConnection, HTTPSConnection +from http.client import HTTPConnection, HTTPSConnection # Set this to True to cause response headers from the intercepted @@ -227,8 +216,7 @@ def make_environ(inp, host, port, script_name): environ = {} method_line = inp.readline() - if six.PY3: - method_line = method_line.decode('ISO-8859-1') + method_line = method_line.decode('ISO-8859-1') content_type = None content_length = None @@ -302,13 +290,11 @@ def make_environ(inp, host, port, script_name): # fill out our dictionary. # - # In Python3 turn the bytes of the path info into a string of - # latin-1 code points, because that's what the spec says we must - # do to be like a server. Later various libraries will be forced - # to decode and then reencode to get the UTF-8 that everyone - # wants. - if six.PY3: - path_info = path_info.decode('latin-1') + # Turn the bytes of the path info into a string of latin-1 code points, + # because that's what the spec says we must do to be like a server. Later + # various libraries will be forced to decode and then reencode to get the + # UTF-8 that everyone wants. + path_info = path_info.decode('latin-1') environ.update({ "wsgi.version": (1, 0), @@ -535,6 +521,26 @@ class WSGI_HTTPConnection(HTTPConnection): Intercept all traffic to certain hosts & redirect into a WSGI application object. """ + + def __init__(self, *args, **kwargs): + """ + Do a complex dance to deal with urllib3's method signature + constraints. + """ + # TODO: This seems really really fragile but is passing + # tests. + if 'host' in kwargs: + host = kwargs.pop('host') + if 'port' in kwargs: + port = kwargs.pop('port') + else: + port = None + super().__init__(host, port, *args, **kwargs) + else: + if len(args) > 2: + args = args[0:2] + super().__init__(*args, **kwargs) + def get_app(self, host, port): """ Return the app object for the given (host, port). @@ -587,6 +593,7 @@ class WSGI_HTTPSConnection(HTTPSConnection, WSGI_HTTPConnection): Intercept all traffic to certain hosts & redirect into a WSGI application object. """ + def get_app(self, host, port): """ Return the app object for the given (host, port). @@ -624,22 +631,31 @@ def connect(self): try: import ssl if hasattr(self, '_context'): + # Extract cert_reqs from requests + urllib3. + # They do some of their own SSL context management + # that wsgi intercept routes around, so we need to + # be careful. + if hasattr(self, '_intercept_cert_reqs'): + cert_reqs = self._intercept_cert_reqs + else: + cert_reqs = self.cert_reqs + self._context.check_hostname = self.assert_hostname self._check_hostname = self.assert_hostname # Py3.6 if hasattr(ssl, 'VerifyMode'): # Support for Python3.6 and higher - if isinstance(self.cert_reqs, ssl.VerifyMode): - self._context.verify_mode = self.cert_reqs + if isinstance(cert_reqs, ssl.VerifyMode): + self._context.verify_mode = cert_reqs else: self._context.verify_mode = ssl.VerifyMode[ - self.cert_reqs] - elif isinstance(self.cert_reqs, six.string_types): + cert_reqs] + elif isinstance(cert_reqs, str): # Support for Python3.5 and below self._context.verify_mode = getattr(ssl, - self.cert_reqs, + cert_reqs, self._context.verify_mode) else: - self._context.verify_mode = self.cert_reqs + self._context.verify_mode = cert_reqs if not hasattr(self, 'key_file'): self.key_file = None @@ -658,7 +674,6 @@ def connect(self): else: self._check_hostname = self.check_hostname except (ImportError, AttributeError): - import traceback traceback.print_exc() HTTPSConnection.connect(self) diff --git a/wsgi_intercept/_urllib3.py b/wsgi_intercept/_urllib3.py index 969ddac..f757541 100644 --- a/wsgi_intercept/_urllib3.py +++ b/wsgi_intercept/_urllib3.py @@ -8,16 +8,33 @@ wsgi_fake_socket.settimeout = lambda self, timeout: None +HTTP_KEYWORD_POPS = [ + 'strict', + 'socket_options', + 'server_hostname', +] + +HTTPS_KEYWORD_POPS = HTTP_KEYWORD_POPS + [ + 'key_password', + 'server_hostname', + 'cert_reqs', + 'ca_certs', + 'ca_cert_dir', + 'assert_hostname', + 'assert_fingerprint', + 'ssl_version', + 'ssl_minimum_version', + 'ssl_maximum_version', +] + def make_urllib3_override(HTTPConnectionPool, HTTPSConnectionPool, HTTPConnection, HTTPSConnection): class HTTP_WSGIInterceptor(WSGI_HTTPConnection, HTTPConnection): def __init__(self, *args, **kwargs): - if 'strict' in kwargs and sys.version_info > (3, 0): - kwargs.pop('strict') - kwargs.pop('socket_options', None) - kwargs.pop('server_hostname', None) + for kw in HTTP_KEYWORD_POPS: + kwargs.pop(kw, None) WSGI_HTTPConnection.__init__(self, *args, **kwargs) HTTPConnection.__init__(self, *args, **kwargs) @@ -25,11 +42,10 @@ class HTTPS_WSGIInterceptor(WSGI_HTTPSConnection, HTTPSConnection): is_verified = True def __init__(self, *args, **kwargs): - if 'strict' in kwargs and sys.version_info > (3, 0): - kwargs.pop('strict') - kwargs.pop('socket_options', None) - kwargs.pop('key_password', None) - kwargs.pop('server_hostname', None) + if 'cert_reqs' in kwargs and kwargs['cert_reqs'] is not None: + self._intercept_cert_reqs = kwargs.pop("cert_reqs") + for kw in HTTPS_KEYWORD_POPS: + kwargs.pop(kw, None) if sys.version_info > (3, 12): kwargs.pop('key_file', None) kwargs.pop('cert_file', None) diff --git a/wsgi_intercept/http_client_intercept.py b/wsgi_intercept/http_client_intercept.py index 56d930c..7f86b0b 100644 --- a/wsgi_intercept/http_client_intercept.py +++ b/wsgi_intercept/http_client_intercept.py @@ -1,23 +1,14 @@ """Intercept HTTP connections that use httplib (Py2) or http.client (Py3). """ -try: - import http.client as http_lib -except ImportError: - import httplib as http_lib +import http.client as http_lib from . import WSGI_HTTPConnection, WSGI_HTTPSConnection -try: - from http.client import ( - HTTPConnection as OriginalHTTPConnection, - HTTPSConnection as OriginalHTTPSConnection - ) -except ImportError: - from httplib import ( - HTTPConnection as OriginalHTTPConnection, - HTTPSConnection as OriginalHTTPSConnection - ) +from http.client import ( + HTTPConnection as OriginalHTTPConnection, + HTTPSConnection as OriginalHTTPSConnection +) class HTTP_WSGIInterceptor(WSGI_HTTPConnection, http_lib.HTTPConnection): diff --git a/wsgi_intercept/interceptor.py b/wsgi_intercept/interceptor.py index 2d2d581..a6f5baa 100644 --- a/wsgi_intercept/interceptor.py +++ b/wsgi_intercept/interceptor.py @@ -4,7 +4,7 @@ from importlib import import_module from uuid import uuid4 -from six.moves.urllib import parse as urlparse +from urllib import parse as urlparse import wsgi_intercept diff --git a/wsgi_intercept/tests/test_http_client.py b/wsgi_intercept/tests/test_http_client.py index f33f4c7..d2f9a96 100644 --- a/wsgi_intercept/tests/test_http_client.py +++ b/wsgi_intercept/tests/test_http_client.py @@ -2,10 +2,7 @@ from wsgi_intercept import http_client_intercept, WSGIAppError from . import wsgi_app from .install import installer_class, skipnetwork -try: - import http.client as http_lib -except ImportError: - import httplib as http_lib +import http.client as http_lib HOST = 'some_hopefully_nonexistant_domain' diff --git a/wsgi_intercept/tests/test_interceptor.py b/wsgi_intercept/tests/test_interceptor.py index f3dd2f0..28eab42 100644 --- a/wsgi_intercept/tests/test_interceptor.py +++ b/wsgi_intercept/tests/test_interceptor.py @@ -11,13 +11,9 @@ import requests import urllib3 from httplib2 import Http, ServerNotFoundError -# don't use six as the monkey patching gets confused -try: - import http.client as http_client -except ImportError: - import httplib as http_client -from six.moves.urllib.request import urlopen -from six.moves.urllib.error import URLError +import http.client as http_client +from urllib.request import urlopen +from urllib.error import URLError from wsgi_intercept.interceptor import ( Interceptor, HttpClientInterceptor, Httplib2Interceptor, diff --git a/wsgi_intercept/tests/test_response_headers.py b/wsgi_intercept/tests/test_response_headers.py index 8970555..19685f9 100644 --- a/wsgi_intercept/tests/test_response_headers.py +++ b/wsgi_intercept/tests/test_response_headers.py @@ -10,7 +10,6 @@ import pytest import requests -import six import wsgi_intercept from wsgi_intercept.interceptor import RequestsInterceptor @@ -56,10 +55,7 @@ def header_app(): def test_encoding_violation(): """If the header is unicode we expect boom.""" header_key = 'request-id' - if six.PY2: - header_value = u'alpha' - else: - header_value = b'alpha' + header_value = b'alpha' # we expect our http library to give us a str returned_header = 'alpha' diff --git a/wsgi_intercept/tests/test_urllib.py b/wsgi_intercept/tests/test_urllib.py index 9b1df03..ba9a486 100644 --- a/wsgi_intercept/tests/test_urllib.py +++ b/wsgi_intercept/tests/test_urllib.py @@ -3,10 +3,7 @@ from wsgi_intercept import urllib_intercept, WSGIAppError from . import wsgi_app from .install import installer_class, skipnetwork -try: - import urllib.request as url_lib -except ImportError: - import urllib2 as url_lib +import urllib.request as url_lib HOST = 'some_hopefully_nonexistant_domain' diff --git a/wsgi_intercept/tests/test_wsgi_compliance.py b/wsgi_intercept/tests/test_wsgi_compliance.py index 39ee12b..89f73d2 100644 --- a/wsgi_intercept/tests/test_wsgi_compliance.py +++ b/wsgi_intercept/tests/test_wsgi_compliance.py @@ -1,9 +1,6 @@ import sys import pytest -try: - from urllib.parse import unquote -except ImportError: - from urllib import unquote +from urllib.parse import unquote from wsgi_intercept import httplib2_intercept from . import wsgi_app from .install import installer_class diff --git a/wsgi_intercept/tests/wsgi_app.py b/wsgi_intercept/tests/wsgi_app.py index 9761bbb..0c573dc 100644 --- a/wsgi_intercept/tests/wsgi_app.py +++ b/wsgi_intercept/tests/wsgi_app.py @@ -4,11 +4,6 @@ from pprint import pformat -try: - bytes -except ImportError: - bytes = str - def simple_app(environ, start_response): """Simplest possible application object""" diff --git a/wsgi_intercept/urllib3_intercept.py b/wsgi_intercept/urllib3_intercept.py index d4e82e3..eff3997 100644 --- a/wsgi_intercept/urllib3_intercept.py +++ b/wsgi_intercept/urllib3_intercept.py @@ -1,8 +1,5 @@ """Intercept HTTP connections that use `urllib3 `_. - -Note that currently only urllib3 <2.0.0 is supported. 2.0.0 support -is in progress. """ from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool diff --git a/wsgi_intercept/urllib_intercept.py b/wsgi_intercept/urllib_intercept.py index 2454d5c..fa0697c 100644 --- a/wsgi_intercept/urllib_intercept.py +++ b/wsgi_intercept/urllib_intercept.py @@ -4,10 +4,7 @@ import os -try: - import urllib.request as url_lib -except ImportError: - import urllib2 as url_lib +import urllib.request as url_lib from . import WSGI_HTTPConnection, WSGI_HTTPSConnection