From 72c5233c8cb0cd29be22251ad9b0ecf7079829a5 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 14 Jan 2025 02:26:00 -0800 Subject: [PATCH 1/8] improve ScaleObj + use dumper --- .../substrate_interface.py | 104 ++++++++++++++++-- tests/unit_tests/test_substrate_interface.py | 57 +++++++++- 2 files changed, 150 insertions(+), 11 deletions(-) diff --git a/async_substrate_interface/substrate_interface.py b/async_substrate_interface/substrate_interface.py index d17c698..23fc7fd 100644 --- a/async_substrate_interface/substrate_interface.py +++ b/async_substrate_interface/substrate_interface.py @@ -51,14 +51,14 @@ class ScaleObj: - def __new__(cls, value): - if isinstance(value, (dict, str, int)): - return value - return super().__new__(cls) + """Bittensor representation of Scale Object.""" def __init__(self, value): self.value = list(value) if isinstance(value, tuple) else value + def __new__(cls, value): + return super().__new__(cls) + def __str__(self): return f"BittensorScaleType(value={self.value})>" @@ -66,14 +66,96 @@ def __repr__(self): return repr(self.value) def __eq__(self, other): - return self.value == other + return self.value == (other.value if isinstance(other, ScaleObj) else other) + + def __lt__(self, other): + return self.value < (other.value if isinstance(other, ScaleObj) else other) + + def __gt__(self, other): + return self.value > (other.value if isinstance(other, ScaleObj) else other) + + def __le__(self, other): + return self.value <= (other.value if isinstance(other, ScaleObj) else other) + + def __ge__(self, other): + return self.value >= (other.value if isinstance(other, ScaleObj) else other) + + def __add__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value + other.value) + return ScaleObj(self.value + other) + + def __radd__(self, other): + return ScaleObj(other + self.value) + + def __sub__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value - other.value) + return ScaleObj(self.value - other) + + def __rsub__(self, other): + return ScaleObj(other - self.value) + + def __mul__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value * other.value) + return ScaleObj(self.value * other) + + def __rmul__(self, other): + return ScaleObj(other * self.value) + + def __truediv__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value / other.value) + return ScaleObj(self.value / other) + + def __rtruediv__(self, other): + return ScaleObj(other / self.value) + + def __floordiv__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value // other.value) + return ScaleObj(self.value // other) + + def __rfloordiv__(self, other): + return ScaleObj(other // self.value) + + def __mod__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value % other.value) + return ScaleObj(self.value % other) + + def __rmod__(self, other): + return ScaleObj(other % self.value) + + def __pow__(self, other): + if isinstance(other, ScaleObj): + return ScaleObj(self.value**other.value) + return ScaleObj(self.value**other) + + def __rpow__(self, other): + return ScaleObj(other**self.value) + + def __getitem__(self, key): + if isinstance(self.value, (list, tuple, dict, str)): + return self.value[key] + raise TypeError( + f"Object of type '{type(self.value).__name__}' does not support indexing" + ) def __iter__(self): - for item in self.value: - yield item + if hasattr(self.value, "__iter__"): + return iter(self.value) + raise TypeError(f"Object of type '{type(self.value).__name__}' is not iterable") - def __getitem__(self, item): - return self.value[item] + def __len__(self): + return len(self.value) + + @classmethod + def dumper(cls, obj): + if isinstance(obj, ScaleObj): + return obj.value + return obj class AsyncExtrinsicReceipt: @@ -889,7 +971,9 @@ async def send(self, payload: dict) -> int: self.id += 1 # self._open_subscriptions += 1 try: - await self.ws.send(json.dumps({**payload, **{"id": original_id}})) + payload = {**payload, **{"id": original_id}} + dumped_payload = json.dumps(payload, default=ScaleObj.dumper) + await self.ws.send(dumped_payload) return original_id except (ConnectionClosed, ssl.SSLError, EOFError): async with self._lock: diff --git a/tests/unit_tests/test_substrate_interface.py b/tests/unit_tests/test_substrate_interface.py index a178363..f012d07 100644 --- a/tests/unit_tests/test_substrate_interface.py +++ b/tests/unit_tests/test_substrate_interface.py @@ -1,7 +1,10 @@ import pytest from websockets.exceptions import InvalidURI -from async_substrate_interface.substrate_interface import AsyncSubstrateInterface +from async_substrate_interface.substrate_interface import ( + AsyncSubstrateInterface, + ScaleObj, +) @pytest.mark.asyncio @@ -9,3 +12,55 @@ async def test_invalid_url_raises_exception(): """Test that invalid URI raises an InvalidURI exception.""" with pytest.raises(InvalidURI): AsyncSubstrateInterface("non_existent_entry_point") + + +def test_scale_object(): + """Verifies that the instance can be subject to various operations.""" + # Preps + inst_int = ScaleObj(100) + + # Asserts + assert inst_int + 1 == 101 + assert 1 + inst_int == 101 + assert inst_int - 1 == 99 + assert 101 - inst_int == 1 + assert inst_int * 2 == 200 + assert 2 * inst_int == 200 + assert inst_int / 2 == 50 + assert 100 / inst_int == 1 + assert inst_int // 2 == 50 + assert 1001 // inst_int == 10 + assert inst_int % 3 == 1 + assert 1002 % inst_int == 2 + assert inst_int >= 99 + assert inst_int <= 101 + + # Preps + inst_str = ScaleObj("test") + + # Asserts + assert inst_str + "test1" == "testtest1" + assert "test1" + inst_str == "test1test" + assert inst_str * 2 == "testtest" + assert 2 * inst_str == "testtest" + assert inst_str >= "test" + assert inst_str <= "testtest" + assert inst_str[0] == "t" + assert [i for i in inst_str] == ["t", "e", "s", "t"] + + # Preps + inst_list = ScaleObj([1, 2, 3]) + + # Asserts + assert inst_list[0] == 1 + assert inst_list[-1] == 3 + assert inst_list * 2 == inst_list + inst_list + assert [i for i in inst_list] == [1, 2, 3] + assert inst_list >= [1, 2] + assert inst_list <= [1, 2, 3, 4] + assert len(inst_list) == 3 + + inst_dict = ScaleObj({"a": 1, "b": 2}) + assert inst_dict["a"] == 1 + assert inst_dict["b"] == 2 + assert [i for i in inst_dict] == ["a", "b"] From 34b01ecd8f092822fa899ad297c9a0232e9be103 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 14 Jan 2025 16:10:52 +0200 Subject: [PATCH 2/8] Added additional missing methods for backwards compatibility. --- async_substrate_interface/substrate_interface.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/async_substrate_interface/substrate_interface.py b/async_substrate_interface/substrate_interface.py index 23fc7fd..09a9aa6 100644 --- a/async_substrate_interface/substrate_interface.py +++ b/async_substrate_interface/substrate_interface.py @@ -151,6 +151,12 @@ def __iter__(self): def __len__(self): return len(self.value) + def serialize(self): + return self.value + + def decode(self): + return self.value + @classmethod def dumper(cls, obj): if isinstance(obj, ScaleObj): From fe05cb91ba155e70ab3e3087b699c4bcf13070c7 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 14 Jan 2025 09:36:23 -0800 Subject: [PATCH 3/8] rename wrong naming :) --- async_substrate_interface/substrate_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/async_substrate_interface/substrate_interface.py b/async_substrate_interface/substrate_interface.py index 09a9aa6..23c7875 100644 --- a/async_substrate_interface/substrate_interface.py +++ b/async_substrate_interface/substrate_interface.py @@ -158,7 +158,7 @@ def decode(self): return self.value @classmethod - def dumper(cls, obj): + def dump(cls, obj): if isinstance(obj, ScaleObj): return obj.value return obj @@ -978,7 +978,7 @@ async def send(self, payload: dict) -> int: # self._open_subscriptions += 1 try: payload = {**payload, **{"id": original_id}} - dumped_payload = json.dumps(payload, default=ScaleObj.dumper) + dumped_payload = json.dumps(payload, default=ScaleObj.dump) await self.ws.send(dumped_payload) return original_id except (ConnectionClosed, ssl.SSLError, EOFError): From 3ee3f59b5a1f938c254affa4d69ba07581ff5ed5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 14 Jan 2025 20:47:48 +0200 Subject: [PATCH 4/8] Further improvements, remove dump. --- .../substrate_interface.py | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/async_substrate_interface/substrate_interface.py b/async_substrate_interface/substrate_interface.py index 09a9aa6..3e95c09 100644 --- a/async_substrate_interface/substrate_interface.py +++ b/async_substrate_interface/substrate_interface.py @@ -12,6 +12,7 @@ import ssl import time from collections import defaultdict +from collections.abc import Iterable from dataclasses import dataclass from datetime import datetime from hashlib import blake2b @@ -62,8 +63,14 @@ def __new__(cls, value): def __str__(self): return f"BittensorScaleType(value={self.value})>" + def __bool__(self): + if self.value: + return True + else: + return False + def __repr__(self): - return repr(self.value) + return repr(f"BittensorScaleType(value={self.value})>") def __eq__(self, other): return self.value == (other.value if isinstance(other, ScaleObj) else other) @@ -144,7 +151,7 @@ def __getitem__(self, key): ) def __iter__(self): - if hasattr(self.value, "__iter__"): + if isinstance(self.value, Iterable): return iter(self.value) raise TypeError(f"Object of type '{type(self.value).__name__}' is not iterable") @@ -157,12 +164,6 @@ def serialize(self): def decode(self): return self.value - @classmethod - def dumper(cls, obj): - if isinstance(obj, ScaleObj): - return obj.value - return obj - class AsyncExtrinsicReceipt: """ @@ -977,9 +978,7 @@ async def send(self, payload: dict) -> int: self.id += 1 # self._open_subscriptions += 1 try: - payload = {**payload, **{"id": original_id}} - dumped_payload = json.dumps(payload, default=ScaleObj.dumper) - await self.ws.send(dumped_payload) + await self.ws.send(json.dumps({**payload, **{"id": original_id}})) return original_id except (ConnectionClosed, ssl.SSLError, EOFError): async with self._lock: @@ -3568,9 +3567,9 @@ async def query( raw_storage_key: Optional[bytes] = None, subscription_handler=None, reuse_block_hash: bool = False, - ) -> "ScaleType": + ) -> Optional[Union["ScaleObj", Any]]: """ - Queries subtensor. This should only be used when making a single request. For multiple requests, + Queries substrate. This should only be used when making a single request. For multiple requests, you should use ``self.query_multiple`` """ block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) @@ -3614,7 +3613,7 @@ async def query_map( page_size: int = 100, ignore_decoding_errors: bool = False, reuse_block_hash: bool = False, - ) -> "QueryMapResult": + ) -> QueryMapResult: """ Iterates over all key-pairs located at the given module and storage_function. The storage item must be a map. @@ -3776,9 +3775,7 @@ def concat_hash_len(key_hasher: str) -> int: if not ignore_decoding_errors: raise item_value = None - result.append([item_key, item_value]) - return QueryMapResult( records=result, page_size=page_size, From cdf21b4187bc72c3e741b869343ec1ec9a2e6ebb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 14 Jan 2025 22:11:57 +0200 Subject: [PATCH 5/8] Skip test --- tests/unit_tests/test_substrate_interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit_tests/test_substrate_interface.py b/tests/unit_tests/test_substrate_interface.py index f012d07..9f02b9b 100644 --- a/tests/unit_tests/test_substrate_interface.py +++ b/tests/unit_tests/test_substrate_interface.py @@ -1,3 +1,4 @@ +import asyncio import pytest from websockets.exceptions import InvalidURI @@ -7,6 +8,8 @@ ) + +@pytest.mark.skip(reason="Issues with nested asyncio") @pytest.mark.asyncio async def test_invalid_url_raises_exception(): """Test that invalid URI raises an InvalidURI exception.""" From 557336c408351dc558e87cc3bdb8a9bf50133cc8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 14 Jan 2025 22:53:04 +0200 Subject: [PATCH 6/8] Changes the initialisation to a task. --- async_substrate_interface/substrate_interface.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/async_substrate_interface/substrate_interface.py b/async_substrate_interface/substrate_interface.py index 3e95c09..66876ff 100644 --- a/async_substrate_interface/substrate_interface.py +++ b/async_substrate_interface/substrate_interface.py @@ -1087,10 +1087,7 @@ def __init__( ) if pre_initialize: if not _mock: - execute_coroutine( - coroutine=self.initialize(), - event_loop=self.event_loop, - ) + self.event_loop.create_task(self.initialize()) else: self.reload_type_registry() From 0df786316b1ccdf84e79f4dd23e9c794963c43dc Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 14 Jan 2025 22:58:56 +0200 Subject: [PATCH 7/8] test --- tests/unit_tests/test_substrate_interface.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_substrate_interface.py b/tests/unit_tests/test_substrate_interface.py index 9f02b9b..616152d 100644 --- a/tests/unit_tests/test_substrate_interface.py +++ b/tests/unit_tests/test_substrate_interface.py @@ -8,13 +8,12 @@ ) - -@pytest.mark.skip(reason="Issues with nested asyncio") @pytest.mark.asyncio async def test_invalid_url_raises_exception(): """Test that invalid URI raises an InvalidURI exception.""" with pytest.raises(InvalidURI): - AsyncSubstrateInterface("non_existent_entry_point") + async with AsyncSubstrateInterface("non_existent_entry_point"): + pass def test_scale_object(): From a3ae910575faa08b87cfd58ad9784aa3a4d15f6b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 15 Jan 2025 18:48:50 -0800 Subject: [PATCH 8/8] Bumps version and changelog --- CHANGELOG.md | 9 ++++++++- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07681b1..49f17f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## 1.0.0rc1 /2025-01-13 +# Changelog + +## 1.0.0rc2 /2025-01-15 + +## What's Changed +* Improve ScaleObj by @roman-opentensor in https://github.com/opentensor/async-substrate-interface/pull/2 + +## 1.0.0rc1 /2025-01-15 ## What's Changed * New Async Substrate Interface by @thewhaleking and @roman-opentensor in https://github.com/opentensor/async-substrate-interface/tree/main diff --git a/pyproject.toml b/pyproject.toml index 0c383d9..c82d049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "async-substrate-interface" -version = "1.0.0rc1" +version = "1.0.0rc2" description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface" readme = "README.md" license = { file = "LICENSE" } diff --git a/setup.py b/setup.py index 1b84e79..ddcdea7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="async-substrate-interface", - version="1.0.0rc1", + version="1.0.0rc2", description="Asyncio library for interacting with substrate.", long_description=open("README.md").read(), long_description_content_type="text/markdown",