diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index e1f1d1b51d..41203af679 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1046,6 +1046,14 @@ async def all_subnets( return_exceptions=True, ) + if isinstance(decoded, Exception): + # ``return_exceptions=True`` surfaces a failed runtime call as the + # ``decoded`` value; iterating it below would raise TypeError. + logging.warning( + f"Unable to fetch all subnets info for block {block}, block hash {block_hash}: {decoded}" + ) + return None + if not isinstance(subnet_prices, (SubstrateRequestException, ValueError)): for sn in decoded: sn.update( @@ -1409,6 +1417,13 @@ async def get_all_subnets_info( ), return_exceptions=True, ) + if isinstance(result, Exception): + # ``return_exceptions=True`` surfaces a failed runtime call as the + # ``result`` value; SubnetInfo.list_from_dicts would then raise TypeError. + logging.warning( + f"Unable to fetch all subnets info for block {block}, block hash {block_hash}: {result}" + ) + return [] if not result: return [] diff --git a/mypy.ini b/mypy.ini index d38bdc7172..054851282e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,13 @@ [mypy] ignore_missing_imports = True ignore_errors = True +follow_imports_for_stubs = True + +[mypy-numpy.*] +follow_imports = skip + +[mypy-numpy] +follow_imports = skip [mypy-*.axon.*] ignore_errors = False diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 94029b20ab..674be255a8 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2819,6 +2819,27 @@ async def test_get_all_subnets_info_success(mocker, subtensor): ) +@pytest.mark.asyncio +async def test_get_all_subnets_info_returns_empty_on_rpc_error(mocker, subtensor): + """If the runtime call fails, gather(return_exceptions=True) yields the exception as + ``result``; get_all_subnets_info must return [] instead of crashing inside + SubnetInfo.list_from_dicts with a TypeError.""" + mocker.patch.object(subtensor, "get_subnet_prices", return_value={}) + mocker.patch.object( + subtensor, + "query_runtime_api", + side_effect=async_subtensor.SubstrateRequestException("rpc down"), + ) + mocked_list_from_dicts = mocker.patch.object( + async_subtensor.SubnetInfo, "list_from_dicts" + ) + + result = await subtensor.get_all_subnets_info() + + assert result == [] + mocked_list_from_dicts.assert_not_called() + + @pytest.mark.asyncio async def test_set_subnet_identity(mocker, subtensor, fake_wallet): """Verify that subtensor method `set_subnet_identity` calls proper function with proper arguments.""" @@ -3768,6 +3789,24 @@ async def test_all_subnets(subtensor, mocker): assert result == mocked_di_list_from_dicts.return_value +@pytest.mark.asyncio +async def test_all_subnets_returns_none_on_rpc_error(subtensor, mocker): + """If the runtime call fails, gather(return_exceptions=True) yields the exception as + ``decoded``; all_subnets must return None (its documented failure value) instead of + raising TypeError while trying to iterate the exception.""" + mocker.patch.object(subtensor, "determine_block_hash") + mocker.patch.object(subtensor, "get_subnet_prices", return_value={}) + mocker.patch.object( + subtensor.substrate, + "runtime_call", + side_effect=async_subtensor.SubstrateRequestException("rpc down"), + ) + + result = await subtensor.all_subnets() + + assert result is None + + @pytest.mark.asyncio async def test_subnet(subtensor, mocker): """Verify that `subnet` calls proper methods and returns the correct value."""