diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a3a2d61a5..021b766bd 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -67,18 +67,18 @@ jobs: run: pyright - name: Test with tox run: tox -e py - - name: Coveralls Parallel - uses: coverallsapp/github-action@v2 - with: - flag-name: run-${{ join(matrix.*, '-') }} - parallel: true - finish: - needs: build - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true + # - name: Coveralls Parallel + # uses: coverallsapp/github-action@v2 + # with: + # flag-name: run-${{ join(matrix.*, '-') }} + # parallel: true + # finish: + # needs: build + # if: ${{ always() }} + # runs-on: ubuntu-latest + # timeout-minutes: 5 + # steps: + # - name: Coveralls Finished + # uses: coverallsapp/github-action@v2 + # with: + # parallel-finished: true diff --git a/binance/ws/websocket_api.py b/binance/ws/websocket_api.py index 8542b62db..52c5518d7 100644 --- a/binance/ws/websocket_api.py +++ b/binance/ws/websocket_api.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional import asyncio from websockets import WebSocketClientProtocol # type: ignore @@ -13,10 +13,15 @@ def __init__(self, url: str, tld: str = "com", testnet: bool = False): self._tld = tld self._testnet = testnet self._responses: Dict[str, asyncio.Future] = {} - self._connection_lock = ( - asyncio.Lock() - ) # used to ensure only one connection is established at a time + self._connection_lock: Optional[asyncio.Lock] = None super().__init__(url=url, prefix="", path="", is_binary=False) + + @property + def connection_lock(self) -> asyncio.Lock: + if self._connection_lock is None: + loop = asyncio.get_event_loop() + self._connection_lock = asyncio.Lock() + return self._connection_lock def _handle_message(self, msg): """Override message handling to support request-response""" @@ -51,7 +56,7 @@ async def _ensure_ws_connection(self) -> None: 3. Wait for connection to be ready 4. Handle reconnection if needed """ - async with self._connection_lock: + async with self.connection_lock: try: if ( self.ws is None diff --git a/tests/conftest.py b/tests/conftest.py index ca4550e5d..660369746 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,20 +62,32 @@ def futuresClient(): @pytest.fixture(scope="function") -def clientAsync(): - return AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=testnet) +async def clientAsync(): + client = AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=testnet) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") -def futuresClientAsync(): - return AsyncClient( +async def futuresClientAsync(): + client = AsyncClient( futures_api_key, futures_api_secret, https_proxy=proxy, testnet=testnet ) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") -def liveClientAsync(): - return AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=False) +async def liveClientAsync(): + client = AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=False) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") def manager(): diff --git a/tests/test_ws_api.py b/tests/test_ws_api.py index dfdbad1cc..a3694ee5d 100644 --- a/tests/test_ws_api.py +++ b/tests/test_ws_api.py @@ -114,38 +114,58 @@ async def test_testnet_url(): @pytest.mark.asyncio async def test_message_handling(clientAsync): """Test message handling with various message types""" - # Test valid message - future = asyncio.Future() - clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 200, "result": {"test": "data"}} - clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - result = await clientAsync.ws_api._responses["123"] - assert result == valid_msg - -@pytest.mark.asyncio -async def test_message_handling_raise_exception(clientAsync): - with pytest.raises(BinanceAPIException): + try: + # Test valid message future = asyncio.Future() clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + valid_msg = {"id": "123", "status": 200, "result": {"test": "data"}} clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - await future + result = await clientAsync.ws_api._responses["123"] + assert result == valid_msg + finally: + # Ensure cleanup + await clientAsync.close_connection() + + +@pytest.mark.asyncio +async def test_message_handling_raise_exception(clientAsync): + try: + with pytest.raises(BinanceAPIException): + future = asyncio.Future() + clientAsync.ws_api._responses["123"] = future + valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + clientAsync.ws_api._handle_message(json.dumps(valid_msg)) + await future + finally: + # Ensure cleanup + await clientAsync.close_connection() + + @pytest.mark.asyncio async def test_message_handling_raise_exception_without_id(clientAsync): - with pytest.raises(BinanceAPIException): - future = asyncio.Future() - clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} - clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - await future - + try: + with pytest.raises(BinanceAPIException): + future = asyncio.Future() + clientAsync.ws_api._responses["123"] = future + valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + clientAsync.ws_api._handle_message(json.dumps(valid_msg)) + await future + finally: + # Ensure cleanup + await clientAsync.close_connection() + + @pytest.mark.asyncio async def test_message_handling_invalid_json(clientAsync): - with pytest.raises(json.JSONDecodeError): - clientAsync.ws_api._handle_message("invalid json") + try: + with pytest.raises(json.JSONDecodeError): + clientAsync.ws_api._handle_message("invalid json") - with pytest.raises(json.JSONDecodeError): - clientAsync.ws_api._handle_message("invalid json") + with pytest.raises(json.JSONDecodeError): + clientAsync.ws_api._handle_message("invalid json") + finally: + # Ensure cleanup + await clientAsync.close_connection() @pytest.mark.asyncio(scope="function") @@ -199,6 +219,7 @@ async def test_ws_queue_overflow(clientAsync): # Restore original queue size clientAsync.ws_api.MAX_QUEUE_SIZE = original_size + @pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") @pytest.mark.asyncio async def test_ws_api_with_stream(clientAsync): diff --git a/tox.ini b/tox.ini index ab2427681..d26e34650 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ passenv = TEST_API_SECRET TEST_FUTURES_API_KEY TEST_FUTURES_API_SECRET -commands = pytest -n 1 -v tests/ --doctest-modules --cov binance --cov-report term-missing --reruns 3 --reruns-delay 120 +commands = pytest -n 5 -v tests/ --doctest-modules --cov binance --cov-report term-missing --reruns 3 --reruns-delay 120 [pep8] ignore = E501