From a3bd3d8dff63e8b4b197042152b2831b4007d5a0 Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Mon, 30 Apr 2018 23:25:38 +0800 Subject: [PATCH 01/10] add explicit load_backend function --- test/test_backends.py | 31 +++++++++++++ urllib3/_async/connection.py | 5 +-- urllib3/_backends/__init__.py | 9 ---- urllib3/_backends/_loader.py | 67 ++++++++++++++++++++++++++++ urllib3/_backends/twisted_backend.py | 6 +-- urllib3/backends.py | 9 ++++ 6 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 test/test_backends.py create mode 100644 urllib3/_backends/_loader.py create mode 100644 urllib3/backends.py diff --git a/test/test_backends.py b/test/test_backends.py new file mode 100644 index 00000000..e8e10a74 --- /dev/null +++ b/test/test_backends.py @@ -0,0 +1,31 @@ +import sys + +import pytest + +from urllib3.backends import Backend as UserSpecifiedBackend +from urllib3._backends._loader import load_backend + + +class TestLoadBackend(object): + """ + We assert that we are able to import compatible backends, + and that we fail correctly if we attempt to use an unavailable or unknown backend. + """ + def test_dummy(self): + with pytest.raises(ImportError): + load_backend("dummy") + + def test_sync(self): + load_backend("sync") + load_backend(UserSpecifiedBackend("sync")) + + @pytest.mark.skipif( + sys.version_info < (3, 5), + reason="async backends require Python 3.5 or greater", + ) + def test_async(self): + load_backend("trio") + load_backend("twisted") + + from twisted.internet import reactor + load_backend(UserSpecifiedBackend("twisted", reactor=reactor)) diff --git a/urllib3/_async/connection.py b/urllib3/_async/connection.py index cae10637..60ba5cd5 100644 --- a/urllib3/_async/connection.py +++ b/urllib3/_async/connection.py @@ -30,8 +30,8 @@ ) from urllib3.packages import six from ..util import ssl_ as ssl_util -from .._backends import SyncBackend from .._backends._common import LoopAbort +from .._backends._loader import load_backend try: import ssl @@ -311,8 +311,7 @@ def __init__(self, host, port, backend=None, source_address=None, tunnel_host=None, tunnel_port=None, tunnel_headers=None): self.is_verified = False - - self._backend = backend or SyncBackend() + self._backend = backend or load_backend("sync") self._host = host self._port = port self._socket_options = ( diff --git a/urllib3/_backends/__init__.py b/urllib3/_backends/__init__.py index d3753330..e69de29b 100644 --- a/urllib3/_backends/__init__.py +++ b/urllib3/_backends/__init__.py @@ -1,9 +0,0 @@ -from urllib3.packages import six -from .sync_backend import SyncBackend - -__all__ = ['SyncBackend'] - -if six.PY3: - from .trio_backend import TrioBackend - from .twisted_backend import TwistedBackend - __all__ += ['TrioBackend', 'TwistedBackend'] diff --git a/urllib3/_backends/_loader.py b/urllib3/_backends/_loader.py new file mode 100644 index 00000000..dff12b25 --- /dev/null +++ b/urllib3/_backends/_loader.py @@ -0,0 +1,67 @@ +import sys + +from ..backends import Backend as UserSpecifiedBackend + + +def check_for_python_3_5(): + if sys.version_info < (3, 5): + raise ValueError("This backend requires Python 3.5 or greater.") + + +def load_dummy_backend(kwargs): + """ + This function is called by unit tests. + It asserts that urllib3 can be used without having a specific backend installed. + The .dummy_backend module should not exist. + """ + from .dummy_backend import DummyBackend + return DummyBackend(**kwargs) + + +def load_sync_backend(kwargs): + from .sync_backend import SyncBackend + return SyncBackend(**kwargs) + + +def load_trio_backend(kwargs): + check_for_python_3_5() + from .trio_backend import TrioBackend + return TrioBackend(**kwargs) + + +def load_twisted_backend(kwargs): + check_for_python_3_5() + from .twisted_backend import TwistedBackend + return TwistedBackend(**kwargs) + + +def backend_directory(): + """ + We defer any heavy duty imports until the last minute. + """ + return { + "dummy": load_dummy_backend, + "sync": load_sync_backend, + "trio": load_trio_backend, + "twisted": load_twisted_backend, + } + + +def user_specified_unknown_backend(backend_name): + known_backend_names = sorted(backend_directory().keys()) + return "Unknown backend specifier {backend_name}. Choose one of: {known_backend_names}".format( + backend_name=backend_name, + known_backend_names=", ".join(known_backend_names) + ) + + +def load_backend(user_specified_backend): + if not isinstance(user_specified_backend, UserSpecifiedBackend): + user_specified_backend = UserSpecifiedBackend(name=user_specified_backend) + + available_loaders = backend_directory() + if user_specified_backend.name not in available_loaders: + raise ValueError(user_specified_unknown_backend(user_specified_backend.name)) + + loader = available_loaders[user_specified_backend.name] + return loader(user_specified_backend.kwargs) diff --git a/urllib3/_backends/twisted_backend.py b/urllib3/_backends/twisted_backend.py index 0d336971..4f484c1c 100644 --- a/urllib3/_backends/twisted_backend.py +++ b/urllib3/_backends/twisted_backend.py @@ -1,6 +1,6 @@ import socket import OpenSSL.crypto -from twisted.internet import protocol, ssl +from twisted.internet import protocol, reactor as default_reactor, ssl from twisted.internet.interfaces import IHandshakeListener from twisted.internet.endpoints import HostnameEndpoint, connectProtocol from twisted.internet.defer import ( @@ -15,8 +15,8 @@ class TwistedBackend: - def __init__(self, reactor): - self._reactor = reactor + def __init__(self, reactor=None): + self._reactor = reactor or default_reactor async def connect(self, host, port, connect_timeout, source_address=None, socket_options=None): diff --git a/urllib3/backends.py b/urllib3/backends.py new file mode 100644 index 00000000..dad81110 --- /dev/null +++ b/urllib3/backends.py @@ -0,0 +1,9 @@ +class Backend: + """ + Specifies the desired backend and any argumnets passed to it's constructor. + + Projects that use urllib3 can subclass this interface to expose it to users. + """ + def __init__(self, name, **kwargs): + self.name = name + self.kwargs = kwargs From 21dfc1c81457b13b903699eaeeb391b57cad5a79 Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Tue, 1 May 2018 11:31:50 +0800 Subject: [PATCH 02/10] remove xfail from TestImportWithoutSSL --- test/test_no_ssl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_no_ssl.py b/test/test_no_ssl.py index e142c525..cbea0482 100644 --- a/test/test_no_ssl.py +++ b/test/test_no_ssl.py @@ -90,6 +90,5 @@ def import_ssl(): self.assertRaises(ImportError, import_ssl) - @pytest.mark.xfail def test_import_urllib3(self): import urllib3 # noqa: F401 From 680943f9bef51c1ee0dc6887e252873885a5c27c Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Thu, 3 May 2018 20:56:26 +0800 Subject: [PATCH 03/10] skip TestHTTPWithoutSSL --- test/with_dummyserver/test_no_ssl.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/with_dummyserver/test_no_ssl.py b/test/with_dummyserver/test_no_ssl.py index e37794d5..a3f837a9 100644 --- a/test/with_dummyserver/test_no_ssl.py +++ b/test/with_dummyserver/test_no_ssl.py @@ -8,10 +8,17 @@ from dummyserver.testcase import ( HTTPDummyServerTestCase, HTTPSDummyServerTestCase) +import pytest import urllib3 class TestHTTPWithoutSSL(HTTPDummyServerTestCase, TestWithoutSSL): + + @pytest.mark.skip(reason=( + "TestWithoutSSL mutates sys.modules." + "This breaks the backend loading code which imports modules at runtime." + "See discussion at https://github.com/python-trio/urllib3/pull/42" + )) def test_simple(self): pool = urllib3.HTTPConnectionPool(self.host, self.port) self.addCleanup(pool.close) From 7d61a2fc3bf52feaeac9e1e38950264d5c2f3dec Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Sun, 6 May 2018 00:07:56 +0800 Subject: [PATCH 04/10] fix spelling --- urllib3/backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/urllib3/backends.py b/urllib3/backends.py index dad81110..31129682 100644 --- a/urllib3/backends.py +++ b/urllib3/backends.py @@ -1,6 +1,6 @@ class Backend: """ - Specifies the desired backend and any argumnets passed to it's constructor. + Specifies the desired backend and any arguments passed to its constructor. Projects that use urllib3 can subclass this interface to expose it to users. """ From e3d6c2b0ad7ccc50f06549e178e4581d511128f5 Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Sun, 6 May 2018 00:08:28 +0800 Subject: [PATCH 05/10] add __eq__ to backend objects for testing --- urllib3/backends.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/urllib3/backends.py b/urllib3/backends.py index 31129682..28685f32 100644 --- a/urllib3/backends.py +++ b/urllib3/backends.py @@ -7,3 +7,6 @@ class Backend: def __init__(self, name, **kwargs): self.name = name self.kwargs = kwargs + + def __eq__(self, other): + return self.name == other.name and self.kwargs == other.kwargs From d491f1d65665c15821a02c2519c6a6cdfa4dea8f Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Sun, 6 May 2018 00:09:25 +0800 Subject: [PATCH 06/10] add normalize_backend function, decouple loader and backend objects --- urllib3/_backends/_loader.py | 106 ++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 28 deletions(-) diff --git a/urllib3/_backends/_loader.py b/urllib3/_backends/_loader.py index dff12b25..f92e8b33 100644 --- a/urllib3/_backends/_loader.py +++ b/urllib3/_backends/_loader.py @@ -1,21 +1,17 @@ import sys -from ..backends import Backend as UserSpecifiedBackend +from ..backends import Backend -def check_for_python_3_5(): - if sys.version_info < (3, 5): - raise ValueError("This backend requires Python 3.5 or greater.") +class Loader: + def __init__(self, name, loader, is_async): + self.name = name + self.loader = loader + self.is_async = is_async -def load_dummy_backend(kwargs): - """ - This function is called by unit tests. - It asserts that urllib3 can be used without having a specific backend installed. - The .dummy_backend module should not exist. - """ - from .dummy_backend import DummyBackend - return DummyBackend(**kwargs) + def __call__(self, *args, **kwargs): + return self.loader(kwargs) def load_sync_backend(kwargs): @@ -24,13 +20,11 @@ def load_sync_backend(kwargs): def load_trio_backend(kwargs): - check_for_python_3_5() from .trio_backend import TrioBackend return TrioBackend(**kwargs) def load_twisted_backend(kwargs): - check_for_python_3_5() from .twisted_backend import TwistedBackend return TwistedBackend(**kwargs) @@ -39,29 +33,85 @@ def backend_directory(): """ We defer any heavy duty imports until the last minute. """ + loaders = [ + Loader( + name="sync", + loader=load_sync_backend, + is_async=False, + ), + Loader( + name="trio", + loader=load_trio_backend, + is_async=True, + ), + Loader( + name="twisted", + loader=load_twisted_backend, + is_async=True, + ), + ] return { - "dummy": load_dummy_backend, - "sync": load_sync_backend, - "trio": load_trio_backend, - "twisted": load_twisted_backend, + loader.name: loader for loader in loaders } def user_specified_unknown_backend(backend_name): - known_backend_names = sorted(backend_directory().keys()) + backend_names = [loader.name for loader in backend_directory().values()] return "Unknown backend specifier {backend_name}. Choose one of: {known_backend_names}".format( backend_name=backend_name, - known_backend_names=", ".join(known_backend_names) + known_backend_names=", ".join(backend_names) ) -def load_backend(user_specified_backend): - if not isinstance(user_specified_backend, UserSpecifiedBackend): - user_specified_backend = UserSpecifiedBackend(name=user_specified_backend) +def async_supported(): + """ + Tests if the async keyword is supported. + """ + async def f(): + """ + Functions with an `async` prefix return a coroutine. + This is removed by the bleaching code, which will change this function to return None. + """ + return None + + obj = f() + if obj is None: + return False + else: + obj.close() # prevent unawaited coroutine warning + return True + + +def user_specified_incompatible_backend(backend_name, is_async_supported, is_async_backend): + lib_kind = "async" if is_async_supported else "sync" + loader_kind = "an async backend" if is_async_backend else "a sync backend" + return "{name} is {loader_kind} which is incompatible with the {lib_kind} version of urllib3.".format( + name=backend_name, + loader_kind=loader_kind, + lib_kind=lib_kind, + ) + + +def normalize_backend(backend): + if backend is None: + backend = Backend(name="sync") # sync backend is the default + elif not isinstance(backend, Backend): + backend = Backend(name=backend) + + loaders_by_name = backend_directory() + if backend.name not in loaders_by_name: + raise ValueError(user_specified_unknown_backend(backend.name)) + + loader = loaders_by_name[backend.name] + + is_async_supported = async_supported() + if is_async_supported != loader.is_async: + raise ValueError(user_specified_incompatible_backend(loader.name, is_async_supported, loader.is_async)) + + return backend - available_loaders = backend_directory() - if user_specified_backend.name not in available_loaders: - raise ValueError(user_specified_unknown_backend(user_specified_backend.name)) - loader = available_loaders[user_specified_backend.name] - return loader(user_specified_backend.kwargs) +def load_backend(backend): + loaders_by_name = backend_directory() + loader = loaders_by_name[backend.name] + return loader(backend.kwargs) From ebbbf8639ccc8eab598569273786a8fee8bd675e Mon Sep 17 00:00:00 2001 From: Oli Kingshott Date: Sun, 6 May 2018 00:09:47 +0800 Subject: [PATCH 07/10] add TestNormalizeBackend --- test/test_backends.py | 71 +++++++++++++++++++++++++++--------- urllib3/_backends/_loader.py | 27 +++----------- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/test/test_backends.py b/test/test_backends.py index e8e10a74..268beee9 100644 --- a/test/test_backends.py +++ b/test/test_backends.py @@ -2,30 +2,67 @@ import pytest -from urllib3.backends import Backend as UserSpecifiedBackend -from urllib3._backends._loader import load_backend +import urllib3 +from urllib3.backends import Backend +from urllib3._backends._loader import normalize_backend, load_backend -class TestLoadBackend(object): +requires_async_pool_manager = pytest.mark.skipif( + not hasattr(urllib3, "AsyncPoolManager"), + reason="async backends require AsyncPoolManager", +) + + +requires_sync_pool_manager = pytest.mark.skipif( + hasattr(urllib3, "AsyncPoolManager"), + reason="sync backends cannot be used with AsyncPoolManager", +) + + +class TestNormalizeBackend(object): """ - We assert that we are able to import compatible backends, - and that we fail correctly if we attempt to use an unavailable or unknown backend. + Assert that we fail correctly if we attempt to use an unknown or incompatible backend. """ - def test_dummy(self): - with pytest.raises(ImportError): - load_backend("dummy") + def test_unknown(self): + with pytest.raises(ValueError) as excinfo: + normalize_backend("_unknown") + assert 'unknown backend specifier _unknown' == str(excinfo.value) + + @requires_sync_pool_manager def test_sync(self): - load_backend("sync") - load_backend(UserSpecifiedBackend("sync")) + assert normalize_backend(Backend("sync")) == Backend("sync") + assert normalize_backend("sync") == Backend("sync") + assert normalize_backend(None) == Backend("sync") + + with pytest.raises(ValueError) as excinfo: + normalize_backend(Backend("trio")) + + assert 'trio backend requires urllib3 to be built with async support' == str(excinfo.value) - @pytest.mark.skipif( - sys.version_info < (3, 5), - reason="async backends require Python 3.5 or greater", - ) + @requires_async_pool_manager def test_async(self): - load_backend("trio") - load_backend("twisted") + assert normalize_backend(Backend("trio")) == Backend("trio") + assert normalize_backend("twisted") == Backend("twisted") + + with pytest.raises(ValueError) as excinfo: + normalize_backend(Backend("sync")) + + assert 'sync backend requires urllib3 to be built without async support' == str(excinfo.value) from twisted.internet import reactor - load_backend(UserSpecifiedBackend("twisted", reactor=reactor)) + assert normalize_backend(Backend("twisted", reactor=reactor)) == Backend("twisted", reactor=reactor) + + +class TestLoadBackend(object): + """ + Assert that we can load a normalized backend + """ + @requires_sync_pool_manager() + def test_sync(self): + load_backend(normalize_backend("sync")) + + @requires_async_pool_manager() + def test_async(self): + from twisted.internet import reactor + load_backend(Backend("twisted", reactor=reactor)) diff --git a/urllib3/_backends/_loader.py b/urllib3/_backends/_loader.py index f92e8b33..d50b7e45 100644 --- a/urllib3/_backends/_loader.py +++ b/urllib3/_backends/_loader.py @@ -55,14 +55,6 @@ def backend_directory(): } -def user_specified_unknown_backend(backend_name): - backend_names = [loader.name for loader in backend_directory().values()] - return "Unknown backend specifier {backend_name}. Choose one of: {known_backend_names}".format( - backend_name=backend_name, - known_backend_names=", ".join(backend_names) - ) - - def async_supported(): """ Tests if the async keyword is supported. @@ -82,16 +74,6 @@ async def f(): return True -def user_specified_incompatible_backend(backend_name, is_async_supported, is_async_backend): - lib_kind = "async" if is_async_supported else "sync" - loader_kind = "an async backend" if is_async_backend else "a sync backend" - return "{name} is {loader_kind} which is incompatible with the {lib_kind} version of urllib3.".format( - name=backend_name, - loader_kind=loader_kind, - lib_kind=lib_kind, - ) - - def normalize_backend(backend): if backend is None: backend = Backend(name="sync") # sync backend is the default @@ -100,13 +82,16 @@ def normalize_backend(backend): loaders_by_name = backend_directory() if backend.name not in loaders_by_name: - raise ValueError(user_specified_unknown_backend(backend.name)) + raise ValueError("unknown backend specifier {}".format(backend.name)) loader = loaders_by_name[backend.name] is_async_supported = async_supported() - if is_async_supported != loader.is_async: - raise ValueError(user_specified_incompatible_backend(loader.name, is_async_supported, loader.is_async)) + if is_async_supported and not loader.is_async: + raise ValueError("{} backend requires urllib3 to be built without async support".format(loader.name)) + + if not is_async_supported and loader.is_async: + raise ValueError("{} backend requires urllib3 to be built with async support".format(loader.name)) return backend From da734eb6359c9506a201d25c73aefaf0cf3c9fca Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 26 Jun 2018 10:20:30 +0400 Subject: [PATCH 08/10] Fix lint --- test/test_backends.py | 13 ++++++++----- test/test_no_ssl.py | 2 -- test/with_dummyserver/test_chunked_transfer.py | 2 -- urllib3/_async/connection.py | 6 ++++-- urllib3/_backends/_loader.py | 10 ++++++---- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/test/test_backends.py b/test/test_backends.py index 268beee9..eb0ed060 100644 --- a/test/test_backends.py +++ b/test/test_backends.py @@ -1,5 +1,3 @@ -import sys - import pytest import urllib3 @@ -38,7 +36,8 @@ def test_sync(self): with pytest.raises(ValueError) as excinfo: normalize_backend(Backend("trio")) - assert 'trio backend requires urllib3 to be built with async support' == str(excinfo.value) + assert ('trio backend requires urllib3 to be built with async support' + == str(excinfo.value)) @requires_async_pool_manager def test_async(self): @@ -48,10 +47,14 @@ def test_async(self): with pytest.raises(ValueError) as excinfo: normalize_backend(Backend("sync")) - assert 'sync backend requires urllib3 to be built without async support' == str(excinfo.value) + assert ( + 'sync backend requires urllib3 to be built without async support' + == str(excinfo.value)) from twisted.internet import reactor - assert normalize_backend(Backend("twisted", reactor=reactor)) == Backend("twisted", reactor=reactor) + assert ( + normalize_backend(Backend("twisted", reactor=reactor)) + == Backend("twisted", reactor=reactor)) class TestLoadBackend(object): diff --git a/test/test_no_ssl.py b/test/test_no_ssl.py index cbea0482..3f04cd8b 100644 --- a/test/test_no_ssl.py +++ b/test/test_no_ssl.py @@ -5,8 +5,6 @@ * HTTPS requests must fail with an error that points at the ssl module """ -import pytest - import sys if sys.version_info >= (2, 7): import unittest diff --git a/test/with_dummyserver/test_chunked_transfer.py b/test/with_dummyserver/test_chunked_transfer.py index b0ad26c0..8348efe5 100644 --- a/test/with_dummyserver/test_chunked_transfer.py +++ b/test/with_dummyserver/test_chunked_transfer.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import pytest - from urllib3 import HTTPConnectionPool from urllib3.exceptions import InvalidBodyError from urllib3.packages import six diff --git a/urllib3/_async/connection.py b/urllib3/_async/connection.py index 60ba5cd5..7f8f6b18 100644 --- a/urllib3/_async/connection.py +++ b/urllib3/_async/connection.py @@ -143,8 +143,10 @@ def all_pieces_iter(): yield state_machine.send(h11.EndOfMessage()) - # Try to combine the header bytes + (first set of body bytes or end of message bytes) into one packet. - # As long as all_pieces_iter() yields at least two messages, this should never raise StopIteration. + # Try to combine the header bytes + (first set of body bytes or end of + # message bytes) into one packet. + # As long as all_pieces_iter() yields at least two messages, this should + # never raise StopIteration. remaining_pieces = all_pieces_iter() first_packet_bytes = next(remaining_pieces) + next(remaining_pieces) all_pieces_combined_iter = itertools.chain([first_packet_bytes], remaining_pieces) diff --git a/urllib3/_backends/_loader.py b/urllib3/_backends/_loader.py index d50b7e45..aece1436 100644 --- a/urllib3/_backends/_loader.py +++ b/urllib3/_backends/_loader.py @@ -1,5 +1,3 @@ -import sys - from ..backends import Backend @@ -88,10 +86,14 @@ def normalize_backend(backend): is_async_supported = async_supported() if is_async_supported and not loader.is_async: - raise ValueError("{} backend requires urllib3 to be built without async support".format(loader.name)) + raise ValueError( + "{} backend requires urllib3 to be built without async support". + format(loader.name)) if not is_async_supported and loader.is_async: - raise ValueError("{} backend requires urllib3 to be built with async support".format(loader.name)) + raise ValueError( + "{} backend requires urllib3 to be built with async support". + format(loader.name)) return backend From 9cd6efc7ccf44014bd36f06ce8f0acbbb984070b Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 27 Jun 2018 09:22:47 +0400 Subject: [PATCH 09/10] Fix async support test urllib3 is always built with both async and sync support, the real question is "are we in async or bleached code?" We answer this question by testing for async support in a bleached part of the code. --- test/test_backends.py | 37 ++++++++++++------------------------ urllib3/_async/connection.py | 21 ++++++++++++++++++-- urllib3/_backends/_loader.py | 36 +++++++---------------------------- 3 files changed, 38 insertions(+), 56 deletions(-) diff --git a/test/test_backends.py b/test/test_backends.py index eb0ed060..8c376db4 100644 --- a/test/test_backends.py +++ b/test/test_backends.py @@ -11,49 +11,37 @@ ) -requires_sync_pool_manager = pytest.mark.skipif( - hasattr(urllib3, "AsyncPoolManager"), - reason="sync backends cannot be used with AsyncPoolManager", -) - - class TestNormalizeBackend(object): """ Assert that we fail correctly if we attempt to use an unknown or incompatible backend. """ def test_unknown(self): with pytest.raises(ValueError) as excinfo: - normalize_backend("_unknown") + normalize_backend("_unknown", async_mode=False) assert 'unknown backend specifier _unknown' == str(excinfo.value) - @requires_sync_pool_manager def test_sync(self): - assert normalize_backend(Backend("sync")) == Backend("sync") - assert normalize_backend("sync") == Backend("sync") - assert normalize_backend(None) == Backend("sync") + assert normalize_backend(Backend("sync"), async_mode=False) == Backend("sync") + assert normalize_backend("sync", async_mode=False) == Backend("sync") + assert normalize_backend(None, async_mode=False) == Backend("sync") with pytest.raises(ValueError) as excinfo: - normalize_backend(Backend("trio")) - - assert ('trio backend requires urllib3 to be built with async support' - == str(excinfo.value)) + normalize_backend(Backend("trio"), async_mode=False) + assert ('trio backend needs to be run in async mode' == str(excinfo.value)) @requires_async_pool_manager def test_async(self): - assert normalize_backend(Backend("trio")) == Backend("trio") - assert normalize_backend("twisted") == Backend("twisted") + assert normalize_backend(Backend("trio"), async_mode=True) == Backend("trio") + assert normalize_backend("twisted", async_mode=True) == Backend("twisted") with pytest.raises(ValueError) as excinfo: - normalize_backend(Backend("sync")) - - assert ( - 'sync backend requires urllib3 to be built without async support' - == str(excinfo.value)) + normalize_backend(Backend("sync"), async_mode=True) + assert ('sync backend needs to be run in sync mode' == str(excinfo.value)) from twisted.internet import reactor assert ( - normalize_backend(Backend("twisted", reactor=reactor)) + normalize_backend(Backend("twisted", reactor=reactor), async_mode=True) == Backend("twisted", reactor=reactor)) @@ -61,9 +49,8 @@ class TestLoadBackend(object): """ Assert that we can load a normalized backend """ - @requires_sync_pool_manager() def test_sync(self): - load_backend(normalize_backend("sync")) + load_backend(normalize_backend("sync", async_mode=False)) @requires_async_pool_manager() def test_async(self): diff --git a/urllib3/_async/connection.py b/urllib3/_async/connection.py index 7f8f6b18..05ee770b 100644 --- a/urllib3/_async/connection.py +++ b/urllib3/_async/connection.py @@ -31,7 +31,7 @@ from urllib3.packages import six from ..util import ssl_ as ssl_util from .._backends._common import LoopAbort -from .._backends._loader import load_backend +from .._backends._loader import load_backend, normalize_backend try: import ssl @@ -39,6 +39,23 @@ ssl = None +def is_async_mode(): + """Tests if we're in the async part of the code or if we're bleached""" + async def f(): + """bleaching transforms async functions in sync functions""" + return None + + obj = f() + if obj is None: + return False + else: + obj.close() # prevent unawaited coroutine warning + return True + + +_ASYNC_MODE = is_async_mode() + + # When updating RECENT_DATE, move it to within two years of the current date, # and not less than 6 months ago. # Example: if Today is 2018-01-01, then RECENT_DATE should be any date on or @@ -313,7 +330,7 @@ def __init__(self, host, port, backend=None, source_address=None, tunnel_host=None, tunnel_port=None, tunnel_headers=None): self.is_verified = False - self._backend = backend or load_backend("sync") + self._backend = load_backend(normalize_backend(backend, _ASYNC_MODE)) self._host = host self._port = port self._socket_options = ( diff --git a/urllib3/_backends/_loader.py b/urllib3/_backends/_loader.py index aece1436..cd2fa0b2 100644 --- a/urllib3/_backends/_loader.py +++ b/urllib3/_backends/_loader.py @@ -53,26 +53,7 @@ def backend_directory(): } -def async_supported(): - """ - Tests if the async keyword is supported. - """ - async def f(): - """ - Functions with an `async` prefix return a coroutine. - This is removed by the bleaching code, which will change this function to return None. - """ - return None - - obj = f() - if obj is None: - return False - else: - obj.close() # prevent unawaited coroutine warning - return True - - -def normalize_backend(backend): +def normalize_backend(backend, async_mode): if backend is None: backend = Backend(name="sync") # sync backend is the default elif not isinstance(backend, Backend): @@ -84,16 +65,13 @@ def normalize_backend(backend): loader = loaders_by_name[backend.name] - is_async_supported = async_supported() - if is_async_supported and not loader.is_async: - raise ValueError( - "{} backend requires urllib3 to be built without async support". - format(loader.name)) + if async_mode and not loader.is_async: + raise ValueError("{} backend needs to be run in sync mode".format( + loader.name)) - if not is_async_supported and loader.is_async: - raise ValueError( - "{} backend requires urllib3 to be built with async support". - format(loader.name)) + if not async_mode and loader.is_async: + raise ValueError("{} backend needs to be run in async mode".format( + loader.name)) return backend From b1df30793d504f6f0575f145730c911cbc81b214 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 27 Jun 2018 09:25:22 +0400 Subject: [PATCH 10/10] demo: specify backends using the new API --- demo/async-demo.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/demo/async-demo.py b/demo/async-demo.py index c86b63e3..9a869d39 100644 --- a/demo/async-demo.py +++ b/demo/async-demo.py @@ -1,8 +1,7 @@ # This should work on python 3.6+ import urllib3 -# TODO: less janky way of specifying backends -from urllib3._backends import TrioBackend, TwistedBackend +from urllib3.backends import Backend URL = "http://httpbin.org/uuid" @@ -15,11 +14,11 @@ async def main(backend): print("--- urllib3 using Trio ---") import trio -trio.run(main, TrioBackend()) +trio.run(main, "trio") print("\n--- urllib3 using Twisted ---") from twisted.internet.task import react from twisted.internet.defer import ensureDeferred def twisted_main(reactor): - return ensureDeferred(main(TwistedBackend(reactor))) + return ensureDeferred(main(Backend("twisted", reactor=reactor))) react(twisted_main)