Skip to content
This repository was archived by the owner on Jul 12, 2026. It is now read-only.

Commit fc3bbbc

Browse files
fix(sudo): show single numbered hyperparameter table in interactive sudo set
Interactive `btcli sudo set` printed the full hyperparameter values table and then a second picker table repeating names and descriptions. The values table now carries the selection numbers itself, with settable registry params absent from the chain response (e.g. alpha_values) appended so nothing previously selectable is lost.
1 parent 7816ff0 commit fc3bbbc

3 files changed

Lines changed: 120 additions & 54 deletions

File tree

bittensor_cli/cli.py

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
COLORS,
4343
HYPERPARAMS,
4444
HYPERPARAMS_METADATA,
45-
RootSudoOnly,
4645
WalletOptions,
4746
)
4847
from bittensor_cli.src.bittensor import utils
@@ -7304,12 +7303,17 @@ def sudo_set(
73047303
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
73057304
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
73067305

7306+
param_names: list[str] = []
73077307
if not param_name or not param_value:
7308-
hyperparams = self._run_command(
7309-
sudo.get_hyperparameters(self.initialize_chain(network), netuid),
7308+
success, param_names = self._run_command(
7309+
sudo.get_hyperparameters(
7310+
self.initialize_chain(network),
7311+
netuid,
7312+
numbered=not param_name,
7313+
),
73107314
exit_early=False,
7311-
)
7312-
if not hyperparams:
7315+
) or (False, [])
7316+
if not success:
73137317
# Ensure we don't leave the websocket connection hanging.
73147318
if self.subtensor:
73157319
try:
@@ -7328,49 +7332,13 @@ def sudo_set(
73287332
"Param name not supplied with `--no-prompt` flag. Cannot continue"
73297333
)
73307334
return False
7331-
hyperparam_list = sorted(HYPERPARAMS.keys())
7332-
console.print("Available hyperparameters:\n")
7333-
7334-
# Create a table to show hyperparameters with descriptions
7335-
param_table = Table(
7336-
Column("[white]#", style="dim", width=4),
7337-
Column("[white]HYPERPARAMETER", style=COLORS.SU.HYPERPARAMETER),
7338-
Column("[white]OWNER SETTABLE", style="bright_cyan", width=18),
7339-
Column("[white]DESCRIPTION", style="dim", overflow="fold"),
7340-
box=box.SIMPLE,
7341-
show_edge=False,
7342-
pad_edge=False,
7343-
)
7344-
7345-
for idx, param in enumerate(hyperparam_list, start=1):
7346-
metadata = HYPERPARAMS_METADATA.get(param, {})
7347-
description = metadata.get("description", "No description available.")
7348-
7349-
# Check ownership from HYPERPARAMS
7350-
_, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE))
7351-
if root_sudo == RootSudoOnly.TRUE:
7352-
owner_settable_str = "[red]No (Root Only)[/red]"
7353-
elif root_sudo == RootSudoOnly.COMPLICATED:
7354-
owner_settable_str = "[yellow]COMPLICATED[/yellow]"
7355-
else:
7356-
owner_settable_str = "[green]Yes[/green]"
7357-
7358-
param_table.add_row(
7359-
str(idx),
7360-
f"[bold]{param}[/bold]",
7361-
owner_settable_str,
7362-
description,
7363-
)
7364-
7365-
console.print(param_table)
73667335
console.print()
7367-
73687336
choice = IntPrompt.ask(
73697337
"Enter the [bold]number[/bold] of the hyperparameter",
7370-
choices=[str(i) for i in range(1, len(hyperparam_list) + 1)],
7338+
choices=[str(i) for i in range(1, len(param_names) + 1)],
73717339
show_choices=False,
73727340
)
7373-
param_name = hyperparam_list[choice - 1]
7341+
param_name = param_names[choice - 1]
73747342

