Skip to content

Commit 2816a38

Browse files
authored
Merge pull request #126 from opentensor/release/1.2.2
Release/1.2.2
2 parents 540a603 + d88d2b7 commit 2816a38

File tree

5 files changed

+85
-16
lines changed

5 files changed

+85
-16
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.2.1 /2025-05-22
4+
5+
## What's Changed
6+
* Add proper mock support by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/123
7+
* Handle Incorrect Timeouts by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/124
8+
9+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.2.1...v1.2.2
10+
311
## 1.2.1 /2025-05-12
412

513
## What's Changed

async_substrate_interface/async_substrate.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import logging
1010
import ssl
1111
import time
12+
import warnings
13+
from unittest.mock import AsyncMock
1214
from hashlib import blake2b
1315
from typing import (
1416
Optional,
@@ -530,15 +532,31 @@ def __init__(
530532
self._exit_task = None
531533
self._open_subscriptions = 0
532534
self._options = options if options else {}
533-
self.last_received = time.time()
535+
try:
536+
now = asyncio.get_running_loop().time()
537+
except RuntimeError:
538+
warnings.warn(
539+
"You are instantiating the AsyncSubstrateInterface Websocket outside of an event loop. "
540+
"Verify this is intended."
541+
)
542+
now = asyncio.new_event_loop().time()
543+
self.last_received = now
544+
self.last_sent = now
534545

535546
async def __aenter__(self):
536547
async with self._lock:
537548
self._in_use += 1
538549
await self.connect()
539550
return self
540551

552+
@staticmethod
553+
async def loop_time() -> float:
554+
return asyncio.get_running_loop().time()
555+
541556
async def connect(self, force=False):
557+
now = await self.loop_time()
558+
self.last_received = now
559+
self.last_sent = now
542560
if self._exit_task:
543561
self._exit_task.cancel()
544562
if not self._initialized or force:
@@ -594,7 +612,7 @@ async def _recv(self) -> None:
594612
try:
595613
# TODO consider wrapping this in asyncio.wait_for and use that for the timeout logic
596614
response = json.loads(await self.ws.recv(decode=False))
597-
self.last_received = time.time()
615+
self.last_received = await self.loop_time()
598616
async with self._lock:
599617
# note that these 'subscriptions' are all waiting sent messages which have not received
600618
# responses, and thus are not the same as RPC 'subscriptions', which are unique
@@ -630,12 +648,12 @@ async def send(self, payload: dict) -> int:
630648
Returns:
631649
id: the internal ID of the request (incremented int)
632650
"""
633-
# async with self._lock:
634651
original_id = get_next_id()
635652
# self._open_subscriptions += 1
636653
await self.max_subscriptions.acquire()
637654
try:
638655
await self.ws.send(json.dumps({**payload, **{"id": original_id}}))
656+
self.last_sent = await self.loop_time()
639657
return original_id
640658
except (ConnectionClosed, ssl.SSLError, EOFError):
641659
async with self._lock:
@@ -697,13 +715,16 @@ def __init__(
697715
self.chain_endpoint = url
698716
self.url = url
699717
self._chain = chain_name
700-
self.ws = Websocket(
701-
url,
702-
options={
703-
"max_size": self.ws_max_size,
704-
"write_limit": 2**16,
705-
},
706-
)
718+
if not _mock:
719+
self.ws = Websocket(
720+
url,
721+
options={
722+
"max_size": self.ws_max_size,
723+
"write_limit": 2**16,
724+
},
725+
)
726+
else:
727+
self.ws = AsyncMock(spec=Websocket)
707728
self._lock = asyncio.Lock()
708729
self.config = {
709730
"use_remote_preset": use_remote_preset,
@@ -726,9 +747,11 @@ def __init__(
726747
self._initializing = False
727748
self.registry_type_map = {}
728749
self.type_id_to_name = {}
750+
self._mock = _mock
729751

730752
async def __aenter__(self):
731-
await self.initialize()
753+
if not self._mock:
754+
await self.initialize()
732755
return self
733756

734757
async def initialize(self):
@@ -2120,7 +2143,11 @@ async def _make_rpc_request(
21202143

21212144
if request_manager.is_complete:
21222145
break
2123-
if time.time() - self.ws.last_received >= self.retry_timeout:
2146+
if (
2147+
(current_time := await self.ws.loop_time()) - self.ws.last_received
2148+
>= self.retry_timeout
2149+
and current_time - self.ws.last_sent >= self.retry_timeout
2150+
):
21242151
if attempt >= self.max_retries:
21252152
logger.warning(
21262153
f"Timed out waiting for RPC requests {attempt} times. Exiting."

async_substrate_interface/sync_substrate.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import socket
44
from hashlib import blake2b
55
from typing import Optional, Union, Callable, Any
6+
from unittest.mock import MagicMock
67

78
from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
89
from scalecodec import (
@@ -13,7 +14,7 @@
1314
MultiAccountId,
1415
)
1516
from scalecodec.base import RuntimeConfigurationObject, ScaleBytes, ScaleType
16-
from websockets.sync.client import connect
17+
from websockets.sync.client import connect, ClientConnection
1718
from websockets.exceptions import ConnectionClosed
1819

1920
from async_substrate_interface.const import SS58_FORMAT
@@ -522,14 +523,18 @@ def __init__(
522523
)
523524
self.metadata_version_hex = "0x0f000000" # v15
524525
self.reload_type_registry()
525-
self.ws = self.connect(init=True)
526526
self.registry_type_map = {}
527527
self.type_id_to_name = {}
528+
self._mock = _mock
528529
if not _mock:
530+
self.ws = self.connect(init=True)
529531
self.initialize()
532+
else:
533+
self.ws = MagicMock(spec=ClientConnection)
530534

531535
def __enter__(self):
532-
self.initialize()
536+
if not self._mock:
537+
self.initialize()
533538
return self
534539

535540
def __del__(self):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "async-substrate-interface"
3-
version = "1.2.1"
3+
version = "1.2.2"
44
description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
55
readme = "README.md"
66
license = { file = "LICENSE" }

tests/unit_tests/test_mock.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from websockets.exceptions import InvalidURI
2+
import pytest
3+
4+
from async_substrate_interface import AsyncSubstrateInterface, SubstrateInterface
5+
6+
7+
@pytest.mark.asyncio
8+
async def test_async_mock():
9+
ssi = AsyncSubstrateInterface("notreal")
10+
assert isinstance(ssi, AsyncSubstrateInterface)
11+
with pytest.raises(InvalidURI):
12+
await ssi.initialize()
13+
async with AsyncSubstrateInterface("notreal", _mock=True) as ssi:
14+
assert isinstance(ssi, AsyncSubstrateInterface)
15+
ssi = AsyncSubstrateInterface("notreal", _mock=True)
16+
async with ssi:
17+
pass
18+
19+
20+
def test_sync_mock():
21+
with pytest.raises(InvalidURI):
22+
SubstrateInterface("notreal")
23+
ssi = SubstrateInterface("notreal", _mock=True)
24+
assert isinstance(ssi, SubstrateInterface)
25+
with pytest.raises(InvalidURI):
26+
with SubstrateInterface("notreal") as ssi:
27+
pass
28+
with SubstrateInterface("notreal", _mock=True) as ssi:
29+
assert isinstance(ssi, SubstrateInterface)

0 commit comments

Comments
 (0)