web3 7.12.1__py3-none-any.whl → 7.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. ens/async_ens.py +9 -5
  2. ens/base_ens.py +1 -1
  3. ens/utils.py +2 -1
  4. web3/_utils/abi.py +5 -5
  5. web3/_utils/async_transactions.py +12 -9
  6. web3/_utils/batching.py +1 -1
  7. web3/_utils/caching/caching_utils.py +2 -2
  8. web3/_utils/contracts.py +5 -5
  9. web3/_utils/ens.py +1 -1
  10. web3/_utils/events.py +1 -1
  11. web3/_utils/module_testing/eth_module.py +133 -125
  12. web3/_utils/module_testing/go_ethereum_admin_module.py +7 -6
  13. web3/_utils/module_testing/go_ethereum_debug_module.py +5 -4
  14. web3/_utils/module_testing/go_ethereum_txpool_module.py +6 -3
  15. web3/_utils/module_testing/module_testing_utils.py +1 -1
  16. web3/_utils/module_testing/net_module.py +4 -3
  17. web3/_utils/module_testing/persistent_connection_provider.py +132 -20
  18. web3/_utils/module_testing/utils.py +7 -6
  19. web3/_utils/module_testing/web3_module.py +15 -11
  20. web3/_utils/normalizers.py +3 -3
  21. web3/_utils/transactions.py +2 -1
  22. web3/contract/async_contract.py +13 -13
  23. web3/contract/base_contract.py +11 -12
  24. web3/contract/utils.py +7 -7
  25. web3/eth/async_eth.py +3 -14
  26. web3/exceptions.py +8 -1
  27. web3/gas_strategies/time_based.py +1 -1
  28. web3/main.py +24 -11
  29. web3/manager.py +11 -13
  30. web3/middleware/__init__.py +1 -1
  31. web3/middleware/base.py +5 -5
  32. web3/middleware/buffered_gas_estimate.py +1 -1
  33. web3/middleware/filter.py +10 -9
  34. web3/middleware/formatting.py +11 -5
  35. web3/middleware/gas_price_strategy.py +1 -1
  36. web3/middleware/names.py +4 -4
  37. web3/middleware/signing.py +3 -3
  38. web3/middleware/stalecheck.py +2 -2
  39. web3/middleware/validation.py +2 -2
  40. web3/module.py +3 -3
  41. web3/providers/async_base.py +2 -2
  42. web3/providers/base.py +2 -2
  43. web3/providers/eth_tester/defaults.py +2 -2
  44. web3/providers/eth_tester/main.py +1 -1
  45. web3/providers/eth_tester/middleware.py +2 -2
  46. web3/providers/persistent/persistent.py +8 -7
  47. web3/providers/persistent/persistent_connection.py +1 -1
  48. web3/providers/persistent/subscription_manager.py +67 -14
  49. web3/providers/persistent/utils.py +1 -1
  50. web3/types.py +16 -3
  51. web3/utils/subscriptions.py +26 -4
  52. {web3-7.12.1.dist-info → web3-7.14.0.dist-info}/METADATA +1 -1
  53. {web3-7.12.1.dist-info → web3-7.14.0.dist-info}/RECORD +56 -57
  54. ens/specs/.DS_Store +0 -0
  55. {web3-7.12.1.dist-info → web3-7.14.0.dist-info}/WHEEL +0 -0
  56. {web3-7.12.1.dist-info → web3-7.14.0.dist-info}/licenses/LICENSE +0 -0
  57. {web3-7.12.1.dist-info → web3-7.14.0.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from eth_utils.toolz import (
16
16
  )
17
17
 
18
18
  from web3.exceptions import (
19
+ BadResponseFormat,
19
20
  Web3ValueError,
20
21
  )
21
22
  from web3.middleware.base import (
@@ -77,7 +78,12 @@ def _apply_response_formatters(
77
78
  response, response_type, method_response_formatter(appropriate_response)
78
79
  )
79
80
 
80
- if response.get("result") is not None and method in result_formatters:
81
+ if not isinstance(response, dict):
82
+ raise BadResponseFormat(
83
+ "Malformed response: expected a valid JSON-RPC response object, got: "
84
+ "`{}`".format(response)
85
+ )
86
+ elif response.get("result") is not None and method in result_formatters:
81
87
  return _format_response("result", result_formatters[method])
82
88
  elif (
83
89
  # eth_subscription responses
@@ -94,7 +100,7 @@ def _apply_response_formatters(
94
100
 
95
101
  SYNC_FORMATTERS_BUILDER = Callable[["Web3", RPCEndpoint], FormattersDict]
96
102
  ASYNC_FORMATTERS_BUILDER = Callable[
97
- ["AsyncWeb3", RPCEndpoint], Coroutine[Any, Any, FormattersDict]
103
+ ["AsyncWeb3[Any]", RPCEndpoint], Coroutine[Any, Any, FormattersDict]
98
104
  ]
99
105
 
100
106
 
@@ -108,7 +114,7 @@ class FormattingMiddlewareBuilder(Web3MiddlewareBuilder):
108
114
  @staticmethod
109
115
  @curry
110
116
  def build(
111
- w3: Union["AsyncWeb3", "Web3"],
117
+ w3: Union["Web3", "AsyncWeb3[Any]"],
112
118
  # formatters option:
113
119
  request_formatters: Optional[Formatters] = None,
114
120
  result_formatters: Optional[Formatters] = None,
@@ -180,7 +186,7 @@ class FormattingMiddlewareBuilder(Web3MiddlewareBuilder):
180
186
  formatters = merge(
181
187
  FORMATTER_DEFAULTS,
182
188
  await self.async_formatters_builder(
183
- cast("AsyncWeb3", self._w3), method
189
+ cast("AsyncWeb3[Any]", self._w3), method
184
190
  ),
185
191
  )
186
192
  self.request_formatters = formatters.pop("request_formatters")
@@ -198,7 +204,7 @@ class FormattingMiddlewareBuilder(Web3MiddlewareBuilder):
198
204
  formatters = merge(
199
205
  FORMATTER_DEFAULTS,
200
206
  await self.async_formatters_builder(
201
- cast("AsyncWeb3", self._w3), method
207
+ cast("AsyncWeb3[Any]", self._w3), method
202
208
  ),
203
209
  )
204
210
  self.result_formatters = formatters["result_formatters"]
@@ -106,7 +106,7 @@ class GasPriceStrategyMiddleware(Web3Middleware):
106
106
  async def async_request_processor(self, method: RPCEndpoint, params: Any) -> Any:
107
107
  if method == "eth_sendTransaction":
108
108
  transaction = params[0]
109
- w3 = cast("AsyncWeb3", self._w3)
109
+ w3 = cast("AsyncWeb3[Any]", self._w3)
110
110
  generated_gas_price = w3.eth.generate_gas_price(transaction)
111
111
  latest_block = await w3.eth.get_block("latest")
112
112
  transaction = validate_transaction_params(
web3/middleware/names.py CHANGED
@@ -53,7 +53,7 @@ def _is_logs_subscription_with_optional_args(method: RPCEndpoint, params: Any) -
53
53
 
54
54
 
55
55
  async def async_format_all_ens_names_to_address(
56
- async_web3: "AsyncWeb3",
56
+ async_web3: "AsyncWeb3[Any]",
57
57
  abi_types_for_method: Sequence[Any],
58
58
  data: Sequence[Any],
59
59
  ) -> Sequence[Any]:
@@ -69,7 +69,7 @@ async def async_format_all_ens_names_to_address(
69
69
 
70
70
 
71
71
  async def async_apply_ens_to_address_conversion(
72
- async_web3: "AsyncWeb3",
72
+ async_web3: "AsyncWeb3[Any]",
73
73
  params: Any,
74
74
  abi_types_for_method: Union[Sequence[str], Dict[str, str]],
75
75
  ) -> Any:
@@ -125,7 +125,7 @@ class ENSNameToAddressMiddleware(Web3Middleware):
125
125
  # eth_subscribe optional logs params are unique.
126
126
  # Handle them separately here.
127
127
  (formatted_dict,) = await async_apply_ens_to_address_conversion(
128
- cast("AsyncWeb3", self._w3),
128
+ cast("AsyncWeb3[Any]", self._w3),
129
129
  (params[1],),
130
130
  {
131
131
  "address": "address",
@@ -136,7 +136,7 @@ class ENSNameToAddressMiddleware(Web3Middleware):
136
136
 
137
137
  else:
138
138
  params = await async_apply_ens_to_address_conversion(
139
- cast("AsyncWeb3", self._w3),
139
+ cast("AsyncWeb3[Any]", self._w3),
140
140
  params,
141
141
  abi_types_for_method,
142
142
  )
@@ -93,7 +93,7 @@ _PrivateKey = Union[LocalAccount, PrivateKey, HexStr, bytes]
93
93
 
94
94
  @to_dict
95
95
  def gen_normalized_accounts(
96
- val: Union[_PrivateKey, Collection[_PrivateKey]]
96
+ val: Union[_PrivateKey, Collection[_PrivateKey]],
97
97
  ) -> Iterable[Tuple[ChecksumAddress, LocalAccount]]:
98
98
  if isinstance(
99
99
  val,
@@ -158,7 +158,7 @@ class SignAndSendRawMiddlewareBuilder(Web3MiddlewareBuilder):
158
158
  @curry
159
159
  def build(
160
160
  private_key_or_account: Union[_PrivateKey, Collection[_PrivateKey]],
161
- w3: Union["Web3", "AsyncWeb3"],
161
+ w3: Union["Web3", "AsyncWeb3[Any]"],
162
162
  ) -> "SignAndSendRawMiddlewareBuilder":
163
163
  middleware = SignAndSendRawMiddlewareBuilder(w3)
164
164
  middleware._accounts = gen_normalized_accounts(private_key_or_account)
@@ -199,7 +199,7 @@ class SignAndSendRawMiddlewareBuilder(Web3MiddlewareBuilder):
199
199
  return method, params
200
200
 
201
201
  else:
202
- w3 = cast("AsyncWeb3", self._w3)
202
+ w3 = cast("AsyncWeb3[Any]", self._w3)
203
203
 
204
204
  formatted_transaction = format_transaction(params[0])
205
205
  filled_transaction = await async_fill_transaction_defaults(
@@ -51,7 +51,7 @@ class StalecheckMiddlewareBuilder(Web3MiddlewareBuilder):
51
51
  @curry
52
52
  def build(
53
53
  allowable_delay: int,
54
- w3: Union["Web3", "AsyncWeb3"],
54
+ w3: Union["Web3", "AsyncWeb3[Any]"],
55
55
  skip_stalecheck_for_methods: Collection[str] = SKIP_STALECHECK_FOR_METHODS,
56
56
  ) -> Web3Middleware:
57
57
  if allowable_delay <= 0:
@@ -82,7 +82,7 @@ class StalecheckMiddlewareBuilder(Web3MiddlewareBuilder):
82
82
  async def async_request_processor(self, method: "RPCEndpoint", params: Any) -> Any:
83
83
  if method not in self.skip_stalecheck_for_methods:
84
84
  if not _is_fresh(self.cache["latest"], self.allowable_delay):
85
- w3 = cast("AsyncWeb3", self._w3)
85
+ w3 = cast("AsyncWeb3[Any]", self._w3)
86
86
  latest = await w3.eth.get_block("latest")
87
87
 
88
88
  if _is_fresh(latest, self.allowable_delay):
@@ -121,7 +121,7 @@ def _chain_id_validator(web3_chain_id: int) -> Callable[..., Any]:
121
121
 
122
122
 
123
123
  def _build_formatters_dict(
124
- request_formatters: Dict[RPCEndpoint, Any]
124
+ request_formatters: Dict[RPCEndpoint, Any],
125
125
  ) -> FormattersDict:
126
126
  return dict(
127
127
  request_formatters=request_formatters,
@@ -149,7 +149,7 @@ def build_method_validators(w3: "Web3", method: RPCEndpoint) -> FormattersDict:
149
149
 
150
150
 
151
151
  async def async_build_method_validators(
152
- async_w3: "AsyncWeb3", method: RPCEndpoint
152
+ async_w3: "AsyncWeb3[Any]", method: RPCEndpoint
153
153
  ) -> FormattersDict:
154
154
  request_formatters: Formatters = {}
155
155
  if RPCEndpoint(method) in METHODS_TO_VALIDATE:
web3/module.py CHANGED
@@ -60,7 +60,7 @@ TReturn = TypeVar("TReturn")
60
60
 
61
61
  @curry
62
62
  def retrieve_request_information_for_batching(
63
- w3: Union["AsyncWeb3", "Web3"],
63
+ w3: Union["AsyncWeb3[Any]", "Web3"],
64
64
  module: "Module",
65
65
  method: Method[Callable[..., Any]],
66
66
  ) -> Union[
@@ -119,7 +119,7 @@ def retrieve_blocking_method_call_fn(
119
119
 
120
120
  @curry
121
121
  def retrieve_async_method_call_fn(
122
- async_w3: "AsyncWeb3",
122
+ async_w3: "AsyncWeb3[Any]",
123
123
  module: "Module",
124
124
  method: Method[Callable[..., Any]],
125
125
  ) -> Callable[
@@ -167,7 +167,7 @@ def retrieve_async_method_call_fn(
167
167
  class Module:
168
168
  is_async = False
169
169
 
170
- def __init__(self, w3: Union["AsyncWeb3", "Web3"]) -> None:
170
+ def __init__(self, w3: Union["AsyncWeb3[Any]", "Web3"]) -> None:
171
171
  if self.is_async:
172
172
  self.retrieve_caller_fn = retrieve_async_method_call_fn(w3, self)
173
173
  else:
@@ -112,7 +112,7 @@ class AsyncBaseProvider:
112
112
  return self._batching_context.get() is not None
113
113
 
114
114
  async def request_func(
115
- self, async_w3: "AsyncWeb3", middleware_onion: MiddlewareOnion
115
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: MiddlewareOnion
116
116
  ) -> Callable[..., Coroutine[Any, Any, RPCResponse]]:
117
117
  middleware: Tuple[Middleware, ...] = middleware_onion.as_tuple_of_middleware()
118
118
 
@@ -129,7 +129,7 @@ class AsyncBaseProvider:
129
129
  return self._request_func_cache[-1]
130
130
 
131
131
  async def batch_request_func(
132
- self, async_w3: "AsyncWeb3", middleware_onion: MiddlewareOnion
132
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: MiddlewareOnion
133
133
  ) -> Callable[..., Coroutine[Any, Any, Union[List[RPCResponse], RPCResponse]]]:
134
134
  middleware: Tuple[Middleware, ...] = middleware_onion.as_tuple_of_middleware()
135
135
 
web3/providers/base.py CHANGED
@@ -192,9 +192,9 @@ class JSONBaseProvider(BaseProvider):
192
192
  # type ignore bc in order to wrap the method, we have to call
193
193
  # `wrap_make_batch_request` with the accumulator_fn as the argument
194
194
  # which breaks the type hinting for this particular case.
195
- accumulator_fn = initialized.wrap_make_batch_request(
195
+ accumulator_fn = initialized.wrap_make_batch_request( # type: ignore
196
196
  accumulator_fn
197
- ) # type: ignore # noqa: E501
197
+ )
198
198
  self._batch_request_func_cache = (middleware, accumulator_fn)
199
199
 
200
200
  return self._batch_request_func_cache[-1]
@@ -116,7 +116,7 @@ def call_eth_tester(
116
116
 
117
117
 
118
118
  def without_eth_tester(
119
- fn: Callable[[TParams], TReturn]
119
+ fn: Callable[[TParams], TReturn],
120
120
  ) -> Callable[["EthereumTester", TParams], TReturn]:
121
121
  # workaround for: https://github.com/pytoolz/cytoolz/issues/103
122
122
  # @functools.wraps(fn)
@@ -127,7 +127,7 @@ def without_eth_tester(
127
127
 
128
128
 
129
129
  def without_params(
130
- fn: Callable[[TParams], TReturn]
130
+ fn: Callable[[TParams], TReturn],
131
131
  ) -> Callable[["EthereumTester", TParams], TReturn]:
132
132
  # workaround for: https://github.com/pytoolz/cytoolz/issues/103
133
133
  # @functools.wraps(fn)
@@ -82,7 +82,7 @@ class AsyncEthereumTesterProvider(AsyncBaseProvider):
82
82
  self.api_endpoints = API_ENDPOINTS
83
83
 
84
84
  async def request_func(
85
- self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
85
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: "MiddlewareOnion"
86
86
  ) -> Callable[..., Coroutine[Any, Any, RPCResponse]]:
87
87
  # override the request_func to add the ethereum_tester_middleware
88
88
 
@@ -376,7 +376,7 @@ def fill_default(
376
376
 
377
377
 
378
378
  async def async_guess_from(
379
- async_w3: "AsyncWeb3", _: TxParams
379
+ async_w3: "AsyncWeb3[Any]", _: TxParams
380
380
  ) -> Optional[ChecksumAddress]:
381
381
  accounts = await async_w3.eth.accounts
382
382
  if accounts is not None and len(accounts) > 0:
@@ -388,7 +388,7 @@ async def async_guess_from(
388
388
  async def async_fill_default(
389
389
  field: str,
390
390
  guess_func: Callable[..., Any],
391
- async_w3: "AsyncWeb3",
391
+ async_w3: "AsyncWeb3[Any]",
392
392
  transaction: TxParams,
393
393
  ) -> TxParams:
394
394
  # type ignored b/c TxParams keys must be string literal types
@@ -106,7 +106,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
106
106
  # -- cached middleware request/response functions -- #
107
107
 
108
108
  async def send_func(
109
- self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
109
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: "MiddlewareOnion"
110
110
  ) -> Callable[..., Coroutine[Any, Any, RPCRequest]]:
111
111
  """
112
112
  Cache the middleware chain for `send`.
@@ -130,7 +130,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
130
130
  return self._send_func_cache[1]
131
131
 
132
132
  async def recv_func(
133
- self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
133
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: "MiddlewareOnion"
134
134
  ) -> Any:
135
135
  """
136
136
  Cache and compose the middleware stack for `recv`.
@@ -156,7 +156,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
156
156
  return self._recv_func_cache[1]
157
157
 
158
158
  async def send_batch_func(
159
- self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
159
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: "MiddlewareOnion"
160
160
  ) -> Callable[..., Coroutine[Any, Any, List[RPCRequest]]]:
161
161
  middleware = middleware_onion.as_tuple_of_middleware()
162
162
  cache_key = hash(tuple(id(mw) for mw in middleware))
@@ -164,7 +164,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
164
164
  if cache_key != self._send_batch_func_cache[0]:
165
165
 
166
166
  async def send_func(
167
- requests: List[Tuple[RPCEndpoint, Any]]
167
+ requests: List[Tuple[RPCEndpoint, Any]],
168
168
  ) -> List[RPCRequest]:
169
169
  for mw in middleware:
170
170
  initialized = mw(async_w3)
@@ -179,7 +179,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
179
179
  return self._send_batch_func_cache[1]
180
180
 
181
181
  async def recv_batch_func(
182
- self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
182
+ self, async_w3: "AsyncWeb3[Any]", middleware_onion: "MiddlewareOnion"
183
183
  ) -> Callable[..., Coroutine[Any, Any, List[RPCResponse]]]:
184
184
  middleware = middleware_onion.as_tuple_of_middleware()
185
185
  cache_key = hash(tuple(id(mw) for mw in middleware))
@@ -376,11 +376,12 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
376
376
  ) -> None:
377
377
  # Puts a `TaskNotRunning` in appropriate queues to signal the end of the
378
378
  # listener task to any listeners relying on the queues.
379
+ message = "Message listener task has ended."
379
380
  self._request_processor._subscription_response_queue.put_nowait(
380
- TaskNotRunning(message_listener_task)
381
+ TaskNotRunning(message_listener_task, message=message)
381
382
  )
382
383
  self._request_processor._handler_subscription_queue.put_nowait(
383
- TaskNotRunning(message_listener_task)
384
+ TaskNotRunning(message_listener_task, message=message)
384
385
  )
385
386
 
386
387
  def _raise_stray_errors_from_cache(self) -> None:
@@ -30,7 +30,7 @@ class PersistentConnection:
30
30
  via a `AsyncWeb3` instance instantiated with a `PersistentConnectionProvider` class.
31
31
  """
32
32
 
33
- def __init__(self, w3: "AsyncWeb3"):
33
+ def __init__(self, w3: "AsyncWeb3[Any]"):
34
34
  self._manager = w3.manager
35
35
  self.provider = cast("PersistentConnectionProvider", self._manager.provider)
36
36
 
@@ -5,6 +5,7 @@ from typing import (
5
5
  Any,
6
6
  List,
7
7
  Sequence,
8
+ Set,
8
9
  Union,
9
10
  cast,
10
11
  overload,
@@ -15,6 +16,7 @@ from eth_typing import (
15
16
  )
16
17
 
17
18
  from web3.exceptions import (
19
+ SubscriptionHandlerTaskException,
18
20
  SubscriptionProcessingFinished,
19
21
  TaskNotRunning,
20
22
  Web3TypeError,
@@ -50,19 +52,26 @@ class SubscriptionManager:
50
52
  logger: logging.Logger = logging.getLogger(
51
53
  "web3.providers.persistent.subscription_manager"
52
54
  )
53
- total_handler_calls: int = 0
54
55
 
55
- def __init__(self, w3: "AsyncWeb3") -> None:
56
+ def __init__(self, w3: "AsyncWeb3[Any]") -> None:
56
57
  self._w3 = w3
57
58
  self._provider = cast("PersistentConnectionProvider", w3.provider)
58
59
  self._subscription_container = SubscriptionContainer()
59
60
 
61
+ # parallelize all subscription handler calls
62
+ self.parallelize = False
63
+ self.task_timeout = 1
64
+ # TODO: can remove quotes from type hints once Python 3.8 support is dropped
65
+ self._tasks: Set["asyncio.Task[None]"] = set()
66
+
60
67
  # share the subscription container with the request processor so it can separate
61
68
  # subscriptions into different queues based on ``sub._handler`` presence
62
69
  self._provider._request_processor._subscription_container = (
63
70
  self._subscription_container
64
71
  )
65
72
 
73
+ self.total_handler_calls: int = 0
74
+
66
75
  def _add_subscription(self, subscription: EthSubscription[Any]) -> None:
67
76
  self._subscription_container.add_subscription(subscription)
68
77
 
@@ -86,6 +95,35 @@ class SubscriptionManager:
86
95
  f"labels.\n label: {subscription._label}"
87
96
  )
88
97
 
98
+ # TODO: can remove quotes from type hints once Python 3.8 support is dropped
99
+ def _handler_task_callback(self, task: "asyncio.Task[None]") -> None:
100
+ """
101
+ Callback when a handler task completes. Similar to _message_listener_callback.
102
+ Puts handler exceptions into the queue to be raised in the main loop, else
103
+ removes the task from the set of active tasks.
104
+ """
105
+ if task.done() and not task.cancelled():
106
+ try:
107
+ task.result()
108
+ self._tasks.discard(task)
109
+ except Exception as e:
110
+ self.logger.exception("Subscription handler task raised an exception.")
111
+ self._provider._request_processor._handler_subscription_queue.put_nowait( # noqa: E501
112
+ SubscriptionHandlerTaskException(task, message=str(e))
113
+ )
114
+
115
+ async def _cleanup_remaining_tasks(self) -> None:
116
+ """Cancel and clean up all remaining tasks."""
117
+ if not self._tasks:
118
+ return
119
+
120
+ self.logger.debug("Cleaning up %d remaining tasks...", len(self._tasks))
121
+ for task in self._tasks:
122
+ if not task.done():
123
+ task.cancel()
124
+
125
+ self._tasks.clear()
126
+
89
127
  @property
90
128
  def subscriptions(self) -> List[EthSubscription[Any]]:
91
129
  return self._subscription_container.subscriptions
@@ -281,14 +319,23 @@ class SubscriptionManager:
281
319
  sub_id
282
320
  )
283
321
  if sub:
284
- await sub._handler(
285
- EthSubscriptionContext(
286
- self._w3,
287
- sub,
288
- formatted_sub_response["result"],
289
- **sub._handler_context,
290
- )
322
+ sub_context = EthSubscriptionContext(
323
+ self._w3,
324
+ sub,
325
+ formatted_sub_response["result"],
326
+ **sub._handler_context,
291
327
  )
328
+ if sub.parallelize is True or (
329
+ sub.parallelize is None and self.parallelize
330
+ ):
331
+ # run the handler in a task to allow parallel processing
332
+ task = asyncio.create_task(sub._handler(sub_context))
333
+ self._tasks.add(task)
334
+ task.add_done_callback(self._handler_task_callback)
335
+ else:
336
+ # await the handler in the main loop to ensure order
337
+ await sub._handler(sub_context)
338
+
292
339
  except SubscriptionProcessingFinished:
293
340
  if not run_forever:
294
341
  self.logger.info(
@@ -296,14 +343,20 @@ class SubscriptionManager:
296
343
  "Stopping subscription handling."
297
344
  )
298
345
  break
299
- except TaskNotRunning:
300
- await asyncio.sleep(0)
301
- self._provider._handle_listener_task_exceptions()
346
+ except SubscriptionHandlerTaskException:
302
347
  self.logger.error(
303
- "Message listener background task for the provider has stopped "
304
- "unexpectedly. Stopping subscription handling."
348
+ "An exception occurred in a subscription handler task. "
349
+ "Stopping subscription handling."
305
350
  )
351
+ await self._cleanup_remaining_tasks()
352
+ raise
353
+ except TaskNotRunning as e:
354
+ self.logger.error("Stopping subscription handling: %s", e.message)
355
+ self._provider._handle_listener_task_exceptions()
306
356
  break
307
357
 
308
358
  # no active handler subscriptions, clear the handler subscription queue
309
359
  self._provider._request_processor._reset_handler_subscription_queue()
360
+
361
+ if self._tasks:
362
+ await self._cleanup_remaining_tasks()
@@ -26,7 +26,7 @@ def persistent_connection_provider_method(message: str = None) -> Callable[...,
26
26
 
27
27
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
28
28
  @functools.wraps(func)
29
- def inner(self: "AsyncWeb3", *args: Any, **kwargs: Any) -> Any:
29
+ def inner(self: "AsyncWeb3[Any]", *args: Any, **kwargs: Any) -> Any:
30
30
  nonlocal message
31
31
  if message is None:
32
32
  message = (
web3/types.py CHANGED
@@ -62,6 +62,19 @@ ABIElementIdentifier = Union[str, Type[FallbackFn], Type[ReceiveFn]]
62
62
 
63
63
  # bytes, hexbytes, or hexstr representing a 32 byte hash
64
64
  _Hash32 = Union[Hash32, HexBytes, HexStr]
65
+
66
+ # --- ``TopicFilter`` type for event log filtering with AND/OR patterns --- #
67
+ # - ``None``: wildcard, matches any value at this position
68
+ # - ``_Hash32``: single topic that must match exactly
69
+ # - ``Sequence[Union[None, _Hash32]]``: OR condition, at least one must match
70
+ # - ``Sequence[Sequence[...]]``: nested OR conditions
71
+ TopicFilter = Union[
72
+ None,
73
+ _Hash32,
74
+ Sequence[Union[None, _Hash32]],
75
+ Sequence["TopicFilter"],
76
+ ]
77
+
65
78
  EnodeURI = NewType("EnodeURI", str)
66
79
  ENS = NewType("ENS", str)
67
80
  Nonce = NewType("Nonce", int)
@@ -345,7 +358,7 @@ class FilterParams(TypedDict, total=False):
345
358
  blockHash: HexBytes
346
359
  fromBlock: BlockIdentifier
347
360
  toBlock: BlockIdentifier
348
- topics: Sequence[Optional[Union[_Hash32, Sequence[_Hash32]]]]
361
+ topics: Sequence[TopicFilter]
349
362
 
350
363
 
351
364
  class FeeHistory(TypedDict):
@@ -367,7 +380,7 @@ StateOverride = Dict[Union[str, Address, ChecksumAddress], StateOverrideParams]
367
380
 
368
381
 
369
382
  GasPriceStrategy = Union[
370
- Callable[["Web3", TxParams], Wei], Callable[["AsyncWeb3", TxParams], Wei]
383
+ Callable[["Web3", TxParams], Wei], Callable[["AsyncWeb3[Any]", TxParams], Wei]
371
384
  ]
372
385
 
373
386
 
@@ -667,4 +680,4 @@ class LogsSubscriptionArg(TypedDict, total=False):
667
680
  ENS,
668
681
  Sequence[Union[Address, ChecksumAddress, ENS]],
669
682
  ]
670
- topics: Sequence[Union[HexStr, Sequence[HexStr]]]
683
+ topics: Sequence[TopicFilter]