diff --git a/CHANGELOG.md b/CHANGELOG.md index e1acd20cd2..eb66e0fa12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 9.0.3 /2025-02-26 + +## What's Changed +* Release/9.0.2 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2696 +* fix: typos in config test by @EricHasegawa in https://github.com/opentensor/bittensor/pull/2693 +* Removes limits in async + unstake_multiple by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2701 +* Fixes get_all_commitments, adds tests. by @thewhaleking in https://github.com/opentensor/bittensor/pull/2699 +* Use `.value` in e2e test by @thewhaleking in https://github.com/opentensor/bittensor/pull/2700 +* Fix e2e test setup by @thewhaleking in https://github.com/opentensor/bittensor/pull/2681 +* Dendrite `__del__` method fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/2702 +* Fix E2E test_set_weights by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2703 +* Updates test incentive by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2688 +* Add `get_timestamp` method by @thewhaleking in https://github.com/opentensor/bittensor/pull/2704 +* fix: async get_delegated by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2706 +* Properly mock data_chain class methods by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2705 +* Install btcli from install sh by @thewhaleking in https://github.com/opentensor/bittensor/pull/2708 +* Bumps dependencies of async substrate + btwallet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2710 +* Backmerge/main to staging 902 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2711 + +## New Contributors +* @EricHasegawa made their first contribution in https://github.com/opentensor/bittensor/pull/2693 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.2...v9.0.3 + ## 9.0.2 /2025-02-24 ## What's Changed diff --git a/VERSION b/VERSION index f202cd983c..8e055f7721 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.0.2 \ No newline at end of file +9.0.3 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 2f718d7c29..81304bbd29 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1,5 +1,6 @@ import asyncio import copy +from datetime import datetime, timezone import ssl from functools import partial from typing import Optional, Any, Union, Iterable, TYPE_CHECKING @@ -27,6 +28,7 @@ decode_account_id, DynamicInfo, ) +from bittensor.core.chain_data.delegate_info import DelegatedInfo from bittensor.core.chain_data.utils import decode_metadata from bittensor.core.config import Config from bittensor.core.errors import SubstrateRequestException @@ -999,7 +1001,7 @@ async def get_all_commitments( ) result = {} async for id_, value in query: - result[decode_account_id(id_[0])] = decode_account_id(value) + result[decode_account_id(id_[0])] = decode_metadata(value) return result async def get_current_weight_commit_info( @@ -1219,7 +1221,7 @@ async def get_delegated( if not result: return [] - return DelegateInfo.delegated_list_from_dicts(result) + return DelegatedInfo.list_from_dicts(result) async def get_delegates( self, @@ -2744,6 +2746,36 @@ async def weights_rate_limit( ) return None if call is None else int(call) + async def get_timestamp( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> datetime: + """ + Retrieves the datetime timestamp for a given block + + Arguments: + block: The blockchain block number for the query. Do not specify if specifying block_hash or reuse_block. + block_hash: The blockchain block_hash representation of the block id. Do not specify if specifying block + or reuse_block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not specify if specifying block or + block_hash. + + Returns: + datetime object for the timestamp of the block + """ + unix = ( + await self.query_module( + "Timestamp", + "Now", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + ).value + return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + # Extrinsics helper ================================================================================================ async def sign_and_send_extrinsic( diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index ef1992ac4b..d8a2fb08fa 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -877,7 +877,14 @@ def __del__(self): # ... some operations ... del dendrite # This will implicitly invoke the __del__ method and close the session. """ - self.close_session() + try: + self.close_session() + except RuntimeError: + if self._session: + logging.debug( + "A Dendrite session was unable to be closed during garbage-collection of the Dendrite object. This " + "usually indicates that you were not using the async context manager." + ) # For back-compatibility with torch diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 944219fcbb..36e7d3f6d5 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -314,20 +314,6 @@ async def add_stake_multiple_extrinsic( if staking_response is True: # If we successfully staked. # We only wait here if we expect finalization. - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_query = await subtensor.substrate.query( - module="SubtensorModule", storage_function="TxRateLimit" - ) - tx_rate_limit_blocks: int = getattr(tx_query, "value", 0) - if tx_rate_limit_blocks > 0: - logging.error( - f":hourglass: [yellow]Waiting for tx rate limit: [white]{tx_rate_limit_blocks}[/white] " - f"blocks[/yellow]" - ) - # 12 seconds per block - await asyncio.sleep(tx_rate_limit_blocks * 12) - if not wait_for_finalization and not wait_for_inclusion: old_balance -= staking_balance successful_stakes += 1 diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 01f41297b6..7ba1d120a2 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -281,18 +281,6 @@ async def unstake_multiple_extrinsic( if staking_response is True: # If we successfully unstaked. # We only wait here if we expect finalization. - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = await subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - logging.info( - f":hourglass: [yellow]Waiting for tx rate limit: " - f"[white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ) - await asyncio.sleep( - tx_rate_limit_blocks * 12 - ) # 12 seconds per block - if not wait_for_finalization and not wait_for_inclusion: successful_unstakes += 1 continue diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 51be15cce1..eb178f56f9 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,4 +1,3 @@ -import time from typing import Optional, TYPE_CHECKING from bittensor.core.errors import StakeError, NotRegisteredError @@ -266,16 +265,6 @@ def unstake_multiple_extrinsic( if staking_response is True: # If we successfully unstaked. # We only wait here if we expect finalization. - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - logging.info( - f":hourglass: [yellow]Waiting for tx rate limit: " - f"[white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ) - time.sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - if not wait_for_finalization and not wait_for_inclusion: successful_unstakes += 1 continue diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index dcd5fcc8ca..853c390d7d 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,4 +1,4 @@ -__version__ = "9.0.2" +__version__ = "9.0.3" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7709eba43c..ab7e2b5d53 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,4 +1,6 @@ import copy +from datetime import datetime, timezone + from functools import lru_cache from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast @@ -754,7 +756,7 @@ def get_all_commitments( ) result = {} for id_, value in query: - result[decode_account_id(id_[0])] = decode_account_id(value) + result[decode_account_id(id_[0])] = decode_metadata(value) return result def get_current_weight_commit_info( @@ -2070,6 +2072,19 @@ def weights_rate_limit( ) return None if call is None else int(call) + def get_timestamp(self, block: Optional[int] = None) -> datetime: + """ + Retrieves the datetime timestamp for a given block + + Arguments: + block: The blockchain block number for the query. + + Returns: + datetime object for the timestamp of the block + """ + unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value + return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + # Extrinsics helper ================================================================================================ def sign_and_send_extrinsic( diff --git a/scripts/install.sh b/scripts/install.sh index ada198a53e..6171d18761 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -105,6 +105,7 @@ linux_install_bittensor() { git clone https://github.com/opentensor/bittensor.git ~/.bittensor/bittensor/ 2> /dev/null || (cd ~/.bittensor/bittensor/ ; git fetch origin master ; git checkout master ; git pull --ff-only ; git reset --hard ; git clean -xdf) ohai "Installing bittensor" $python -m pip install -e ~/.bittensor/bittensor/ + $python -m pip install -U bittensor-cli exit_on_error $? } @@ -163,10 +164,11 @@ mac_update_pip() { } mac_install_bittensor() { - ohai "Cloning bittensor@text_prompting into ~/.bittensor/bittensor" + ohai "Cloning bittensor into ~/.bittensor/bittensor" git clone https://github.com/opentensor/bittensor.git ~/.bittensor/bittensor/ 2> /dev/null || (cd ~/.bittensor/bittensor/ ; git fetch origin master ; git checkout master ; git pull --ff-only ; git reset --hard; git clean -xdf) ohai "Installing bittensor" $python -m pip install -e ~/.bittensor/bittensor/ + $python -m pip install -U bittensor-cli exit_on_error $? deactivate } diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index acb29e54ba..9bcdf903fd 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -8,8 +8,9 @@ import pytest from async_substrate_interface import SubstrateInterface -from bittensor.core.subtensor import Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.subtensor import Subtensor from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.e2e_test_utils import ( Templates, @@ -101,6 +102,11 @@ def subtensor(local_chain): return Subtensor(network="ws://localhost:9944") +@pytest.fixture +def async_subtensor(local_chain): + return AsyncSubtensor(network="ws://localhost:9944") + + @pytest.fixture def alice_wallet(): keypair, wallet = setup_wallet("//Alice") diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 61ac041f20..e94baf3d6c 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -267,7 +267,7 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert success is True # Wait a few blocks - await asyncio.sleep(2) # Wait for the txs to be included in the chain + await asyncio.sleep(10) # Wait for the txs to be included in the chain # Query the WeightCommits storage map for all three salts weight_commits = subtensor.query_module( diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index b289b92192..66319c01d0 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -51,3 +51,61 @@ def test_commitment(subtensor, alice_wallet): netuid=1, uid=uid, ) + + assert ( + subtensor.get_all_commitments(netuid=1)[alice_wallet.hotkey.ss58_address] + == "Hello World!" + ) + + +@pytest.mark.asyncio +async def test_commitment_async(async_subtensor, alice_wallet): + async with async_subtensor as sub: + with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + await sub.set_commitment( + alice_wallet, + netuid=1, + data="Hello World!", + ) + + assert await sub.burned_register( + alice_wallet, + netuid=1, + ) + + uid = await sub.get_uid_for_hotkey_on_subnet( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert uid is not None + + assert "" == await sub.get_commitment( + netuid=1, + uid=uid, + ) + + assert await sub.set_commitment( + alice_wallet, + netuid=1, + data="Hello World!", + ) + + with pytest.raises( + SubstrateRequestException, + match="CommitmentSetRateLimitExceeded", + ): + await sub.set_commitment( + alice_wallet, + netuid=1, + data="Hello World!", + ) + + assert "Hello World!" == await sub.get_commitment( + netuid=1, + uid=uid, + ) + + assert (await sub.get_all_commitments(netuid=1))[ + alice_wallet.hotkey.ss58_address + ] == "Hello World!" diff --git a/tests/e2e_tests/test_cross_subtensor_compatibility.py b/tests/e2e_tests/test_cross_subtensor_compatibility.py new file mode 100644 index 0000000000..e26769a1df --- /dev/null +++ b/tests/e2e_tests/test_cross_subtensor_compatibility.py @@ -0,0 +1,18 @@ +from datetime import datetime +import pytest + + +@pytest.mark.asyncio +async def test_get_timestamp(subtensor, async_subtensor, local_chain): + with subtensor: + block_number = subtensor.get_current_block() + assert isinstance( + subtensor.get_timestamp(), datetime + ) # verify it works with no block number specified + sync_result = subtensor.get_timestamp( + block=block_number + ) # verify it works with block number specified + async with async_subtensor: + assert isinstance(await async_subtensor.get_timestamp(), datetime) + async_result = await async_subtensor.get_timestamp(block=block_number) + assert sync_result == async_result diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 5245139d36..9352ddbc8e 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -1,5 +1,4 @@ import asyncio -import time import pytest @@ -23,9 +22,6 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa AssertionError: If any of the checks or verifications fail """ - # Wait for 2 tempos to spin up chain properly - subtensor.wait_for_block(20) - print("Testing test_incentive") netuid = 2 @@ -51,7 +47,6 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # Get current miner/validator stats alice_neuron = metagraph.neurons[0] - time.sleep(30) assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 0 assert alice_neuron.stake.tao > 0 @@ -59,7 +54,6 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa bob_neuron = metagraph.neurons[1] - time.sleep(30) assert bob_neuron.incentive == 0 assert bob_neuron.consensus == 0 assert bob_neuron.rank == 0 @@ -79,15 +73,14 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # wait for the Validator to process and set_weights await asyncio.sleep(5) - # Wait until next epoch - await wait_epoch(subtensor, netuid) + # Wait few epochs + await wait_epoch(subtensor, netuid, times=4) # Refresh metagraph metagraph = subtensor.metagraph(netuid) # Get current emissions and validate that Alice has gotten tao alice_neuron = metagraph.neurons[0] - time.sleep(5) assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 1.0 diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 45daeb33fc..61145b5fc2 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -1,14 +1,13 @@ import numpy as np import pytest -import asyncio - from bittensor.utils.balance import Balance from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_hyperparameter_values, sudo_set_admin_utils, + wait_epoch, ) @@ -120,7 +119,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) assert success is True, f"Failed to set weights for subnet {netuid}" # Wait for the txs to be included in the chain - await asyncio.sleep(4) + await wait_epoch(subtensor, netuid=netuids[-1], times=4) for netuid in netuids: # Query the Weights storage map for all three subnets @@ -128,7 +127,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) module="SubtensorModule", name="Weights", params=[netuid, 0], # Alice should be the only UID - ) + ).value assert weights is not None, f"Weights not found for subnet {netuid}" assert weights == list( diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 0e2dd0d851..6cf1d50bd6 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -67,7 +67,7 @@ def sudo_set_hyperparameter_values( return response.is_success -async def wait_epoch(subtensor: "Subtensor", netuid: int = 1): +async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, times: int = 1): """ Waits for the next epoch to start on a specific subnet. @@ -81,9 +81,9 @@ async def wait_epoch(subtensor: "Subtensor", netuid: int = 1): q_tempo = [v for (k, v) in subtensor.query_map_subtensor("Tempo") if k == netuid] if len(q_tempo) == 0: raise Exception("could not determine tempo") - tempo = q_tempo[0] + tempo = q_tempo[0].value logging.info(f"tempo = {tempo}") - await wait_interval(tempo, subtensor, netuid) + await wait_interval(tempo * times, subtensor, netuid) def next_tempo(current_block: int, tempo: int, netuid: int) -> int: diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index c2ffe5c21b..edc3f27c72 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -78,12 +78,7 @@ def clone_or_update_templates(specific_commit=None): return install_dir + templates_repo -def install_templates(install_dir): - subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."]) - - def uninstall_templates(install_dir): - subprocess.check_call([sys.executable, "-m", "pip", "uninstall", "bittensor", "-y"]) # Delete everything in directory shutil.rmtree(install_dir) @@ -93,7 +88,6 @@ def __init__(self): self.dir = clone_or_update_templates() def __enter__(self): - install_templates(self.dir) return self def __exit__(self, exc_type, exc_value, traceback): diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 5123ce7607..becc9776b8 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1,6 +1,8 @@ +import datetime import unittest.mock as mock import pytest +from async_substrate_interface.types import ScaleObj from bittensor_wallet import Wallet from bittensor import u64_normalized_float @@ -389,8 +391,10 @@ async def test_get_delegates(subtensor, mocker, fake_result, response): autospec=subtensor.query_runtime_api, return_value=fake_result ) subtensor.query_runtime_api = mocked_query_runtime_api - mocked_delegate_info_list_from_dicts = mocker.Mock() - async_subtensor.DelegateInfo.list_from_dicts = mocked_delegate_info_list_from_dicts + mocked_delegate_info_list_from_dicts = mocker.patch.object( + async_subtensor.DelegateInfo, + "list_from_dicts", + ) # Call result = await subtensor.get_delegates(block_hash=None, reuse_block=False) @@ -432,7 +436,11 @@ async def test_get_stake_info_for_coldkey(subtensor, mocker, fake_result, respon mocked_stake_info_list_from_dicts = mocker.Mock( return_value=[mock_stake_info] if fake_result else [] ) - async_subtensor.StakeInfo.list_from_dicts = mocked_stake_info_list_from_dicts + mocker.patch.object( + async_subtensor.StakeInfo, + "list_from_dicts", + mocked_stake_info_list_from_dicts, + ) # Call result = await subtensor.get_stake_info_for_coldkey( @@ -974,9 +982,8 @@ async def test_neurons_lite(subtensor, mocker, fake_result, response): mocked_query_runtime_api = mocker.AsyncMock(return_value=fake_result) subtensor.query_runtime_api = mocked_query_runtime_api - mocked_neuron_info_lite_list_from_dicts = mocker.Mock() - async_subtensor.NeuronInfoLite.list_from_dicts = ( - mocked_neuron_info_lite_list_from_dicts + mocked_neuron_info_lite_list_from_dicts = mocker.patch.object( + async_subtensor.NeuronInfoLite, "list_from_dicts" ) # Call @@ -1138,11 +1145,14 @@ async def test_neuron_for_uid_happy_path(subtensor, mocker): fake_netuid = 2 fake_block_hash = "block_hash" - mocked_null_neuron = mocker.Mock() - async_subtensor.NeuronInfo.get_null_neuron = mocked_null_neuron - - mocked_neuron_info_from_dict = mocker.Mock() - async_subtensor.NeuronInfo.from_dict = mocked_neuron_info_from_dict + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) + mocked_neuron_info_from_dict = mocker.patch.object( + async_subtensor.NeuronInfo, + "from_dict", + ) # Call result = await subtensor.neuron_for_uid( @@ -1165,8 +1175,10 @@ async def test_neuron_for_uid_with_none_uid(subtensor, mocker): fake_netuid = 1 fake_block_hash = "block_hash" - mocked_null_neuron = mocker.Mock() - async_subtensor.NeuronInfo.get_null_neuron = mocked_null_neuron + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) # Call result = await subtensor.neuron_for_uid( @@ -1186,8 +1198,10 @@ async def test_neuron_for_uid(subtensor, mocker): fake_netuid = 2 fake_block_hash = "block_hash" - mocked_null_neuron = mocker.Mock() - async_subtensor.NeuronInfo.get_null_neuron = mocked_null_neuron + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) # no result in response mocked_substrate_runtime_call = mocker.AsyncMock( @@ -1197,8 +1211,10 @@ async def test_neuron_for_uid(subtensor, mocker): ) subtensor.substrate.runtime_call = mocked_substrate_runtime_call - mocked_neuron_info_from_dict = mocker.Mock() - async_subtensor.NeuronInfo.from_dict = mocked_neuron_info_from_dict + mocked_neuron_info_from_dict = mocker.patch.object( + async_subtensor.NeuronInfo, + "from_dict", + ) # Call result = await subtensor.neuron_for_uid( @@ -1217,9 +1233,9 @@ async def test_get_delegated_no_block_hash_no_reuse(subtensor, mocker): # Preps fake_coldkey_ss58 = "fake_ss58_address" - mocked_delegated_list_from_dicts = mocker.Mock() - async_subtensor.DelegateInfo.delegated_list_from_dicts = ( - mocked_delegated_list_from_dicts + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", ) # Call @@ -1245,9 +1261,9 @@ async def test_get_delegated_with_block_hash(subtensor, mocker): fake_coldkey_ss58 = "fake_ss58_address" fake_block_hash = "fake_block_hash" - mocked_delegated_list_from_dicts = mocker.Mock() - async_subtensor.DelegateInfo.delegated_list_from_dicts = ( - mocked_delegated_list_from_dicts + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", ) # Call @@ -1275,9 +1291,9 @@ async def test_get_delegated_with_reuse_block(subtensor, mocker): fake_coldkey_ss58 = "fake_ss58_address" reuse_block = True - mocked_delegated_list_from_dicts = mocker.Mock() - async_subtensor.DelegateInfo.delegated_list_from_dicts = ( - mocked_delegated_list_from_dicts + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", ) # Call @@ -2173,8 +2189,11 @@ async def test_weights_rate_limit_success(subtensor, mocker): fake_netuid = 1 fake_rate_limit = 10 - mocked_get_hyperparameter = mocker.AsyncMock(return_value=fake_rate_limit) - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_rate_limit, + ) # Call result = await subtensor.weights_rate_limit(netuid=fake_netuid) @@ -2196,8 +2215,11 @@ async def test_weights_rate_limit_none(subtensor, mocker): fake_netuid = 1 fake_result = None - mocked_get_hyperparameter = mocker.AsyncMock(return_value=fake_result) - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) # Call result = await subtensor.weights_rate_limit(netuid=fake_netuid) @@ -2222,10 +2244,11 @@ async def test_blocks_since_last_update_success(subtensor, mocker): current_block = 100 fake_blocks_since_update = current_block - last_update_block - mocked_get_hyperparameter = mocker.AsyncMock( - return_value={fake_uid: last_update_block} + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value={fake_uid: last_update_block}, ) - subtensor.get_hyperparameter = mocked_get_hyperparameter mocked_get_current_block = mocker.AsyncMock(return_value=current_block) subtensor.get_current_block = mocked_get_current_block @@ -2249,8 +2272,11 @@ async def test_blocks_since_last_update_no_last_update(subtensor, mocker): fake_uid = 5 fake_result = None - mocked_get_hyperparameter = mocker.AsyncMock(return_value=fake_result) - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) # Call result = await subtensor.blocks_since_last_update(netuid=fake_netuid, uid=fake_uid) @@ -2690,3 +2716,15 @@ async def test_get_all_neuron_certificates(mocker, subtensor): block_hash=None, reuse_block_hash=False, ) + + +@pytest.mark.asyncio +async def test_get_timestamp(mocker, subtensor): + fake_block = 1000 + mocked_query = mocker.AsyncMock(return_value=ScaleObj(1740586018 * 1000)) + mocker.patch.object(subtensor.substrate, "query", mocked_query) + expected_result = datetime.datetime( + 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc + ) + actual_result = await subtensor.get_timestamp(block=fake_block) + assert expected_result == actual_result diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index fc0ad7c6f7..5c1c2a0edf 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -4,7 +4,6 @@ def test_py_config_parsed_successfully_rust_wallet(): """Verify that python based config object is successfully parsed with rust-based wallet object.""" - # Preps parser = argparse.ArgumentParser() bittensor.wallet.add_args(parser) @@ -14,7 +13,7 @@ def test_py_config_parsed_successfully_rust_wallet(): config = bittensor.config(parser) - # since we can't apply mocking to rust implewmented object then replace those directly + # override config manually since we can't apply mocking to rust objects easily config.wallet.name = "new_wallet_name" config.wallet.hotkey = "new_hotkey" config.wallet.path = "/some/not_default/path" diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 2f5018337e..efbf302b48 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -83,6 +83,17 @@ def test_close(setup_dendrite, setup_axon): assert setup_dendrite._session is None +def test_garbage_collection(setup_dendrite): + del setup_dendrite # should not raise an error + + +@pytest.mark.asyncio +async def test_async_garbage_collection(setup_dendrite, setup_axon): + async with setup_dendrite as dendrite: + assert (await dendrite.session) is not None + del setup_dendrite # should not raise error + + @pytest.mark.asyncio async def test_aclose(setup_dendrite, setup_axon): axon = setup_axon diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0bd533c534..c1844ff63d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,11 +17,13 @@ import argparse import unittest.mock as mock +import datetime from unittest.mock import MagicMock import pytest from bittensor_wallet import Wallet from async_substrate_interface import sync_substrate +from async_substrate_interface.types import ScaleObj import websockets from bittensor import StakeInfo @@ -367,17 +369,23 @@ def test_blocks_since_last_update_success_calls(subtensor, mocker): uid = 7 mocked_current_block = 2 mocked_result = {uid: 1} - subtensor.get_hyperparameter = mocker.MagicMock(return_value=mocked_result) - subtensor.get_current_block = mocker.MagicMock(return_value=mocked_current_block) + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=mocked_result, + ) + mocked_get_current_block = mocker.patch.object( + subtensor, + "get_current_block", + return_value=mocked_current_block, + ) # Call result = subtensor.blocks_since_last_update(netuid=7, uid=uid) # Assertions - subtensor.get_current_block.assert_called_once() - subtensor.get_hyperparameter.assert_called_once_with( - param_name="LastUpdate", netuid=7 - ) + mocked_get_current_block.assert_called_once() + mocked_get_hyperparameter.assert_called_once_with(param_name="LastUpdate", netuid=7) assert result == 1 # if we change the methods logic in the future we have to be make sure the returned type is correct assert isinstance(result, int) @@ -386,13 +394,17 @@ def test_blocks_since_last_update_success_calls(subtensor, mocker): def test_weights_rate_limit_success_calls(subtensor, mocker): """Tests the weights_rate_limit method to ensure it correctly fetches the WeightsSetRateLimit hyperparameter.""" # Prep - subtensor.get_hyperparameter = mocker.MagicMock(return_value=5) + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=5, + ) # Call result = subtensor.weights_rate_limit(netuid=7) # Assertions - subtensor.get_hyperparameter.assert_called_once_with( + mocked_get_hyperparameter.assert_called_once_with( param_name="WeightsSetRateLimit", netuid=7, block=None, @@ -1285,9 +1297,11 @@ def test_subnetwork_n(subtensor, mocker): fake_block = 123 fake_result = 2 - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fake_result - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) # Call result = subtensor.subnetwork_n(fake_netuid, fake_block) @@ -1589,11 +1603,13 @@ def test_immunity_period(subtensor, mocker): # Preps fake_netuid = 1 fake_block = 123 - fare_result = 101 + fake_result = 101 - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fare_result - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) # Call result = subtensor.immunity_period(netuid=fake_netuid, block=fake_block) @@ -1638,11 +1654,13 @@ def test_tempo(subtensor, mocker): # Preps fake_netuid = 1 fake_block = 123 - fare_result = 101 + fake_result = 101 - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fare_result - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) # Call result = subtensor.tempo(netuid=fake_netuid, block=fake_block) @@ -1823,8 +1841,11 @@ def test_min_allowed_weights(subtensor, mocker): fake_block = 123 return_value = 10 - mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=return_value, + ) # Call result = subtensor.min_allowed_weights(netuid=fake_netuid, block=fake_block) @@ -1842,8 +1863,11 @@ def test_max_weight_limit(subtensor, mocker): fake_block = 123 return_value = 100 - mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) - subtensor.get_hyperparameter = mocked_get_hyperparameter + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=return_value, + ) mocked_u16_normalized_float = mocker.patch.object( subtensor_module, @@ -3073,3 +3097,14 @@ def test_get_all_neuron_certificates(mocker, subtensor): params=[fake_netuid], block_hash=None, ) + + +def test_get_timestamp(mocker, subtensor): + fake_block = 1000 + mocked_query = mocker.MagicMock(return_value=ScaleObj(1740586018 * 1000)) + mocker.patch.object(subtensor.substrate, "query", mocked_query) + expected_result = datetime.datetime( + 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc + ) + actual_result = subtensor.get_timestamp(block=fake_block) + assert expected_result == actual_result