From a5fad2a56fd77a628438ec20141a746da1439dc2 Mon Sep 17 00:00:00 2001 From: pacrob Date: Tue, 11 Apr 2023 14:55:54 -0600 Subject: [PATCH 1/9] add raise_if_false flag to sync is_connected --- tests/core/providers/test_provider.py | 4 ++-- web3/main.py | 6 +++--- web3/providers/auto.py | 4 ++-- web3/providers/base.py | 24 ++++++++++++++++++------ web3/providers/eth_tester/main.py | 2 +- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/core/providers/test_provider.py b/tests/core/providers/test_provider.py index 8829242d25..5f3ff0c9f2 100644 --- a/tests/core/providers/test_provider.py +++ b/tests/core/providers/test_provider.py @@ -18,12 +18,12 @@ class ConnectedProvider(BaseProvider): - def is_connected(self): + def is_connected(self, raise_if_false: bool = False): return True class DisconnectedProvider(BaseProvider): - def is_connected(self): + def is_connected(self, raise_if_false: bool = False): return False diff --git a/web3/main.py b/web3/main.py index a1d74fb085..dcfad1e924 100644 --- a/web3/main.py +++ b/web3/main.py @@ -378,7 +378,7 @@ def __init__( self.ens = ens - def is_connected(self) -> Coroutine[Any, Any, bool]: + def is_connected(self, raise_if_false: bool = False) -> Coroutine[Any, Any, bool]: return self.provider.is_connected() @property @@ -441,8 +441,8 @@ def __init__( self.ens = ens - def is_connected(self) -> bool: - return self.provider.is_connected() + def is_connected(self, raise_if_false: bool = False) -> bool: + return self.provider.is_connected(raise_if_false) @classmethod def normalize_values( diff --git a/web3/providers/auto.py b/web3/providers/auto.py index 55feea29f5..8f691183c7 100644 --- a/web3/providers/auto.py +++ b/web3/providers/auto.py @@ -94,9 +94,9 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: except OSError: return self._proxy_request(method, params, use_cache=False) - def is_connected(self) -> bool: + def is_connected(self, raise_if_false: bool = False) -> bool: provider = self._get_active_provider(use_cache=True) - return provider is not None and provider.is_connected() + return provider is not None and provider.is_connected(raise_if_false) def _proxy_request( self, method: RPCEndpoint, params: Any, use_cache: bool = True diff --git a/web3/providers/base.py b/web3/providers/base.py index 9c40fb6e38..03045cc3ce 100644 --- a/web3/providers/base.py +++ b/web3/providers/base.py @@ -16,6 +16,9 @@ from web3._utils.encoding import ( FriendlyJsonSerde, ) +from web3.exceptions import ( + Web3Exception, +) from web3.middleware import ( combine_middlewares, ) @@ -83,7 +86,7 @@ def _generate_request_func( def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: raise NotImplementedError("Providers must implement this method") - def is_connected(self) -> bool: + def is_connected(self, raise_if_false: bool = False) -> bool: raise NotImplementedError("Providers must implement this method") @@ -105,13 +108,22 @@ def encode_rpc_request(self, method: RPCEndpoint, params: Any) -> bytes: encoded = FriendlyJsonSerde().json_encode(rpc_dict) return to_bytes(text=encoded) - def is_connected(self) -> bool: + def is_connected(self, raise_if_false: bool = False) -> bool: try: response = self.make_request(RPCEndpoint("web3_clientVersion"), []) - except OSError: + except OSError as e: + if raise_if_false: + raise e return False - assert response["jsonrpc"] == "2.0" - assert "error" not in response + if "error" in response: + if raise_if_false: + raise Web3Exception(f"Error received from provider: {response}") + return False - return True + if response["jsonrpc"] == "2.0": + return True + else: + if raise_if_false: + raise Web3Exception(f"Bad jsonrpc version: {response}") + return False diff --git a/web3/providers/eth_tester/main.py b/web3/providers/eth_tester/main.py index 2fd0e5327f..5a7dd7cbad 100644 --- a/web3/providers/eth_tester/main.py +++ b/web3/providers/eth_tester/main.py @@ -131,7 +131,7 @@ def __init__( def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: return _make_request(method, params, self.api_endpoints, self.ethereum_tester) - def is_connected(self) -> Literal[True]: + def is_connected(self, raise_if_false: bool = False) -> Literal[True]: return True From db8357649d404a8647ac1bf1aa00878f2dfc26e1 Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 12 Apr 2023 11:13:42 -0600 Subject: [PATCH 2/9] apply to async and add new exception type --- web3/exceptions.py | 8 ++++++++ web3/main.py | 2 +- web3/providers/async_base.py | 26 ++++++++++++++++++++------ web3/providers/base.py | 10 ++++++---- web3/providers/eth_tester/main.py | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/web3/exceptions.py b/web3/exceptions.py index 5ab804caf4..17c49eaa5c 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -47,6 +47,14 @@ class BlockNumberOutofRange(Web3Exception): pass +class ProviderConnectionError(Web3Exception): + """ + Raised when unable to connect to a provider + """ + + pass + + class CannotHandleRequest(Web3Exception): """ Raised by a provider to signal that it cannot handle an RPC request and diff --git a/web3/main.py b/web3/main.py index dcfad1e924..9477f4f5f9 100644 --- a/web3/main.py +++ b/web3/main.py @@ -379,7 +379,7 @@ def __init__( self.ens = ens def is_connected(self, raise_if_false: bool = False) -> Coroutine[Any, Any, bool]: - return self.provider.is_connected() + return self.provider.is_connected(raise_if_false) @property def middleware_onion(self) -> AsyncMiddlewareOnion: diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py index 0225d4e4eb..ddcc6a0c4f 100644 --- a/web3/providers/async_base.py +++ b/web3/providers/async_base.py @@ -17,6 +17,9 @@ from web3._utils.encoding import ( FriendlyJsonSerde, ) +from web3.exceptions import ( + ProviderConnectionError, +) from web3.middleware import ( async_combine_middlewares, ) @@ -81,7 +84,7 @@ async def _generate_request_func( async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: raise NotImplementedError("Providers must implement this method") - async def is_connected(self) -> bool: + async def is_connected(self, raise_if_false: bool = False) -> bool: raise NotImplementedError("Providers must implement this method") @@ -104,13 +107,24 @@ def decode_rpc_response(self, raw_response: bytes) -> RPCResponse: text_response = to_text(raw_response) return cast(RPCResponse, FriendlyJsonSerde().json_decode(text_response)) - async def is_connected(self) -> bool: + async def is_connected(self, raise_if_false: bool = False) -> bool: try: response = await self.make_request(RPCEndpoint("web3_clientVersion"), []) - except OSError: + except ProviderConnectionError as e: + if raise_if_false: + raise e return False - assert response["jsonrpc"] == "2.0" - assert "error" not in response + if "error" in response: + if raise_if_false: + raise ProviderConnectionError( + f"Error received from provider: {response}" + ) + return False - return True + if response["jsonrpc"] == "2.0": + return True + else: + if raise_if_false: + raise ProviderConnectionError(f"Bad jsonrpc version: {response}") + return False diff --git a/web3/providers/base.py b/web3/providers/base.py index 03045cc3ce..7fe77cdc0e 100644 --- a/web3/providers/base.py +++ b/web3/providers/base.py @@ -17,7 +17,7 @@ FriendlyJsonSerde, ) from web3.exceptions import ( - Web3Exception, + ProviderConnectionError, ) from web3.middleware import ( combine_middlewares, @@ -111,19 +111,21 @@ def encode_rpc_request(self, method: RPCEndpoint, params: Any) -> bytes: def is_connected(self, raise_if_false: bool = False) -> bool: try: response = self.make_request(RPCEndpoint("web3_clientVersion"), []) - except OSError as e: + except ProviderConnectionError as e: if raise_if_false: raise e return False if "error" in response: if raise_if_false: - raise Web3Exception(f"Error received from provider: {response}") + raise ProviderConnectionError( + f"Error received from provider: {response}" + ) return False if response["jsonrpc"] == "2.0": return True else: if raise_if_false: - raise Web3Exception(f"Bad jsonrpc version: {response}") + raise ProviderConnectionError(f"Bad jsonrpc version: {response}") return False diff --git a/web3/providers/eth_tester/main.py b/web3/providers/eth_tester/main.py index 5a7dd7cbad..47053efc0b 100644 --- a/web3/providers/eth_tester/main.py +++ b/web3/providers/eth_tester/main.py @@ -76,7 +76,7 @@ def __init__(self) -> None: async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: return _make_request(method, params, self.api_endpoints, self.ethereum_tester) - async def is_connected(self) -> Literal[True]: + async def is_connected(self, raise_if_false: bool = False) -> Literal[True]: return True From 49db04f8730cbad46d18c9bda8d4edcf901f9b8e Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 12 Apr 2023 13:57:51 -0600 Subject: [PATCH 3/9] return default ipc path even if it doesn't exist yet --- web3/exceptions.py | 2 +- web3/providers/ipc.py | 56 ++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/web3/exceptions.py b/web3/exceptions.py index 17c49eaa5c..01e2eba465 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -47,7 +47,7 @@ class BlockNumberOutofRange(Web3Exception): pass -class ProviderConnectionError(Web3Exception): +class ProviderConnectionError(Web3Exception, OSError): """ Raised when unable to connect to a provider """ diff --git a/web3/providers/ipc.py b/web3/providers/ipc.py index ae749b355c..6c257f2851 100644 --- a/web3/providers/ipc.py +++ b/web3/providers/ipc.py @@ -22,6 +22,9 @@ from web3._utils.threads import ( Timeout, ) +from web3.exceptions import ( + ProviderConnectionError, +) from web3.types import ( RPCEndpoint, RPCResponse, @@ -182,35 +185,38 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: ) request = self.encode_rpc_request(method, params) - with self._lock, self._socket as sock: - try: - sock.sendall(request) - except BrokenPipeError: - # one extra attempt, then give up - sock = self._socket.reset() - sock.sendall(request) - - raw_response = b"" - with Timeout(self.timeout) as timeout: - while True: - try: - raw_response += sock.recv(4096) - except socket.timeout: - timeout.sleep(0) - continue - if raw_response == b"": - timeout.sleep(0) - elif has_valid_json_rpc_ending(raw_response): + try: + with self._lock, self._socket as sock: + try: + sock.sendall(request) + except BrokenPipeError: + # one extra attempt, then give up + sock = self._socket.reset() + sock.sendall(request) + + raw_response = b"" + with Timeout(self.timeout) as timeout: + while True: try: - response = self.decode_rpc_response(raw_response) - except JSONDecodeError: + raw_response += sock.recv(4096) + except socket.timeout: timeout.sleep(0) continue + if raw_response == b"": + timeout.sleep(0) + elif has_valid_json_rpc_ending(raw_response): + try: + response = self.decode_rpc_response(raw_response) + except JSONDecodeError: + timeout.sleep(0) + continue + else: + return response else: - return response - else: - timeout.sleep(0) - continue + timeout.sleep(0) + continue + except OSError as e: + raise ProviderConnectionError(f"Cannot connect to provider with error: {e}") # A valid JSON RPC response can only end in } or ] http://www.jsonrpc.org/specification From b990ac33944700d59b75a27c775c16d5ec339e2e Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 12 Apr 2023 14:43:58 -0600 Subject: [PATCH 4/9] add documentation --- docs/internals.rst | 6 +++++- docs/troubleshooting.rst | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 32f319fd12..36e3edcfa3 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -101,13 +101,17 @@ setting the middlewares the provider should use. the JSON-RPC method being called. -.. py:method:: BaseProvider.is_connected() +.. py:method:: BaseProvider.is_connected(raise_if_false=False) This function should return ``True`` or ``False`` depending on whether the provider should be considered *connected*. For example, an IPC socket based provider should return ``True`` if the socket is open and ``False`` if the socket is closed. + If set to ``True``, the optional ``raise_if_false`` boolean should raise a + ``ProviderConnectionError`` and provide information on why the provider should + not be considered *connected*. + .. py:attribute:: BaseProvider.middlewares diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index e840756b9d..52bb3d7988 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -73,9 +73,19 @@ You can check that your instance is connected via the ``is_connected`` method: >>> w3.is_connected() False -There's a variety of explanations for why you may see ``False`` here. If you're -running a local node, such as Geth, double-check that you've indeed started the -binary and that you've started it from the intended directory - particularly if +There are a variety of explanations for why you may see ``False`` here. To help you +diagnose the problem, ``is_connected`` has an optional ``raise_if_false`` argument: + +.. code-block:: python + + >>> w3.is_connected() + # this is an example, your error may differ + + # + ProviderConnectionError: Cannot connect to provider with error: [Errno 2] No such file or directory + +If you're running a local node, such as Geth, double-check that you've indeed started +the binary and that you've started it from the intended directory - particularly if you've specified a relative path to its ipc file. If that does not address your issue, it's probable that you still have a From 26aca0135ecc84a19cb534c7679683f9fc80d380 Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 19 Apr 2023 07:27:37 -0600 Subject: [PATCH 5/9] Except OSError and raise as ProviderConnectionError --- web3/exceptions.py | 2 +- web3/providers/base.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web3/exceptions.py b/web3/exceptions.py index 01e2eba465..17c49eaa5c 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -47,7 +47,7 @@ class BlockNumberOutofRange(Web3Exception): pass -class ProviderConnectionError(Web3Exception, OSError): +class ProviderConnectionError(Web3Exception): """ Raised when unable to connect to a provider """ diff --git a/web3/providers/base.py b/web3/providers/base.py index 7fe77cdc0e..1d60fe58b0 100644 --- a/web3/providers/base.py +++ b/web3/providers/base.py @@ -111,9 +111,11 @@ def encode_rpc_request(self, method: RPCEndpoint, params: Any) -> bytes: def is_connected(self, raise_if_false: bool = False) -> bool: try: response = self.make_request(RPCEndpoint("web3_clientVersion"), []) - except ProviderConnectionError as e: + except OSError as e: if raise_if_false: - raise e + raise ProviderConnectionError( + f"Problem connecting to provider with error: {e}" + ) return False if "error" in response: From cfecc38a5ea71571a09df5cd2aa95ee275af2909 Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 19 Apr 2023 15:33:30 -0600 Subject: [PATCH 6/9] remove unnecessary try-except --- web3/providers/async_base.py | 6 ++-- web3/providers/ipc.py | 56 ++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py index ddcc6a0c4f..c7c92be3c2 100644 --- a/web3/providers/async_base.py +++ b/web3/providers/async_base.py @@ -110,9 +110,11 @@ def decode_rpc_response(self, raw_response: bytes) -> RPCResponse: async def is_connected(self, raise_if_false: bool = False) -> bool: try: response = await self.make_request(RPCEndpoint("web3_clientVersion"), []) - except ProviderConnectionError as e: + except OSError as e: if raise_if_false: - raise e + raise ProviderConnectionError( + f"Problem connecting to provider with error: {e}" + ) return False if "error" in response: diff --git a/web3/providers/ipc.py b/web3/providers/ipc.py index 6c257f2851..ae749b355c 100644 --- a/web3/providers/ipc.py +++ b/web3/providers/ipc.py @@ -22,9 +22,6 @@ from web3._utils.threads import ( Timeout, ) -from web3.exceptions import ( - ProviderConnectionError, -) from web3.types import ( RPCEndpoint, RPCResponse, @@ -185,38 +182,35 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: ) request = self.encode_rpc_request(method, params) - try: - with self._lock, self._socket as sock: - try: - sock.sendall(request) - except BrokenPipeError: - # one extra attempt, then give up - sock = self._socket.reset() - sock.sendall(request) - - raw_response = b"" - with Timeout(self.timeout) as timeout: - while True: + with self._lock, self._socket as sock: + try: + sock.sendall(request) + except BrokenPipeError: + # one extra attempt, then give up + sock = self._socket.reset() + sock.sendall(request) + + raw_response = b"" + with Timeout(self.timeout) as timeout: + while True: + try: + raw_response += sock.recv(4096) + except socket.timeout: + timeout.sleep(0) + continue + if raw_response == b"": + timeout.sleep(0) + elif has_valid_json_rpc_ending(raw_response): try: - raw_response += sock.recv(4096) - except socket.timeout: + response = self.decode_rpc_response(raw_response) + except JSONDecodeError: timeout.sleep(0) continue - if raw_response == b"": - timeout.sleep(0) - elif has_valid_json_rpc_ending(raw_response): - try: - response = self.decode_rpc_response(raw_response) - except JSONDecodeError: - timeout.sleep(0) - continue - else: - return response else: - timeout.sleep(0) - continue - except OSError as e: - raise ProviderConnectionError(f"Cannot connect to provider with error: {e}") + return response + else: + timeout.sleep(0) + continue # A valid JSON RPC response can only end in } or ] http://www.jsonrpc.org/specification From 37b0630b50b1b02499023cc75e83df6009e34128 Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 20 Apr 2023 08:55:12 -0600 Subject: [PATCH 7/9] set sync and async tester provider tests to match --- ...st_provider.py => test_tester_provider.py} | 65 +++++-------------- 1 file changed, 15 insertions(+), 50 deletions(-) rename tests/core/providers/{test_provider.py => test_tester_provider.py} (60%) diff --git a/tests/core/providers/test_provider.py b/tests/core/providers/test_tester_provider.py similarity index 60% rename from tests/core/providers/test_provider.py rename to tests/core/providers/test_tester_provider.py index 5f3ff0c9f2..6f36a4bb5e 100644 --- a/tests/core/providers/test_provider.py +++ b/tests/core/providers/test_tester_provider.py @@ -6,65 +6,30 @@ from web3 import ( EthereumTesterProvider, - Web3, -) -from web3.providers import ( - AutoProvider, - BaseProvider, ) from web3.types import ( RPCEndpoint, ) -class ConnectedProvider(BaseProvider): - def is_connected(self, raise_if_false: bool = False): - return True - - -class DisconnectedProvider(BaseProvider): - def is_connected(self, raise_if_false: bool = False): - return False - - -def test_is_connected_connected(): - """ - Web3.is_connected() returns True when connected to a node. - """ - w3 = Web3(ConnectedProvider()) - assert w3.is_connected() is True - - -def test_is_connected_disconnected(): - """ - Web3.is_connected() returns False when configured with a provider - that's not connected to a node. - """ - w3 = Web3(DisconnectedProvider()) - assert w3.is_connected() is False - - -def test_autoprovider_detection(): - def no_provider(): - return None +def test_async_tester_provider_is_connected() -> None: + provider = EthereumTesterProvider() + connected = provider.is_connected() + assert connected - def must_not_call(): - assert False - auto = AutoProvider( - [ - no_provider, - DisconnectedProvider, - ConnectedProvider, - must_not_call, - ] +def test_async_tester_provider_creates_a_block() -> None: + provider = EthereumTesterProvider() + accounts = provider.make_request("eth_accounts", []) + a, b = accounts["result"][:2] + current_block = provider.make_request("eth_blockNumber", []) + assert current_block["result"] == 0 + tx = provider.make_request( + "eth_sendTransaction", [{"from": a, "to": b, "gas": 21000}] ) - - w3 = Web3(auto) - - assert w3.is_connected() - - assert isinstance(auto._active_provider, ConnectedProvider) + assert tx + current_block = provider.make_request("eth_blockNumber", []) + assert current_block["result"] == 1 @pytest.mark.parametrize( From 691a44eaa6398cb6948ee6b42684bb9ae6b51b52 Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 20 Apr 2023 09:38:38 -0600 Subject: [PATCH 8/9] add is_connected tests across all providers --- docs/troubleshooting.rst | 4 +- newsfragments/2912.feature.rst | 1 + .../providers/test_async_http_provider.py | 11 +++- tests/core/providers/test_base_provider.py | 57 +++++++++++++++++++ tests/core/providers/test_http_provider.py | 8 +++ tests/core/providers/test_ipc_provider.py | 5 ++ tests/core/providers/test_tester_provider.py | 4 +- .../core/providers/test_websocket_provider.py | 11 ++++ web3/providers/async_base.py | 2 +- web3/providers/base.py | 2 +- 10 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 newsfragments/2912.feature.rst create mode 100644 tests/core/providers/test_base_provider.py diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 52bb3d7988..2abb5c6ae7 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -78,11 +78,11 @@ diagnose the problem, ``is_connected`` has an optional ``raise_if_false`` argume .. code-block:: python - >>> w3.is_connected() + >>> w3.is_connected(raise_if_false=True) # this is an example, your error may differ # - ProviderConnectionError: Cannot connect to provider with error: [Errno 2] No such file or directory + ProviderConnectionError: Problem connecting to provider with error: : cannot connect to IPC socket at path: None If you're running a local node, such as Geth, double-check that you've indeed started the binary and that you've started it from the intended directory - particularly if diff --git a/newsfragments/2912.feature.rst b/newsfragments/2912.feature.rst new file mode 100644 index 0000000000..ff6d3cbde1 --- /dev/null +++ b/newsfragments/2912.feature.rst @@ -0,0 +1 @@ +add raise_if_false flag to is_connected to allow user to see connection error reason diff --git a/tests/core/providers/test_async_http_provider.py b/tests/core/providers/test_async_http_provider.py index b5aeed096c..88a0261cd4 100644 --- a/tests/core/providers/test_async_http_provider.py +++ b/tests/core/providers/test_async_http_provider.py @@ -13,6 +13,9 @@ from web3.eth import ( AsyncEth, ) +from web3.exceptions import ( + ProviderConnectionError, +) from web3.geth import ( AsyncGeth, AsyncGethAdmin, @@ -35,11 +38,15 @@ URI = "http://mynode.local:8545" -def test_no_args(): +@pytest.mark.asyncio +async def test_no_args(): provider = AsyncHTTPProvider() w3 = AsyncWeb3(provider) assert w3.manager.provider == provider assert w3.manager.provider.is_async + assert not await w3.is_connected() + with pytest.raises(ProviderConnectionError): + await w3.is_connected(raise_if_false=True) def test_init_kwargs(): @@ -83,5 +90,5 @@ async def test_user_provided_session() -> None: session = ClientSession() provider = AsyncHTTPProvider(endpoint_uri=URI) cached_session = await provider.cache_async_session(session) - assert len(request._async_session_cache) == 1 + assert len(request._async_session_cache) == 2 assert cached_session == session diff --git a/tests/core/providers/test_base_provider.py b/tests/core/providers/test_base_provider.py new file mode 100644 index 0000000000..ec2b73d9aa --- /dev/null +++ b/tests/core/providers/test_base_provider.py @@ -0,0 +1,57 @@ +from web3 import ( + Web3, +) +from web3.providers import ( + AutoProvider, + BaseProvider, +) + + +class ConnectedProvider(BaseProvider): + def is_connected(self, raise_if_false: bool = False): + return True + + +class DisconnectedProvider(BaseProvider): + def is_connected(self, raise_if_false: bool = False): + return False + + +def test_is_connected_connected(): + """ + Web3.is_connected() returns True when connected to a node. + """ + w3 = Web3(ConnectedProvider()) + assert w3.is_connected() is True + + +def test_is_connected_disconnected(): + """ + Web3.is_connected() returns False when configured with a provider + that's not connected to a node. + """ + w3 = Web3(DisconnectedProvider()) + assert w3.is_connected() is False + + +def test_autoprovider_detection(): + def no_provider(): + return None + + def must_not_call(): + assert False + + auto = AutoProvider( + [ + no_provider, + DisconnectedProvider, + ConnectedProvider, + must_not_call, + ] + ) + + w3 = Web3(auto) + + assert w3.is_connected() + + assert isinstance(auto._active_provider, ConnectedProvider) diff --git a/tests/core/providers/test_http_provider.py b/tests/core/providers/test_http_provider.py index 7f4f5c035c..c2d2af127b 100644 --- a/tests/core/providers/test_http_provider.py +++ b/tests/core/providers/test_http_provider.py @@ -1,3 +1,5 @@ +import pytest + from requests import ( Session, ) @@ -11,6 +13,9 @@ from web3._utils import ( request, ) +from web3.exceptions import ( + ProviderConnectionError, +) from web3.providers import ( HTTPProvider, ) @@ -23,6 +28,9 @@ def test_no_args(): w3 = Web3(provider) assert w3.manager.provider == provider assert not w3.manager.provider.is_async + assert not w3.is_connected() + with pytest.raises(ProviderConnectionError): + w3.is_connected(raise_if_false=True) def test_init_kwargs(): diff --git a/tests/core/providers/test_ipc_provider.py b/tests/core/providers/test_ipc_provider.py index 380aa9264b..41b1ecca78 100644 --- a/tests/core/providers/test_ipc_provider.py +++ b/tests/core/providers/test_ipc_provider.py @@ -12,6 +12,9 @@ from web3.auto.gethdev import ( w3, ) +from web3.exceptions import ( + ProviderConnectionError, +) from web3.middleware import ( construct_fixture_middleware, ) @@ -37,6 +40,8 @@ def test_ipc_no_path(): """ ipc = IPCProvider(None) assert ipc.is_connected() is False + with pytest.raises(ProviderConnectionError): + ipc.is_connected(raise_if_false=True) def test_ipc_tilda_in_path(): diff --git a/tests/core/providers/test_tester_provider.py b/tests/core/providers/test_tester_provider.py index 6f36a4bb5e..7d977168e1 100644 --- a/tests/core/providers/test_tester_provider.py +++ b/tests/core/providers/test_tester_provider.py @@ -12,13 +12,13 @@ ) -def test_async_tester_provider_is_connected() -> None: +def test_tester_provider_is_connected() -> None: provider = EthereumTesterProvider() connected = provider.is_connected() assert connected -def test_async_tester_provider_creates_a_block() -> None: +def test_tester_provider_creates_a_block() -> None: provider = EthereumTesterProvider() accounts = provider.make_request("eth_accounts", []) a, b = accounts["result"][:2] diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py index 766318f304..a93af4ef00 100644 --- a/tests/core/providers/test_websocket_provider.py +++ b/tests/core/providers/test_websocket_provider.py @@ -14,6 +14,7 @@ Web3, ) from web3.exceptions import ( + ProviderConnectionError, Web3ValidationError, ) from web3.providers.websocket import ( @@ -63,6 +64,16 @@ def w3(open_port, start_websocket_server): return Web3(provider) +def test_no_args(): + provider = WebsocketProvider() + w3 = Web3(provider) + assert w3.manager.provider == provider + assert not w3.manager.provider.is_async + assert not w3.is_connected() + with pytest.raises(ProviderConnectionError): + w3.is_connected(raise_if_false=True) + + def test_websocket_provider_timeout(w3): with pytest.raises(TimeoutError): w3.eth.accounts diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py index c7c92be3c2..426578202a 100644 --- a/web3/providers/async_base.py +++ b/web3/providers/async_base.py @@ -113,7 +113,7 @@ async def is_connected(self, raise_if_false: bool = False) -> bool: except OSError as e: if raise_if_false: raise ProviderConnectionError( - f"Problem connecting to provider with error: {e}" + f"Problem connecting to provider with error: {type(e)}: {e}" ) return False diff --git a/web3/providers/base.py b/web3/providers/base.py index 1d60fe58b0..2faa9e352c 100644 --- a/web3/providers/base.py +++ b/web3/providers/base.py @@ -114,7 +114,7 @@ def is_connected(self, raise_if_false: bool = False) -> bool: except OSError as e: if raise_if_false: raise ProviderConnectionError( - f"Problem connecting to provider with error: {e}" + f"Problem connecting to provider with error: {type(e)}: {e}" ) return False From 06d6ee29a0db97062162834073cda7d69f794d1c Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 24 Apr 2023 10:13:21 -0600 Subject: [PATCH 9/9] change raise_if_false to show_traceback --- docs/internals.rst | 4 ++-- docs/troubleshooting.rst | 4 ++-- newsfragments/2912.feature.rst | 2 +- tests/core/providers/test_async_http_provider.py | 16 +++++++++++++--- tests/core/providers/test_base_provider.py | 4 ++-- tests/core/providers/test_http_provider.py | 2 +- tests/core/providers/test_ipc_provider.py | 2 +- tests/core/providers/test_websocket_provider.py | 2 +- web3/main.py | 8 ++++---- web3/providers/async_base.py | 10 +++++----- web3/providers/auto.py | 4 ++-- web3/providers/base.py | 10 +++++----- web3/providers/eth_tester/main.py | 4 ++-- 13 files changed, 41 insertions(+), 31 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 36e3edcfa3..ca4499651a 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -101,14 +101,14 @@ setting the middlewares the provider should use. the JSON-RPC method being called. -.. py:method:: BaseProvider.is_connected(raise_if_false=False) +.. py:method:: BaseProvider.is_connected(show_traceback=False) This function should return ``True`` or ``False`` depending on whether the provider should be considered *connected*. For example, an IPC socket based provider should return ``True`` if the socket is open and ``False`` if the socket is closed. - If set to ``True``, the optional ``raise_if_false`` boolean should raise a + If set to ``True``, the optional ``show_traceback`` boolean will raise a ``ProviderConnectionError`` and provide information on why the provider should not be considered *connected*. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 2abb5c6ae7..0ffd70c872 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -74,11 +74,11 @@ You can check that your instance is connected via the ``is_connected`` method: False There are a variety of explanations for why you may see ``False`` here. To help you -diagnose the problem, ``is_connected`` has an optional ``raise_if_false`` argument: +diagnose the problem, ``is_connected`` has an optional ``show_traceback`` argument: .. code-block:: python - >>> w3.is_connected(raise_if_false=True) + >>> w3.is_connected(show_traceback=True) # this is an example, your error may differ # diff --git a/newsfragments/2912.feature.rst b/newsfragments/2912.feature.rst index ff6d3cbde1..4fd7c424a8 100644 --- a/newsfragments/2912.feature.rst +++ b/newsfragments/2912.feature.rst @@ -1 +1 @@ -add raise_if_false flag to is_connected to allow user to see connection error reason +add show_traceback flag to is_connected to allow user to see connection error reason diff --git a/tests/core/providers/test_async_http_provider.py b/tests/core/providers/test_async_http_provider.py index 88a0261cd4..05ab031f5f 100644 --- a/tests/core/providers/test_async_http_provider.py +++ b/tests/core/providers/test_async_http_provider.py @@ -38,15 +38,25 @@ URI = "http://mynode.local:8545" +async def clean_async_session_cache(): + cache_data = request._async_session_cache._data + while len(cache_data) > 0: + _key, cached_session = cache_data.popitem() + await cached_session.close() + + @pytest.mark.asyncio -async def test_no_args(): +async def test_no_args() -> None: provider = AsyncHTTPProvider() w3 = AsyncWeb3(provider) assert w3.manager.provider == provider assert w3.manager.provider.is_async assert not await w3.is_connected() with pytest.raises(ProviderConnectionError): - await w3.is_connected(raise_if_false=True) + await w3.is_connected(show_traceback=True) + + await clean_async_session_cache() + assert len(request._async_session_cache) == 0 def test_init_kwargs(): @@ -90,5 +100,5 @@ async def test_user_provided_session() -> None: session = ClientSession() provider = AsyncHTTPProvider(endpoint_uri=URI) cached_session = await provider.cache_async_session(session) - assert len(request._async_session_cache) == 2 + assert len(request._async_session_cache) == 1 assert cached_session == session diff --git a/tests/core/providers/test_base_provider.py b/tests/core/providers/test_base_provider.py index ec2b73d9aa..14a2878f96 100644 --- a/tests/core/providers/test_base_provider.py +++ b/tests/core/providers/test_base_provider.py @@ -8,12 +8,12 @@ class ConnectedProvider(BaseProvider): - def is_connected(self, raise_if_false: bool = False): + def is_connected(self, show_traceback: bool = False): return True class DisconnectedProvider(BaseProvider): - def is_connected(self, raise_if_false: bool = False): + def is_connected(self, show_traceback: bool = False): return False diff --git a/tests/core/providers/test_http_provider.py b/tests/core/providers/test_http_provider.py index c2d2af127b..e94ba69bc9 100644 --- a/tests/core/providers/test_http_provider.py +++ b/tests/core/providers/test_http_provider.py @@ -30,7 +30,7 @@ def test_no_args(): assert not w3.manager.provider.is_async assert not w3.is_connected() with pytest.raises(ProviderConnectionError): - w3.is_connected(raise_if_false=True) + w3.is_connected(show_traceback=True) def test_init_kwargs(): diff --git a/tests/core/providers/test_ipc_provider.py b/tests/core/providers/test_ipc_provider.py index 41b1ecca78..17b4a5a6bb 100644 --- a/tests/core/providers/test_ipc_provider.py +++ b/tests/core/providers/test_ipc_provider.py @@ -41,7 +41,7 @@ def test_ipc_no_path(): ipc = IPCProvider(None) assert ipc.is_connected() is False with pytest.raises(ProviderConnectionError): - ipc.is_connected(raise_if_false=True) + ipc.is_connected(show_traceback=True) def test_ipc_tilda_in_path(): diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py index a93af4ef00..405add29f0 100644 --- a/tests/core/providers/test_websocket_provider.py +++ b/tests/core/providers/test_websocket_provider.py @@ -71,7 +71,7 @@ def test_no_args(): assert not w3.manager.provider.is_async assert not w3.is_connected() with pytest.raises(ProviderConnectionError): - w3.is_connected(raise_if_false=True) + w3.is_connected(show_traceback=True) def test_websocket_provider_timeout(w3): diff --git a/web3/main.py b/web3/main.py index 9477f4f5f9..8a01732f4b 100644 --- a/web3/main.py +++ b/web3/main.py @@ -378,8 +378,8 @@ def __init__( self.ens = ens - def is_connected(self, raise_if_false: bool = False) -> Coroutine[Any, Any, bool]: - return self.provider.is_connected(raise_if_false) + def is_connected(self, show_traceback: bool = False) -> Coroutine[Any, Any, bool]: + return self.provider.is_connected(show_traceback) @property def middleware_onion(self) -> AsyncMiddlewareOnion: @@ -441,8 +441,8 @@ def __init__( self.ens = ens - def is_connected(self, raise_if_false: bool = False) -> bool: - return self.provider.is_connected(raise_if_false) + def is_connected(self, show_traceback: bool = False) -> bool: + return self.provider.is_connected(show_traceback) @classmethod def normalize_values( diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py index 426578202a..bbc4d5d997 100644 --- a/web3/providers/async_base.py +++ b/web3/providers/async_base.py @@ -84,7 +84,7 @@ async def _generate_request_func( async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: raise NotImplementedError("Providers must implement this method") - async def is_connected(self, raise_if_false: bool = False) -> bool: + async def is_connected(self, show_traceback: bool = False) -> bool: raise NotImplementedError("Providers must implement this method") @@ -107,18 +107,18 @@ def decode_rpc_response(self, raw_response: bytes) -> RPCResponse: text_response = to_text(raw_response) return cast(RPCResponse, FriendlyJsonSerde().json_decode(text_response)) - async def is_connected(self, raise_if_false: bool = False) -> bool: + async def is_connected(self, show_traceback: bool = False) -> bool: try: response = await self.make_request(RPCEndpoint("web3_clientVersion"), []) except OSError as e: - if raise_if_false: + if show_traceback: raise ProviderConnectionError( f"Problem connecting to provider with error: {type(e)}: {e}" ) return False if "error" in response: - if raise_if_false: + if show_traceback: raise ProviderConnectionError( f"Error received from provider: {response}" ) @@ -127,6 +127,6 @@ async def is_connected(self, raise_if_false: bool = False) -> bool: if response["jsonrpc"] == "2.0": return True else: - if raise_if_false: + if show_traceback: raise ProviderConnectionError(f"Bad jsonrpc version: {response}") return False diff --git a/web3/providers/auto.py b/web3/providers/auto.py index 8f691183c7..1ab806094b 100644 --- a/web3/providers/auto.py +++ b/web3/providers/auto.py @@ -94,9 +94,9 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: except OSError: return self._proxy_request(method, params, use_cache=False) - def is_connected(self, raise_if_false: bool = False) -> bool: + def is_connected(self, show_traceback: bool = False) -> bool: provider = self._get_active_provider(use_cache=True) - return provider is not None and provider.is_connected(raise_if_false) + return provider is not None and provider.is_connected(show_traceback) def _proxy_request( self, method: RPCEndpoint, params: Any, use_cache: bool = True diff --git a/web3/providers/base.py b/web3/providers/base.py index 2faa9e352c..40e2ace2b3 100644 --- a/web3/providers/base.py +++ b/web3/providers/base.py @@ -86,7 +86,7 @@ def _generate_request_func( def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: raise NotImplementedError("Providers must implement this method") - def is_connected(self, raise_if_false: bool = False) -> bool: + def is_connected(self, show_traceback: bool = False) -> bool: raise NotImplementedError("Providers must implement this method") @@ -108,18 +108,18 @@ def encode_rpc_request(self, method: RPCEndpoint, params: Any) -> bytes: encoded = FriendlyJsonSerde().json_encode(rpc_dict) return to_bytes(text=encoded) - def is_connected(self, raise_if_false: bool = False) -> bool: + def is_connected(self, show_traceback: bool = False) -> bool: try: response = self.make_request(RPCEndpoint("web3_clientVersion"), []) except OSError as e: - if raise_if_false: + if show_traceback: raise ProviderConnectionError( f"Problem connecting to provider with error: {type(e)}: {e}" ) return False if "error" in response: - if raise_if_false: + if show_traceback: raise ProviderConnectionError( f"Error received from provider: {response}" ) @@ -128,6 +128,6 @@ def is_connected(self, raise_if_false: bool = False) -> bool: if response["jsonrpc"] == "2.0": return True else: - if raise_if_false: + if show_traceback: raise ProviderConnectionError(f"Bad jsonrpc version: {response}") return False diff --git a/web3/providers/eth_tester/main.py b/web3/providers/eth_tester/main.py index 47053efc0b..ea841aea80 100644 --- a/web3/providers/eth_tester/main.py +++ b/web3/providers/eth_tester/main.py @@ -76,7 +76,7 @@ def __init__(self) -> None: async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: return _make_request(method, params, self.api_endpoints, self.ethereum_tester) - async def is_connected(self, raise_if_false: bool = False) -> Literal[True]: + async def is_connected(self, show_traceback: bool = False) -> Literal[True]: return True @@ -131,7 +131,7 @@ def __init__( def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: return _make_request(method, params, self.api_endpoints, self.ethereum_tester) - def is_connected(self, raise_if_false: bool = False) -> Literal[True]: + def is_connected(self, show_traceback: bool = False) -> Literal[True]: return True