From 8bc59ef6f213152bcc0884844c7a4a5331128dc7 Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 30 Oct 2025 16:57:09 -0600 Subject: [PATCH 1/5] Remove deepcopy in request_id for mocked responses --- setup.py | 3 ++- web3/_utils/module_testing/utils.py | 34 ++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 77576d3b23..36210d3ee9 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ "requests>=2.23.0", "typing-extensions>=4.0.1", "types-requests>=2.0.0", - "websockets>=10.0.0,<16.0.0", + "websockets>=14.0.0", "pyunormalize>=15.0.0", ], python_requires=">=3.10, <4", @@ -99,5 +99,6 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], ) diff --git a/web3/_utils/module_testing/utils.py b/web3/_utils/module_testing/utils.py index 37f298da92..8f9bac7908 100644 --- a/web3/_utils/module_testing/utils.py +++ b/web3/_utils/module_testing/utils.py @@ -1,7 +1,6 @@ from asyncio import ( iscoroutinefunction, ) -import copy from typing import ( TYPE_CHECKING, Any, @@ -113,12 +112,11 @@ def __init__( "AsyncMakeRequestFn", "MakeRequestFn" ] = w3.provider.make_request + self._mock_request_counter = 1 + def _build_request_id(self) -> int: - request_id = ( - next(copy.deepcopy(self.w3.provider.request_counter)) - if hasattr(self.w3.provider, "request_counter") - else 1 - ) + request_id = self._mock_request_counter + self._mock_request_counter += 1 return request_id def __enter__(self) -> "Self": @@ -144,7 +142,11 @@ def _mock_request_handler( if all( method not in mock_dict - for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses) + for mock_dict in ( + self.mock_errors, + self.mock_results, + self.mock_responses, + ) ): return self._make_request(method, params) @@ -265,7 +267,11 @@ async def _async_mock_request_handler( self._make_request = cast("AsyncMakeRequestFn", self._make_request) if all( method not in mock_dict - for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses) + for mock_dict in ( + self.mock_errors, + self.mock_results, + self.mock_responses, + ) ): return await self._make_request(method, params) mocked_result = await self._async_build_mock_result(method, params) @@ -289,7 +295,11 @@ async def _async_mock_send_handler( ) -> "RPCRequest": if all( method not in mock_dict - for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses) + for mock_dict in ( + self.mock_errors, + self.mock_results, + self.mock_responses, + ) ): return await self._send_request(method, params) else: @@ -304,7 +314,11 @@ async def _async_mock_recv_handler( request_id = rpc_request["id"] if all( method not in mock_dict - for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses) + for mock_dict in ( + self.mock_errors, + self.mock_results, + self.mock_responses, + ) ): return await self._recv_for_request(request_id) mocked_result = await self._async_build_mock_result( From 32ef9f3916a6d274db4a80333468f265095ef5d3 Mon Sep 17 00:00:00 2001 From: kclowes Date: Wed, 5 Nov 2025 12:07:31 -0700 Subject: [PATCH 2/5] Add Python 3.14 to CI --- .circleci/config.yml | 6 +++--- tox.ini | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a557b9abba..cdfb14ef46 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -221,7 +221,7 @@ workflows: - common: matrix: parameters: - python_minor_version: ["10", "11", "12", "13"] + python_minor_version: ["10", "11", "12", "13", "14"] tox_env: [ "lint", "core", @@ -234,7 +234,7 @@ workflows: - geth: matrix: parameters: - python_minor_version: ["10", "11", "12", "13"] + python_minor_version: ["10", "11", "12", "13", "14"] tox_env: [ "integration-goethereum-ipc", "integration-goethereum-ipc_async", @@ -252,7 +252,7 @@ workflows: - windows-wheel: matrix: parameters: - python_minor_version: ["10", "11", "12", "13"] + python_minor_version: ["10", "11", "12", "13", "14"] name: "py3<< matrix.python_minor_version >>-windows-wheel" diff --git a/tox.ini b/tox.ini index ff5d2b5e84..8b274a1c83 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist= - py{310,311,312,313}-{ens,core,lint,wheel} - py{310,311,312,313}-integration-{goethereum,ethtester} + py{310,311,312,313,314}-{ens,core,lint,wheel} + py{310,311,312,313,314}-integration-{goethereum,ethtester} docs benchmark windows-wheel @@ -48,8 +48,9 @@ basepython = py311: python3.11 py312: python3.12 py313: python3.13 + py314: python3.14 -[testenv:py{310,311,312,313}-lint] +[testenv:py{310,311,312,313,314}-lint] deps=pre-commit extras=dev commands= @@ -64,7 +65,7 @@ commands= python {toxinidir}/web3/tools/benchmark/main.py --num-calls 100 -[testenv:py{310,311,312,313}-wheel] +[testenv:py{310,311,312,313,314}-wheel] deps= wheel build[virtualenv] From acee1b490042bda37a8a11393a12084c8fb6fd1e Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 6 Nov 2025 14:53:52 -0700 Subject: [PATCH 3/5] Add newsfragments --- newsfragments/3779.breaking.rst | 1 + newsfragments/3779.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/3779.breaking.rst create mode 100644 newsfragments/3779.feature.rst diff --git a/newsfragments/3779.breaking.rst b/newsfragments/3779.breaking.rst new file mode 100644 index 0000000000..e982f1db8c --- /dev/null +++ b/newsfragments/3779.breaking.rst @@ -0,0 +1 @@ +Upgrade websockets requirement to >=14.0. diff --git a/newsfragments/3779.feature.rst b/newsfragments/3779.feature.rst new file mode 100644 index 0000000000..35b51feb8a --- /dev/null +++ b/newsfragments/3779.feature.rst @@ -0,0 +1 @@ +Add support for Python 3.14 From 531e450013022e4d4d90db4ec980fa056aed3813 Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 6 Nov 2025 16:16:11 -0700 Subject: [PATCH 4/5] Get rid of iscoroutinefunction deprecation warning --- tests/utils.py | 3 ++- web3/_utils/caching/caching_utils.py | 6 ++---- web3/_utils/module_testing/utils.py | 10 ++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 0ec8d97717..0937c12d46 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ import asyncio +import inspect import socket from web3._utils.threads import ( @@ -65,7 +66,7 @@ async def _async_wait_for_transaction_fixture_logic(async_w3, txn_hash, timeout= def async_partial(f, *args, **kwargs): async def f2(*args2, **kwargs2): result = f(*args, *args2, **kwargs, **kwargs2) - if asyncio.iscoroutinefunction(f): + if inspect.iscoroutinefunction(f): result = await result return result diff --git a/web3/_utils/caching/caching_utils.py b/web3/_utils/caching/caching_utils.py index 08783172a2..8692da3aee 100644 --- a/web3/_utils/caching/caching_utils.py +++ b/web3/_utils/caching/caching_utils.py @@ -1,8 +1,6 @@ -from asyncio import ( - iscoroutinefunction, -) import collections import hashlib +import inspect import threading from typing import ( TYPE_CHECKING, @@ -330,7 +328,7 @@ async def _async_should_cache_response( cache_validator = ASYNC_INTERNAL_VALIDATION_MAP[method] return ( await cache_validator(provider, params, result) - if iscoroutinefunction(cache_validator) + if inspect.iscoroutinefunction(cache_validator) else cache_validator(provider, params, result) ) return True diff --git a/web3/_utils/module_testing/utils.py b/web3/_utils/module_testing/utils.py index 8f9bac7908..ad70d402f5 100644 --- a/web3/_utils/module_testing/utils.py +++ b/web3/_utils/module_testing/utils.py @@ -1,6 +1,4 @@ -from asyncio import ( - iscoroutinefunction, -) +import inspect from typing import ( TYPE_CHECKING, Any, @@ -226,7 +224,7 @@ async def _async_build_mock_result( if callable(mock_return): mock_return = mock_return(method, params) - elif iscoroutinefunction(mock_return): + elif inspect.iscoroutinefunction(mock_return): # this is the "correct" way to mock the async make_request mock_return = await mock_return(method, params) @@ -241,7 +239,7 @@ async def _async_build_mock_result( if callable(mock_return): # handle callable to make things easier since we're mocking mock_return = mock_return(method, params) - elif iscoroutinefunction(mock_return): + elif inspect.iscoroutinefunction(mock_return): # this is the "correct" way to mock the async make_request mock_return = await mock_return(method, params) @@ -251,7 +249,7 @@ async def _async_build_mock_result( error = self.mock_errors[method] if callable(error): error = error(method, params) - elif iscoroutinefunction(error): + elif inspect.iscoroutinefunction(error): error = await error(method, params) mocked_result = merge(response_dict, self._create_error_object(error)) From 2b2033e5d4967e81f8d32e1ad67e868a23cac36e Mon Sep 17 00:00:00 2001 From: kclowes Date: Thu, 6 Nov 2025 16:58:32 -0700 Subject: [PATCH 5/5] Upgrade websockets away from legacy --- setup.py | 2 +- web3/providers/async_base.py | 6 +++--- web3/providers/persistent/websocket.py | 13 ++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 36210d3ee9..33ef5678f6 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ "towncrier>=24,<25", ], "test": [ - "pytest-asyncio>=0.18.1,<0.23", + "pytest-asyncio>=0.18.1", "pytest-mock>=1.10", "pytest-xdist>=2.4.0", "pytest>=7.0.0", diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py index 50eaa6b6d1..4b1bfa0d92 100644 --- a/web3/providers/async_base.py +++ b/web3/providers/async_base.py @@ -49,8 +49,8 @@ ) if TYPE_CHECKING: - from websockets.legacy.client import ( - WebSocketClientProtocol, + from websockets.asyncio.client import ( + ClientConnection, ) from web3 import ( # noqa: F401 @@ -169,7 +169,7 @@ async def disconnect(self) -> None: ) # WebSocket typing - _ws: "WebSocketClientProtocol" + _ws: "ClientConnection" # IPC typing _reader: asyncio.StreamReader | None diff --git a/web3/providers/persistent/websocket.py b/web3/providers/persistent/websocket.py index 4c18c8b5f7..c91a9aaed6 100644 --- a/web3/providers/persistent/websocket.py +++ b/web3/providers/persistent/websocket.py @@ -12,13 +12,16 @@ from toolz import ( merge, ) +from websockets.asyncio.client import ( + ClientConnection, + connect, +) from websockets.exceptions import ( ConnectionClosedOK, WebSocketException, ) -from websockets.legacy.client import ( - WebSocketClientProtocol, - connect, +from websockets.protocol import ( + State, ) from web3.exceptions import ( @@ -69,7 +72,7 @@ def __init__( ) super().__init__(**kwargs) self.use_text_frames = use_text_frames - self._ws: WebSocketClientProtocol | None = None + self._ws: ClientConnection | None = None if not any( self.endpoint_uri.startswith(prefix) @@ -133,7 +136,7 @@ async def _provider_specific_connect(self) -> None: async def _provider_specific_disconnect(self) -> None: # this should remain idempotent - if self._ws is not None and not self._ws.closed: + if self._ws is not None and self._ws.state == State.OPEN: await self._ws.close() self._ws = None