Skip to content

Commit b5f4878

Browse files
committed
Add AsyncAutoProvider and related async provider functionality
- Introduced AsyncAutoProvider for automatic detection of asynchronous providers. - Implemented load_async_provider_from_environment and load_async_provider_from_uri functions. - Updated existing provider loading functions to raise Web3ValidationError for WebSocket URIs. - Enhanced tests for async provider loading and validation. - Updated documentation for new async provider features.
1 parent 231877c commit b5f4878

File tree

6 files changed

+781
-17
lines changed

6 files changed

+781
-17
lines changed

docs/providers.rst

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,26 @@ Valid formats for this environment variable are:
6565
- ``file:///path/to/node/rpc-json/file.ipc``
6666
- ``http://192.168.1.2:8545``
6767
- ``https://node.ontheweb.com``
68-
- ``ws://127.0.0.1:8546``
68+
- ``ws://127.0.0.1:8546`` (requires ``AsyncWeb3``)
69+
- ``wss://mainnet.infura.io/ws/v3/YOUR_KEY`` (requires ``AsyncWeb3``)
70+
71+
For WebSocket connections (``ws://`` or ``wss://``), you can also use the
72+
``WEB3_WS_PROVIDER_URI`` environment variable. Note that WebSocket providers
73+
are asynchronous and require ``AsyncWeb3`` with ``AsyncAutoProvider``:
74+
75+
.. code-block:: python
76+
77+
>>> import asyncio
78+
>>> from web3 import AsyncWeb3, AsyncAutoProvider
79+
80+
>>> async def main():
81+
... # AsyncAutoProvider will automatically detect WEB3_PROVIDER_URI
82+
... # or WEB3_WS_PROVIDER_URI and use the appropriate async provider
83+
... w3 = AsyncWeb3(AsyncAutoProvider())
84+
... if await w3.is_connected():
85+
... print(await w3.eth.block_number)
86+
87+
>>> asyncio.run(main())
6988
7089
7190
Auto-initialization Provider Shortcuts
@@ -530,9 +549,85 @@ Interacting with the Persistent Connection
530549
AutoProvider
531550
~~~~~~~~~~~~
532551

533-
:class:`~web3.providers.auto.AutoProvider` is the default used when initializing
534-
:class:`web3.Web3` without any providers. There's rarely a reason to use it
535-
explicitly.
552+
.. py:class:: web3.providers.auto.AutoProvider(potential_providers=None)
553+
554+
:class:`~web3.providers.auto.AutoProvider` is the default used when initializing
555+
:class:`web3.Web3` without any providers. It automatically detects and connects
556+
to available synchronous providers.
557+
558+
* ``potential_providers`` is an optional ordered sequence of provider classes
559+
or functions to attempt connection with. If not specified, defaults to
560+
``(load_provider_from_environment, IPCProvider, HTTPProvider)``.
561+
562+
``AutoProvider`` will iterate through the list of potential providers and use
563+
the first one that successfully connects to a node.
564+
565+
.. code-block:: python
566+
567+
>>> from web3 import Web3
568+
569+
# These are equivalent - AutoProvider is used by default
570+
>>> w3 = Web3()
571+
>>> w3 = Web3(Web3.AutoProvider())
572+
573+
.. note::
574+
575+
``AutoProvider`` only supports synchronous providers (HTTP, IPC). For
576+
WebSocket connections, use ``AsyncAutoProvider`` with ``AsyncWeb3``.
577+
578+
579+
AsyncAutoProvider
580+
~~~~~~~~~~~~~~~~~
581+
582+
.. py:class:: web3.providers.auto.AsyncAutoProvider(potential_providers=None)
583+
584+
:class:`~web3.providers.auto.AsyncAutoProvider` is the asynchronous equivalent
585+
of ``AutoProvider``. It automatically detects and connects to available
586+
asynchronous providers, including WebSocket.
587+
588+
* ``potential_providers`` is an optional ordered sequence of async provider
589+
classes or functions to attempt connection with. If not specified, defaults to
590+
``(load_async_provider_from_environment, AsyncHTTPProvider)``.
591+
592+
``AsyncAutoProvider`` checks the ``WEB3_PROVIDER_URI`` and ``WEB3_WS_PROVIDER_URI``
593+
environment variables and automatically selects the appropriate async provider
594+
based on the URI scheme:
595+
596+
- ``http://`` or ``https://`` → ``AsyncHTTPProvider``
597+
- ``ws://`` or ``wss://`` → ``WebSocketProvider``
598+
- ``file://`` → ``AsyncIPCProvider``
599+
600+
.. code-block:: python
601+
602+
>>> import asyncio
603+
>>> from web3 import AsyncWeb3, AsyncAutoProvider
604+
605+
>>> async def main():
606+
... w3 = AsyncWeb3(AsyncAutoProvider())
607+
... if await w3.is_connected():
608+
... block = await w3.eth.block_number
609+
... print(f"Current block: {block}")
610+
611+
>>> asyncio.run(main())
612+
613+
For WebSocket connections that require explicit connection management, you can
614+
combine ``AsyncAutoProvider`` with the context manager pattern:
615+
616+
.. code-block:: python
617+
618+
>>> import os
619+
>>> import asyncio
620+
>>> from web3 import AsyncWeb3, AsyncAutoProvider
621+
622+
>>> os.environ["WEB3_PROVIDER_URI"] = "wss://mainnet.infura.io/ws/v3/YOUR_KEY"
623+
624+
>>> async def main():
625+
... # Note: For WebSocket URIs, AsyncAutoProvider returns a WebSocketProvider
626+
... # which requires explicit connection. Use AsyncWeb3 context manager:
627+
... async with AsyncWeb3(AsyncAutoProvider()) as w3:
628+
... print(await w3.eth.block_number)
629+
630+
>>> asyncio.run(main())
536631
537632
.. py:currentmodule:: web3.providers.eth_tester
538633

newsfragments/3704.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introduce ``AsyncAutoProvider`` for automatic detection of async providers (AsyncHTTP, WebSocket, AsyncIPC) and ``load_async_provider_from_uri`` / ``load_async_provider_from_environment`` helpers.

tests/core/providers/test_auto_provider.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import pytest
22
import os
33

4+
from web3.exceptions import (
5+
Web3ValidationError,
6+
)
47
from web3.providers import (
8+
AsyncAutoProvider,
9+
AsyncHTTPProvider,
510
HTTPProvider,
611
IPCProvider,
12+
WebSocketProvider,
713
)
814
from web3.providers.auto import (
15+
load_async_provider_from_environment,
16+
load_async_provider_from_uri,
917
load_provider_from_environment,
18+
load_provider_from_uri,
1019
)
1120
from web3.providers.ipc import (
1221
get_dev_ipc_path,
@@ -65,3 +74,102 @@ def test_get_dev_ipc_path(monkeypatch, tmp_path):
6574
monkeypatch.setenv("WEB3_PROVIDER_URI", uri)
6675
path = get_dev_ipc_path()
6776
assert path == uri
77+
78+
79+
@pytest.mark.parametrize(
80+
"ws_uri",
81+
(
82+
"ws://localhost:8546",
83+
"wss://mainnet.infura.io/ws/v3/YOUR_KEY",
84+
),
85+
)
86+
def test_load_provider_from_uri_raises_for_websocket(ws_uri):
87+
"""
88+
Test that load_provider_from_uri raises Web3ValidationError for ws/wss URIs.
89+
90+
WebSocket requires async provider, so sync load_provider_from_uri should fail.
91+
"""
92+
with pytest.raises(Web3ValidationError) as exc_info:
93+
load_provider_from_uri(ws_uri)
94+
95+
assert "WebSocket URI" in str(exc_info.value)
96+
assert "AsyncAutoProvider" in str(exc_info.value)
97+
98+
99+
def test_load_provider_from_env_raises_for_websocket(monkeypatch):
100+
"""
101+
Test that load_provider_from_environment raises for WebSocket URIs.
102+
"""
103+
monkeypatch.setenv("WEB3_PROVIDER_URI", "ws://localhost:8546")
104+
with pytest.raises(Web3ValidationError):
105+
load_provider_from_environment()
106+
107+
108+
@pytest.mark.parametrize(
109+
"uri, expected_type",
110+
(
111+
("http://1.2.3.4:5678", AsyncHTTPProvider),
112+
("https://node.ontheweb.com", AsyncHTTPProvider),
113+
("ws://localhost:8546", WebSocketProvider),
114+
("wss://mainnet.infura.io/ws/v3/KEY", WebSocketProvider),
115+
),
116+
)
117+
def test_load_async_provider_from_uri(uri, expected_type):
118+
"""
119+
Test that load_async_provider_from_uri correctly identifies provider types.
120+
"""
121+
provider = load_async_provider_from_uri(uri)
122+
assert isinstance(provider, expected_type)
123+
124+
125+
@pytest.mark.parametrize(
126+
"uri, expected_type, env_var",
127+
(
128+
("http://1.2.3.4:5678", AsyncHTTPProvider, "WEB3_PROVIDER_URI"),
129+
("ws://localhost:8546", WebSocketProvider, "WEB3_PROVIDER_URI"),
130+
("wss://localhost:8546", WebSocketProvider, "WEB3_WS_PROVIDER_URI"),
131+
),
132+
)
133+
def test_load_async_provider_from_env(monkeypatch, uri, expected_type, env_var):
134+
"""
135+
Test that load_async_provider_from_environment correctly loads async providers.
136+
"""
137+
monkeypatch.setenv(env_var, uri)
138+
provider = load_async_provider_from_environment()
139+
assert isinstance(provider, expected_type)
140+
141+
142+
def test_load_async_provider_from_env_returns_none(monkeypatch):
143+
"""
144+
Test that load_async_provider_from_environment returns None when no env vars set.
145+
"""
146+
monkeypatch.delenv("WEB3_PROVIDER_URI", raising=False)
147+
monkeypatch.delenv("WEB3_WS_PROVIDER_URI", raising=False)
148+
provider = load_async_provider_from_environment()
149+
assert provider is None
150+
151+
152+
def test_async_auto_provider_default_providers():
153+
"""
154+
Test that AsyncAutoProvider has correct default providers.
155+
"""
156+
provider = AsyncAutoProvider()
157+
assert provider._potential_providers is not None
158+
assert len(provider._potential_providers) > 0
159+
160+
161+
def test_async_auto_provider_custom_providers():
162+
"""
163+
Test that AsyncAutoProvider accepts custom providers.
164+
"""
165+
custom_providers = (AsyncHTTPProvider,)
166+
provider = AsyncAutoProvider(potential_providers=custom_providers)
167+
assert provider._potential_providers == custom_providers
168+
169+
170+
def test_async_auto_provider_is_async():
171+
"""
172+
Test that AsyncAutoProvider is marked as async.
173+
"""
174+
provider = AsyncAutoProvider()
175+
assert provider.is_async is True

web3/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Web3,
1111
)
1212
from web3.providers import (
13+
AsyncAutoProvider,
1314
AsyncBaseProvider,
1415
AutoProvider,
1516
BaseProvider,
@@ -41,6 +42,7 @@
4142
"AsyncWeb3",
4243
"Web3",
4344
# providers:
45+
"AsyncAutoProvider",
4446
"AsyncBaseProvider",
4547
"AsyncEthereumTesterProvider",
4648
"AsyncHTTPProvider",

web3/providers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
WebSocketProvider,
2626
)
2727
from .auto import (
28+
AsyncAutoProvider,
2829
AutoProvider,
2930
)
3031

3132
__all__ = [
33+
"AsyncAutoProvider",
3234
"AsyncBaseProvider",
3335
"AsyncEthereumTesterProvider",
3436
"AsyncHTTPProvider",

0 commit comments

Comments
 (0)