73757343
# Show additional info for selected parameter
73767344
metadata = HYPERPARAMS_METADATA.get(param_name, {})
@@ -7561,11 +7529,12 @@ def sudo_get(
75617529
[green]$[/green] btcli sudo get --netuid 1
75627530
"""
75637531
self.verbosity_handler(quiet, verbose, json_output, False)
7564-
return self._run_command(
7532+
success, _ = self._run_command(
75657533
sudo.get_hyperparameters(
75667534
self.initialize_chain(network), netuid, json_output
75677535
)
7568-
)
7536+
) or (False, [])
7537+
return success
75697538

75707539
def sudo_senate(
75717540
self,

bittensor_cli/src/commands/sudo.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,8 +1143,16 @@ async def get_hyperparameters(
11431143
netuid: int,
11441144
json_output: bool = False,
11451145
show_descriptions: bool = True,
1146-
) -> bool:
1147-
"""View hyperparameters of a subnetwork."""
1146+
numbered: bool = False,
1147+
) -> tuple[bool, list[str]]:
1148+
"""View hyperparameters of a subnetwork.
1149+
1150+
When `numbered` is True, each row is prefixed with a selection number and
1151+
settable hyperparameters not reported by the chain (e.g. alpha_values) are
1152+
appended, so callers can prompt the user to pick a row from this table.
1153+
1154+
Returns (success, hyperparameter names in display order).
1155+
"""
11481156
print_verbose("Fetching hyperparameters")
11491157
try:
11501158
if not await subtensor.subnet_exists(netuid):
@@ -1153,7 +1161,7 @@ async def get_hyperparameters(
11531161
json_console.print_json(data={"error": error_msg})
11541162
else:
11551163
print_error(error_msg)
1156-
return False
1164+
return False, []
11571165
subnet, subnet_info = await asyncio.gather(
11581166
subtensor.get_subnet_hyperparameters(netuid), subtensor.subnet(netuid)
11591167
)
@@ -1163,19 +1171,22 @@ async def get_hyperparameters(
11631171
json_console.print_json(data={"error": error_msg})
11641172
else:
11651173
print_error(error_msg)
1166-
return False
1174+
return False, []
11671175
except Exception as e:
11681176
if json_output:
11691177
json_console.print_json(data={"error": str(e)})
11701178
else:
11711179
raise
1172-
return False
1180+
return False, []
11731181

11741182
# Determine if we should show extended info (descriptions, ownership)
11751183
show_extended = show_descriptions and not json_output
1184+
numbered = numbered and show_extended
11761185

11771186
if show_extended:
1187+
columns = [Column("[white]#", style="dim")] if numbered else []
11781188
table = Table(
1189+
*columns,
11791190
Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER),
11801191
Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE),
11811192
Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL),
@@ -1209,10 +1220,19 @@ async def get_hyperparameters(
12091220
show_edge=True,
12101221
)
12111222
dict_out = []
1223+
param_names = []
12121224

12131225
normalized_values = normalize_hyperparameters(subnet, json_output=json_output)
12141226
sorted_values = sorted(normalized_values, key=lambda x: x[0])
1227+
if numbered:
1228+
# Keep settable hyperparameters absent from the chain response (e.g.
1229+
# alpha_values) selectable from this single table.
1230+
displayed = {param for param, _, _ in sorted_values}
1231+
sorted_values += [
1232+
(param, "-", "-") for param in sorted(set(HYPERPARAMS) - displayed)
1233+
]
12151234
for param, value, norm_value in sorted_values:
1235+
param_names.append(param)
12161236
if not json_output:
12171237
if show_extended:
12181238
# Get metadata for this hyperparameter
@@ -1236,13 +1256,16 @@ async def get_hyperparameters(
12361256
else:
12371257
description_with_link = description
12381258

1239-
table.add_row(
1259+
row = [
12401260
" " + param,
12411261
value,
12421262
norm_value,
12431263
owner_settable_str,
12441264
description_with_link,
1245-
)
1265+
]
1266+
if numbered:
1267+
row.insert(0, str(len(param_names)))
1268+
table.add_row(*row)
12461269
else:
12471270
table.add_row(" " + param, value, norm_value)
12481271
else:
@@ -1272,7 +1295,7 @@ async def get_hyperparameters(
12721295
json_str = json.dumps(dict_out, ensure_ascii=True)
12731296
sys.stdout.write(json_str + "\n")
12741297
sys.stdout.flush()
1275-
return True
1298+
return True, param_names
12761299
else:
12771300
console.print(table)
12781301
if show_extended:
@@ -1295,7 +1318,7 @@ async def get_hyperparameters(
12951318
console.print(
12961319
"[dim]📚 For detailed documentation, visit: [link]https://docs.bittensor.com[/link]"
12971320
)
1298-
return True
1321+
return True, param_names
12991322

13001323

13011324
async def get_senate(
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Unit tests for sudo.get_hyperparameters.
3+
4+
Covers the (success, hyperparameter names) return contract and the `numbered`
5+
selection mode used by the interactive `btcli sudo set` flow, which renders a
6+
single table instead of a separate picker table.
7+
"""
8+
9+
import pytest
10+
from unittest.mock import AsyncMock, MagicMock
11+
12+
from bittensor_cli.src import HYPERPARAMS
13+
from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
14+
from bittensor_cli.src.commands.sudo import get_hyperparameters
15+
16+
CHAIN_PARAMS = {"immunity_period": 65535, "kappa": 32767, "tempo": 360}
17+
18+
19+
def _prepare_subtensor(mock_subtensor) -> MagicMock:
20+
subnet_info = MagicMock()
21+
subnet_info.subnet_name = "test_subnet"
22+
mock_subtensor.get_subnet_hyperparameters = AsyncMock(
23+
return_value=SubnetHyperparameters(hyperparameters=dict(CHAIN_PARAMS))
24+
)
25+
mock_subtensor.subnet = AsyncMock(return_value=subnet_info)
26+
return mock_subtensor
27+
28+
29+
@pytest.mark.asyncio
30+
async def test_returns_chain_params_in_display_order(mock_subtensor):
31+
subtensor = _prepare_subtensor(mock_subtensor)
32+
33+
success, param_names = await get_hyperparameters(subtensor, netuid=18)
34+
35+
assert success is True
36+
assert param_names == sorted(CHAIN_PARAMS)
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_numbered_appends_registry_only_params(mock_subtensor):
41+
subtensor = _prepare_subtensor(mock_subtensor)
42+
43+
success, param_names = await get_hyperparameters(
44+
subtensor, netuid=18, numbered=True
45+
)
46+
47+
registry_only = sorted(set(HYPERPARAMS) - set(CHAIN_PARAMS))
48+
assert success is True
49+
assert param_names == sorted(CHAIN_PARAMS) + registry_only
50+
assert "alpha_values" in param_names
51+
assert len(param_names) == len(set(param_names))
52+
53+
54+
@pytest.mark.asyncio
55+
async def test_numbered_has_no_effect_on_json_output(mock_subtensor, capsys):
56+
subtensor = _prepare_subtensor(mock_subtensor)
57+
58+
success, param_names = await get_hyperparameters(
59+
subtensor, netuid=18, json_output=True, numbered=True
60+
)
61+
62+
assert success is True
63+
assert param_names == sorted(CHAIN_PARAMS)
64+
assert "alpha_values" not in capsys.readouterr().out
65+
66+
67+
@pytest.mark.asyncio
68+
async def test_nonexistent_subnet_returns_failure(mock_subtensor):
69+
mock_subtensor.subnet_exists = AsyncMock(return_value=False)
70+
71+
success, param_names = await get_hyperparameters(mock_subtensor, netuid=999)
72+
73+
assert success is False
74+
assert param_names == []

0 commit comments

Comments
 (0)