From 33ea2abbdff9c48d171cc4184179ae8b9f8b81b4 Mon Sep 17 00:00:00 2001 From: Mahmoud Harmouch Date: Mon, 14 Feb 2022 20:05:02 +0200 Subject: [PATCH] :heavy_plus_sign: Add Python 3.10 support (#2175) * :heavy_plus_sign: Add Python 3.10 support to CI * Dropped support for all parities * Change docker image to use 3.10 * Update pytest-asyncio plugin * Mark async fixture as such, clean up pytest DeprecationWarnings Signed-off-by: Harmouch101 Co-authored-by: Felipe Selmo Co-authored-by: kclowes Signed-off-by: Harmouch101 --- .circleci/config.yml | 75 ++++++++++++++++++- newsfragments/2175.feature.rst | 1 + newsfragments/2344.feature.rst | 1 + pytest.ini | 1 + setup.py | 9 ++- .../core/providers/test_websocket_provider.py | 34 +++++++-- tests/integration/conftest.py | 2 +- tests/integration/go_ethereum/conftest.py | 2 +- .../go_ethereum/test_goethereum_http.py | 4 +- tests/utils.py | 6 +- tox.ini | 17 +++-- web3/_utils/decorators.py | 19 +++++ web3/_utils/module_testing/web3_module.py | 2 +- web3/providers/websocket.py | 53 ++++++------- 14 files changed, 179 insertions(+), 47 deletions(-) create mode 100644 newsfragments/2175.feature.rst create mode 100644 newsfragments/2344.feature.rst diff --git a/.circleci/config.yml b/.circleci/config.yml index c76ed67ab7..08c9b05884 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -421,10 +421,75 @@ jobs: environment: TOXENV: py39-wheel-cli + # + # Python 3.10 + # + py310-core: + <<: *common + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-core + + py310-ens: + <<: *common + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-ens + + py310-ethpm: + <<: *ethpm_steps + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-ethpm + # Please don't use this key for any shenanigans + WEB3_INFURA_PROJECT_ID: 7707850c2fb7465ebe6f150d67182e22 + + py310-integration-goethereum-ipc: + <<: *geth_steps + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-integration-goethereum-ipc + GETH_VERSION: v1.10.11 + + py310-integration-goethereum-http: + <<: *geth_steps + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-integration-goethereum-http + GETH_VERSION: v1.10.11 + + py310-integration-goethereum-ws: + <<: *geth_steps + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-integration-goethereum-ws + GETH_VERSION: v1.10.11 + + py310-integration-ethtester-pyevm: + <<: *common + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-integration-ethtester + ETHEREUM_TESTER_CHAIN_BACKEND: eth_tester.backends.PyEVMBackend + + py310-wheel-cli: + <<: *common + docker: + - image: circleci/python:3.10 + environment: + TOXENV: py310-wheel-cli + benchmark: <<: *geth_steps docker: - - image: circleci/python:3.9 + - image: circleci/python:3.10 environment: TOXENV: benchmark GETH_VERSION: v1.10.13 @@ -437,6 +502,7 @@ workflows: - py37-core - py38-core - py39-core + - py310-core - lint - docs - benchmark @@ -462,3 +528,10 @@ workflows: - py39-integration-goethereum-ws - py39-integration-ethtester-pyevm - py39-wheel-cli + - py310-ens + - py310-ethpm + - py310-integration-goethereum-ipc + - py310-integration-goethereum-http + - py310-integration-goethereum-ws + - py310-integration-ethtester-pyevm + - py310-wheel-cli diff --git a/newsfragments/2175.feature.rst b/newsfragments/2175.feature.rst new file mode 100644 index 0000000000..dd5d94db98 --- /dev/null +++ b/newsfragments/2175.feature.rst @@ -0,0 +1 @@ +Add support for Python 3.10 diff --git a/newsfragments/2344.feature.rst b/newsfragments/2344.feature.rst new file mode 100644 index 0000000000..8f0516a3b9 --- /dev/null +++ b/newsfragments/2344.feature.rst @@ -0,0 +1 @@ +Adds a depreaction warning for the optional ``loop`` parameter of ``web3.providers.websocket``. \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index f5fdc0ec93..f4b46a1453 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,7 @@ addopts= -v --showlocals --durations 10 python_paths= . xfail_strict=true +asyncio_mode=strict [pytest-watch] runner= pytest --failed-first --maxfail=1 --no-success-flaky-report diff --git a/setup.py b/setup.py index dc1c816c54..e4b28448d5 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ "contextlib2>=0.5.4", "py-geth>=3.6.0,<4", "py-solc>=0.4.0", - "pytest>=4.4.0,<5.0.0", + "pytest>=6.2.5,<7", "sphinx>=3.0,<4", "sphinx_rtd_theme>=0.1.9", "toposort>=1.4", @@ -38,8 +38,8 @@ "bumpversion", "flaky>=3.7.0,<4", "hypothesis>=3.31.2,<6", - "pytest>=4.4.0,<5.0.0", - "pytest-asyncio>=0.10.0,<0.11", + "pytest>=6.2.5,<7", + "pytest-asyncio>=0.18.1,<0.19", "pytest-mock>=1.10,<2", "pytest-pythonpath>=0.3", "pytest-watch>=4.2,<5", @@ -92,7 +92,7 @@ "typing-extensions>=3.7.4.1,<5;python_version<'3.8'", "websockets>=10.0.0,<11", ], - python_requires='>=3.7,<3.10', + python_requires='>=3.7,<3.11', extras_require=extras_require, py_modules=['web3', 'ens', 'ethpm'], entry_points={"pytest11": ["pytest_ethereum = web3.tools.pytest_ethereum.plugins"]}, @@ -110,5 +110,6 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], ) diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py index 562225fa1a..12c621cc13 100644 --- a/tests/core/providers/test_websocket_provider.py +++ b/tests/core/providers/test_websocket_provider.py @@ -15,6 +15,7 @@ ValidationError, ) from web3.providers.websocket import ( + PersistentWebSocket, WebsocketProvider, ) @@ -28,7 +29,7 @@ ) -@pytest.yield_fixture +@pytest.fixture def start_websocket_server(open_port): event_loop = asyncio.new_event_loop() @@ -39,7 +40,7 @@ async def empty_server(websocket, path): await websocket.send(data) asyncio.set_event_loop(event_loop) - server = websockets.serve(empty_server, '127.0.0.1', open_port) + server = websockets.serve(empty_server, "127.0.0.1", open_port) event_loop.run_until_complete(server) event_loop.run_forever() @@ -51,11 +52,11 @@ async def empty_server(websocket, path): event_loop.call_soon_threadsafe(event_loop.stop) -@pytest.fixture() +@pytest.fixture def w3(open_port, start_websocket_server): # need new event loop as the one used by server is already running event_loop = asyncio.new_event_loop() - endpoint_uri = 'ws://127.0.0.1:{}'.format(open_port) + endpoint_uri = "ws://127.0.0.1:{}".format(open_port) event_loop.run_until_complete(wait_for_ws(endpoint_uri)) provider = WebsocketProvider(endpoint_uri, websocket_timeout=0.01) return Web3(provider) @@ -67,7 +68,28 @@ def test_websocket_provider_timeout(w3): def test_restricted_websocket_kwargs(): - invalid_kwargs = {'uri': 'ws://127.0.0.1:8546'} - re_exc_message = r'.*found: {0}*'.format(set(invalid_kwargs.keys())) + invalid_kwargs = {"uri": "ws://127.0.0.1:8546"} + re_exc_message = r".*found: {0}*".format(set(invalid_kwargs.keys())) with pytest.raises(ValidationError, match=re_exc_message): WebsocketProvider(websocket_kwargs=invalid_kwargs) + + +def test_event_loop_argument_deprecated(): + event_loop = asyncio.new_event_loop() + endpoint_uri = "ws://127.0.0.1:8546" + websocket_kwargs = {} + match = ( + "The loop parameter is deprecated and was removed from websocket " + "provider as of web3 v5. Consider instantiating this class without passing this argument instead." + ) + with pytest.warns( + expected_warning=DeprecationWarning, + match=match, + ): + WebsocketProvider(endpoint_uri, websocket_timeout=0.01, loop=event_loop) + with pytest.warns( + expected_warning=DeprecationWarning, + match=match, + ): + PersistentWebSocket(endpoint_uri, websocket_kwargs, loop=event_loop) + event_loop.close() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a9ddbbd19e..dd254c6876 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -37,7 +37,7 @@ def revert_contract_factory(web3): return contract_factory -@pytest.yield_fixture(scope="module") +@pytest.fixture(scope="module") def event_loop(request): loop = asyncio.get_event_loop_policy().new_event_loop() yield loop diff --git a/tests/integration/go_ethereum/conftest.py b/tests/integration/go_ethereum/conftest.py index 7851b2e4a5..59934890c7 100644 --- a/tests/integration/go_ethereum/conftest.py +++ b/tests/integration/go_ethereum/conftest.py @@ -181,7 +181,7 @@ def unlockable_account_dual_type(unlockable_account, address_conversion_func): return address_conversion_func(unlockable_account) -@pytest.yield_fixture +@pytest.fixture def unlocked_account_dual_type(web3, unlockable_account_dual_type, unlockable_account_pw): web3.geth.personal.unlock_account(unlockable_account_dual_type, unlockable_account_pw) yield unlockable_account_dual_type diff --git a/tests/integration/go_ethereum/test_goethereum_http.py b/tests/integration/go_ethereum/test_goethereum_http.py index a94dc8b7f7..a3ea078cac 100644 --- a/tests/integration/go_ethereum/test_goethereum_http.py +++ b/tests/integration/go_ethereum/test_goethereum_http.py @@ -1,5 +1,7 @@ import pytest +import pytest_asyncio + from tests.utils import ( get_open_port, ) @@ -94,7 +96,7 @@ def web3(geth_process, endpoint_uri): return _web3 -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(scope="module") async def async_w3(geth_process, endpoint_uri): await wait_for_aiohttp(endpoint_uri) _web3 = Web3( diff --git a/tests/utils.py b/tests/utils.py index 268d33d6be..3a0eb36c2d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,14 @@ import asyncio import socket import time +import warnings import websockets def get_open_port(): sock = socket.socket() - sock.bind(('127.0.0.1', 0)) + sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] sock.close() return str(port) @@ -15,7 +16,8 @@ def get_open_port(): async def wait_for_ws(endpoint_uri, timeout=10): start = time.time() - while time.time() < start + timeout: + stop = start + timeout + while time.time() < stop: try: async with websockets.connect(uri=endpoint_uri): pass diff --git a/tox.ini b/tox.ini index f91ecfe023..ada9f14247 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] envlist= - py{37,38,39}-ens - py{37,38,39}-ethpm - py{37,38,39}-core - py{37,38,39}-integration-{goethereum,ethtester} + py{37,38,39,310}-ens + py{37,38,39,310}-ethpm + py{37,38,39,310}-core + py{37,38,39,310}-integration-{goethereum,ethtester} lint docs benchmark - py{37,38,39}-wheel-cli + py{37,38,39,310}-wheel-cli [isort] combine_as_imports=True @@ -52,6 +52,7 @@ basepython = py37: python3.7 py38: python3.8 py39: python3.9 + py310: python3.10 [testenv:lint] basepython=python @@ -97,6 +98,12 @@ whitelist_externals={[common-wheel-cli]whitelist_externals} commands={[common-wheel-cli]commands} skip_install=true +[testenv:py310-wheel-cli] +deps={[common-wheel-cli]deps} +whitelist_externals={[common-wheel-cli]whitelist_externals} +commands={[common-wheel-cli]commands} +skip_install=true + [common-wheel-cli-windows] deps=wheel whitelist_externals= diff --git a/web3/_utils/decorators.py b/web3/_utils/decorators.py index 63e14d9506..2b8bb01ae5 100644 --- a/web3/_utils/decorators.py +++ b/web3/_utils/decorators.py @@ -51,3 +51,22 @@ def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]: return to_wrap(*args, **kwargs) return cast(TFunc, wrapper) return decorator + + +class DeprecationMetaClass(type): + """ A custom class that intercepts the __call__ method to decide whether + or not to raize a warning against the loop argument. + """ + def __call__(cls, *args, **kwargs): + new_kwargs = {key: val for key, val in kwargs.items() if key != 'loop'} + if 'loop' in kwargs: + warnings.warn( + "The loop parameter is deprecated and was removed from " + "websocket provider as of web3 v5. Consider instantiating " + "this class without passing this argument instead.", + category=DeprecationWarning, + stacklevel=2, + ) + obj = cls.__new__(cls, *args, **new_kwargs) + obj.__init__(*args, **new_kwargs) + return obj diff --git a/web3/_utils/module_testing/web3_module.py b/web3/_utils/module_testing/web3_module.py index a8eb73f332..17e25e1ff7 100644 --- a/web3/_utils/module_testing/web3_module.py +++ b/web3/_utils/module_testing/web3_module.py @@ -179,7 +179,7 @@ def test_solidityKeccak( self, web3: "Web3", types: Sequence[TypeStr], values: Sequence[Any], expected: HexBytes ) -> None: if isinstance(expected, type) and issubclass(expected, Exception): - with pytest.raises(expected): + with pytest.raises(expected): # type: ignore web3.solidityKeccak(types, values) return diff --git a/web3/providers/websocket.py b/web3/providers/websocket.py index e999d56d3f..c35f01d7a7 100644 --- a/web3/providers/websocket.py +++ b/web3/providers/websocket.py @@ -25,6 +25,9 @@ WebSocketClientProtocol, ) +from web3._utils.decorators import ( + DeprecationMetaClass, +) from web3.exceptions import ( ValidationError, ) @@ -36,7 +39,7 @@ RPCResponse, ) -RESTRICTED_WEBSOCKET_KWARGS = {'uri', 'loop'} +RESTRICTED_WEBSOCKET_KWARGS = {"uri", "loop"} DEFAULT_WEBSOCKET_TIMEOUT = 10 @@ -54,13 +57,14 @@ def _get_threaded_loop() -> asyncio.AbstractEventLoop: def get_default_endpoint() -> URI: - return URI(os.environ.get('WEB3_WS_PROVIDER_URI', 'ws://127.0.0.1:8546')) - + return URI(os.environ.get("WEB3_WS_PROVIDER_URI", "ws://127.0.0.1:8546")) -class PersistentWebSocket: +class PersistentWebSocket(object, metaclass=DeprecationMetaClass): def __init__( - self, endpoint_uri: URI, websocket_kwargs: Any + self, + endpoint_uri: URI, + websocket_kwargs: Any, ) -> None: self.ws: WebSocketClientProtocol = None self.endpoint_uri = endpoint_uri @@ -68,13 +72,14 @@ def __init__( async def __aenter__(self) -> WebSocketClientProtocol: if self.ws is None: - self.ws = await connect( - uri=self.endpoint_uri, **self.websocket_kwargs - ) + self.ws = await connect(uri=self.endpoint_uri, **self.websocket_kwargs) return self.ws async def __aexit__( - self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType + self, + exc_type: Type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType, ) -> None: if exc_val is not None: try: @@ -84,7 +89,7 @@ async def __aexit__( self.ws = None -class WebsocketProvider(JSONBaseProvider): +class WebsocketProvider(JSONBaseProvider, metaclass=DeprecationMetaClass): logger = logging.getLogger("web3.providers.WebsocketProvider") _loop = None @@ -108,12 +113,12 @@ def __init__( ) if found_restricted_keys: raise ValidationError( - '{0} are not allowed in websocket_kwargs, ' - 'found: {1}'.format(RESTRICTED_WEBSOCKET_KWARGS, found_restricted_keys) + "{0} are not allowed in websocket_kwargs, " + "found: {1}".format( + RESTRICTED_WEBSOCKET_KWARGS, found_restricted_keys + ) ) - self.conn = PersistentWebSocket( - self.endpoint_uri, websocket_kwargs - ) + self.conn = PersistentWebSocket(self.endpoint_uri, websocket_kwargs) super().__init__() def __str__(self) -> str: @@ -122,22 +127,20 @@ def __str__(self) -> str: async def coro_make_request(self, request_data: bytes) -> RPCResponse: async with self.conn as conn: await asyncio.wait_for( - conn.send(request_data), - timeout=self.websocket_timeout + conn.send(request_data), timeout=self.websocket_timeout ) return json.loads( - await asyncio.wait_for( - conn.recv(), - timeout=self.websocket_timeout - ) + await asyncio.wait_for(conn.recv(), timeout=self.websocket_timeout) ) def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: - self.logger.debug("Making request WebSocket. URI: %s, " - "Method: %s", self.endpoint_uri, method) + self.logger.debug( + "Making request WebSocket. URI: %s, " "Method: %s", + self.endpoint_uri, + method, + ) request_data = self.encode_rpc_request(method, params) future = asyncio.run_coroutine_threadsafe( - self.coro_make_request(request_data), - WebsocketProvider._loop + self.coro_make_request(request_data), WebsocketProvider._loop ) return future.result()