From dc9587b967b820a2906047aeb13a735652c9e370 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 11 Dec 2025 02:49:38 +0100 Subject: [PATCH 01/10] Improved subnet hyperparameters cmd --- bittensor_cli/cli.py | 67 +++++- bittensor_cli/src/__init__.py | 240 ++++++++++++++++++++ bittensor_cli/src/commands/sudo.py | 116 ++++++++-- tests/e2e_tests/test_hyperparams_setting.py | 7 + tests/e2e_tests/test_staking_sudo.py | 27 ++- 5 files changed, 435 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3af83c735..7ee235f52 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -43,6 +43,8 @@ Constants, COLORS, HYPERPARAMS, + HYPERPARAMS_METADATA, + RootSudoOnly, WalletOptions, ) from bittensor_cli.src.bittensor import utils @@ -6367,10 +6369,16 @@ def sudo_set( Used to set hyperparameters for a specific subnet. This command allows subnet owners to modify hyperparameters such as its tempo, emission rates, and other hyperparameters. + + When listing hyperparameters, descriptions, ownership information, and side-effects are displayed to help you make informed decisions. + + You can also set custom hyperparameters not in the standard list by using the exact parameter name from the chain metadata. EXAMPLE [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 + + [green]$[/green] btcli sudo set --netuid 1 --param custom_param_name --value 123 """ self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) @@ -6394,15 +6402,70 @@ def sudo_set( [field.name for field in fields(SubnetHyperparameters)] ) console.print("Available hyperparameters:\n") + + # Create a table to show hyperparameters with descriptions + from rich.table import Table, Column + from rich import box + param_table = Table( + Column("[white]#", style="dim", width=4), + Column("[white]HYPERPARAMETER", style=COLORS.SU.HYPERPARAMETER), + Column("[white]OWNER SETTABLE", style="bright_cyan", width=18), + Column("[white]DESCRIPTION", style="dim", overflow="fold"), + box=box.SIMPLE, + show_edge=False, + pad_edge=False, + ) + for idx, param in enumerate(hyperparam_list, start=1): - console.print(f" {idx}. {param}") + metadata = HYPERPARAMS_METADATA.get(param, {}) + description = metadata.get("description", "No description available.") + + # Check ownership from HYPERPARAMS + _, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE)) + if root_sudo == RootSudoOnly.TRUE: + owner_settable_str = "[red]No (Root Only)[/red]" + elif root_sudo == RootSudoOnly.COMPLICATED: + owner_settable_str = "[yellow]Maybe[/yellow]" + else: + owner_settable_str = "[green]Yes[/green]" + + param_table.add_row( + str(idx), + f"[bold]{param}[/bold]", + owner_settable_str, + description, + ) + + console.print(param_table) console.print() + choice = IntPrompt.ask( "Enter the [bold]number[/bold] of the hyperparameter", choices=[str(i) for i in range(1, len(hyperparam_list) + 1)], show_choices=False, ) param_name = hyperparam_list[choice - 1] + + # Show additional info for selected parameter + metadata = HYPERPARAMS_METADATA.get(param_name, {}) + if metadata: + console.print(f"\n[bold cyan]Selected:[/bold cyan] {param_name}") + description = metadata.get('description', 'No description available.') + docs_link = metadata.get('docs_link', '') + if docs_link: + # Show description text followed by clickable blue [link] at the end + # Use Rich Text to create clickable [link] with blue color + from rich.text import Text + desc_text = Text(f"{description} ") + desc_text.append("[link]", style=f"link https://{docs_link} bright_blue underline") + console.print(desc_text) + else: + console.print(f"{description}") + side_effects = metadata.get('side_effects', '') + if side_effects: + console.print(f"[dim]Side Effects:[/dim] {side_effects}") + if docs_link: + console.print(f"[dim]📚 Docs:[/dim] [link]https://{docs_link}[/link]\n") if param_name in ["alpha_high", "alpha_low"]: if not prompt: @@ -6501,6 +6564,8 @@ def sudo_get( ): """ Shows a list of the hyperparameters for the specified subnet. + + Displays hyperparameter values along with descriptions, ownership information (which parameters can be set by subnet owners vs root sudo), and side-effects. EXAMPLE diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 9b4e1c3c3..a4a1a9e5c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -683,12 +683,252 @@ class RootSudoOnly(Enum): "bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", RootSudoOnly.FALSE), "transfers_enabled": ("sudo_set_toggle_transfer", RootSudoOnly.FALSE), "min_allowed_uids": ("sudo_set_min_allowed_uids", RootSudoOnly.TRUE), + # Note: These are displayed but not directly settable via HYPERPARAMS + # They are derived or set via other mechanisms + "alpha_high": ("", RootSudoOnly.FALSE), # Derived from alpha_values + "alpha_low": ("", RootSudoOnly.FALSE), # Derived from alpha_values + "max_weight_limit": ("sudo_set_max_weight_limit", RootSudoOnly.FALSE), # Alias for max_weights_limit + "subnet_is_active": ("", RootSudoOnly.FALSE), # Set via btcli subnets start + "yuma_version": ("", RootSudoOnly.FALSE), # Related to yuma3_enabled } HYPERPARAMS_MODULE = { "user_liquidity_enabled": "Swap", } +# Hyperparameter metadata: descriptions, side-effects, ownership, and documentation links +HYPERPARAMS_METADATA = { + "rho": { + "description": "Rho controls the rate at which weights decay over time.", + "side_effects": "Changing rho affects how quickly neurons' influence diminishes, impacting consensus dynamics.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#rho", + }, + "kappa": { + "description": "Kappa determines the scaling factor for consensus calculations.", + "side_effects": "Modifying kappa changes how validator votes are weighted in consensus mechanisms.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#kappa", + }, + "immunity_period": { + "description": "Duration (in blocks) during which newly registered neurons are protected from certain penalties.", + "side_effects": "Increasing immunity period gives new neurons more time to establish themselves before facing penalties.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#immunityperiod", + }, + "min_allowed_weights": { + "description": "Minimum number of weight connections a neuron must maintain to stay active.", + "side_effects": "Lower values allow neurons with fewer connections to remain active; higher values enforce stricter connectivity requirements.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minallowedweights", + }, + "max_weights_limit": { + "description": "Maximum number of weight connections a neuron can have with other neurons.", + "side_effects": "Limits the maximum out-degree of the network graph, affecting network topology and consensus.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxweightslimit", + }, + "tempo": { + "description": "Number of blocks between epoch transitions", + "side_effects": "Lower tempo means more frequent updates but higher chain load. Higher tempo reduces frequency but may slow responsiveness.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#tempo", + }, + "min_difficulty": { + "description": "Minimum proof-of-work difficulty required for registration", + "side_effects": "Increasing min_difficulty raises the computational barrier for new neuron registrations.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#mindifficulty", + }, + "max_difficulty": { + "description": "Maximum proof-of-work difficulty cap.", + "side_effects": "Caps the maximum computational requirement, ensuring registration remains feasible.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxdifficulty", + }, + "weights_version": { + "description": "Version key for weight sets.", + "side_effects": "Changing this invalidates all existing weights, forcing neurons to resubmit weights.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#weightsversion", + }, + "weights_rate_limit": { + "description": "Maximum number of weight updates allowed per epoch.", + "side_effects": "Lower values reduce chain load but may limit legitimate weight updates. Higher values allow more flexibility.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#weightsratelimit--commitmentratelimit", + }, + "adjustment_interval": { + "description": "Number of blocks between automatic difficulty adjustments.", + "side_effects": "Shorter intervals make difficulty more responsive but may cause volatility. Longer intervals provide stability.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#adjustmentinterval", + }, + "activity_cutoff": { + "description": "Minimum activity level required for neurons to remain active.", + "side_effects": "Lower values keep more neurons active; higher values prune inactive neurons more aggressively.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#activitycutoff", + }, + "target_regs_per_interval": { + "description": "Target number of new registrations per adjustment interval.", + "side_effects": "Affects how the difficulty adjustment algorithm targets registration rates.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#targetregistrationsperinterval", + }, + "min_burn": { + "description": "Minimum TAO burn amount required for subnet registration.", + "side_effects": "Increasing min_burn raises the barrier to entry, potentially reducing spam but also limiting participation.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minburn", + }, + "max_burn": { + "description": "Maximum TAO burn amount cap for subnet registration.", + "side_effects": "Caps registration costs, ensuring registration remains accessible even as difficulty increases.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxburn", + }, + "bonds_moving_avg": { + "description": "Moving average window size for bond calculations.", + "side_effects": "Larger windows provide smoother bond values but slower response to changes. Smaller windows react faster but may be more volatile.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#bondsmovingaverage", + }, + "max_regs_per_block": { + "description": "Maximum number of registrations allowed per block.", + "side_effects": "Lower values reduce chain load but may create registration bottlenecks. Higher values allow more throughput.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxregistrationsperblock", + }, + "serving_rate_limit": { + "description": "Rate limit for serving requests.", + "side_effects": "Affects network throughput and prevents individual neurons from monopolizing serving capacity.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#servingratelimit", + }, + "max_validators": { + "description": "Maximum number of validators allowed in the subnet.", + "side_effects": "Lower values reduce consensus overhead but limit decentralization. Higher values increase decentralization but may slow consensus.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxallowedvalidators", + }, + "adjustment_alpha": { + "description": "Alpha parameter for difficulty adjustment algorithm.", + "side_effects": "Higher values make difficulty adjustments more aggressive; lower values provide smoother transitions.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#adjustmentalpha", + }, + "difficulty": { + "description": "Current proof-of-work difficulty for registration.", + "side_effects": "Directly affects registration cost and time. Higher difficulty makes registration harder and more expensive.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#difficulty", + }, + "commit_reveal_period": { + "description": "Duration (in blocks) for commit-reveal weight submission scheme.", + "side_effects": "Longer periods provide more time for commits but delay weight revelation. Shorter periods increase frequency.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#commitrevealperiod", + }, + "commit_reveal_weights_enabled": { + "description": "Enable or disable commit-reveal scheme for weight submissions.", + "side_effects": "Enabling prevents front-running of weight submissions. Disabling allows immediate weight visibility.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#commitrevealweightsenabled", + }, + "alpha_values": { + "description": "Alpha range [low, high] for stake calculations.", + "side_effects": "Affects how stake is converted and calculated. Changing these values impacts staking economics.", + "owner_settable": True, + "docs_link": "", + }, + "liquid_alpha_enabled": { + "description": "Enable or disable liquid alpha staking mechanism.", + "side_effects": "Enabling provides more staking flexibility. Disabling uses traditional staking mechanisms.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#liquidalphaenabled", + }, + "registration_allowed": { + "description": "Enable or disable new registrations to the subnet.", + "side_effects": "Disabling registration closes the subnet to new participants. Enabling allows open registration.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#networkregistrationallowed", + }, + "network_pow_registration_allowed": { + "description": "Enable or disable proof-of-work based registration.", + "side_effects": "Disabling removes PoW requirement, potentially allowing easier registration. Enabling enforces computational proof.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#networkpowregistrationallowed", + }, + "yuma3_enabled": { + "description": "Enable or disable Yuma3 consensus mechanism.", + "side_effects": "Enabling Yuma3 activates advanced consensus features. Disabling uses standard consensus mechanisms.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#yumaversion", + }, + "alpha_sigmoid_steepness": { + "description": "Steepness parameter for alpha sigmoid function.", + "side_effects": "Affects how alpha values are transformed in staking calculations. Higher values create steeper curves.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alphasigmoidsteepness", + }, + "user_liquidity_enabled": { + "description": "Enable or disable user liquidity features.", + "side_effects": "Enabling allows liquidity provision and swaps. Disabling restricts liquidity operations.", + "owner_settable": True, # COMPLICATED - can be set by owner or sudo + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#userliquidityenabled", + }, + "bonds_reset_enabled": { + "description": "Enable or disable periodic bond resets..", + "side_effects": "Enabling provides periodic bond resets, preventing bond accumulation. Disabling allows bonds to accumulate.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#bondsresetenabled", + }, + "transfers_enabled": { + "description": "Enable or disable TAO transfers within the subnet.", + "side_effects": "Enabling allows TAO transfers between neurons. Disabling prevents all transfer operations.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#toggletransfer", + }, + "min_allowed_uids": { + "description": "Minimum number of UIDs (neurons) required for the subnet to remain active.", + "side_effects": "If subnet falls below this threshold, it may be deactivated. Higher values enforce stricter minimums.", + "owner_settable": False, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minalloweduids", + }, + # Additional hyperparameters that appear in chain data but aren't directly settable via HYPERPARAMS + "alpha_high": { + "description": "High bound of the alpha range for stake calculations.", + "side_effects": "Affects the upper bound of alpha conversion in staking mechanisms. Set via alpha_values parameter.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alpha-values", + }, + "alpha_low": { + "description": "Low bound of the alpha range for stake calculations.", + "side_effects": "Affects the lower bound of alpha conversion in staking mechanisms. Set via alpha_values parameter.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alpha-values", + }, + "max_weight_limit": { + "description": "Maximum number of weight connections a neuron can have with other neurons.", + "side_effects": "Limits the maximum out-degree of the network graph, affecting network topology and consensus.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxweightlimit", + }, + "subnet_is_active": { + "description": "Whether the subnet is currently active and operational.", + "side_effects": "When inactive, the subnet cannot process requests or participate in network operations. Set via 'btcli subnets start' command.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/managing-subnets#starting-subnets", + }, + "yuma_version": { + "description": "Version of the Yuma consensus mechanism.", + "side_effects": "Changing the version affects which Yuma consensus features are active. Use yuma3_enabled to toggle Yuma3.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#yuma3", + }, +} + # Help Panels for cli help HELP_PANELS = { "WALLET": { diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 9992fe77e..0578d954a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -7,11 +7,13 @@ from rich import box from rich.table import Column, Table from rich.prompt import Confirm +from rich.text import Text from scalecodec import GenericCall from bittensor_cli.src import ( HYPERPARAMS, HYPERPARAMS_MODULE, + HYPERPARAMS_METADATA, RootSudoOnly, DelegatesDetails, COLOR_PALETTE, @@ -768,7 +770,7 @@ async def sudo_set_hyperparameter( async def get_hyperparameters( - subtensor: "SubtensorInterface", netuid: int, json_output: bool = False + subtensor: "SubtensorInterface", netuid: int, json_output: bool = False, show_descriptions: bool = True ) -> bool: """View hyperparameters of a subnetwork.""" print_verbose("Fetching hyperparameters") @@ -782,40 +784,120 @@ async def get_hyperparameters( print_error(f"Subnet with netuid {netuid} does not exist.") return False - table = Table( - Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER), - Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE), - Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL), - title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: " - f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}" - f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" - f"[/{COLOR_PALETTE.G.SUBHEAD}]" - f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", - show_footer=True, - width=None, - pad_edge=False, - box=box.SIMPLE, - show_edge=True, - ) + # Determine if we should show extended info (descriptions, ownership) + show_extended = show_descriptions and not json_output + + if show_extended: + table = Table( + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER), + Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE), + Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL), + Column("[white]OWNER SETTABLE", style="bright_cyan"), + Column("[white]DESCRIPTION", style="dim", overflow="fold"), + title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}" + f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" + f"[/{COLOR_PALETTE.G.SUBHEAD}]" + f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", + show_footer=True, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + ) + else: + table = Table( + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER), + Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE), + Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL), + title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}" + f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" + f"[/{COLOR_PALETTE.G.SUBHEAD}]" + f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", + show_footer=True, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + ) dict_out = [] normalized_values = normalize_hyperparameters(subnet, json_output=json_output) sorted_values = sorted(normalized_values, key=lambda x: x[0]) for param, value, norm_value in sorted_values: if not json_output: - table.add_row(" " + param, value, norm_value) + if show_extended: + # Get metadata for this hyperparameter + metadata = HYPERPARAMS_METADATA.get(param, {}) + owner_settable = metadata.get("owner_settable", False) + description = metadata.get("description", "No description available.") + + # Check actual ownership from HYPERPARAMS + _, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE)) + if root_sudo == RootSudoOnly.TRUE: + owner_settable_str = "[red]No (Root Only)[/red]" + elif root_sudo == RootSudoOnly.COMPLICATED: + owner_settable_str = "[yellow]Maybe (Owner/Sudo)[/yellow]" + else: + owner_settable_str = "[green]Yes[/green]" + + # Format description with docs link if available + docs_link = metadata.get("docs_link", "") + if docs_link: + # Use Rich Text to create description with clickable bright blue [link] at the end + description_text = Text(f"{description} ") + description_text.append("[link]", style=f"link https://{docs_link} bright_blue underline") + description_with_link = description_text + else: + description_with_link = description + + table.add_row( + " " + param, + value, + norm_value, + owner_settable_str, + description_with_link, + ) + else: + table.add_row(" " + param, value, norm_value) else: + metadata = HYPERPARAMS_METADATA.get(param, {}) dict_out.append( { "hyperparameter": param, "value": value, "normalized_value": norm_value, + "owner_settable": metadata.get("owner_settable", False), + "description": metadata.get("description", "No description available."), + "side_effects": metadata.get("side_effects", "No side effects documented."), + "docs_link": metadata.get("docs_link", ""), } ) if json_output: json_console.print(json.dumps(dict_out)) else: console.print(table) + if show_extended: + console.print( + f"\n[dim]💡 Tip: Use [bold]btcli sudo set --param --value [/bold] to modify hyperparameters." + ) + console.print( + f"[dim]💡 Tip: Subnet owners can set parameters marked '[green]Yes[/green]'. " + f"Parameters marked '[red]No (Root Only)[/red]' require root sudo access." + ) + console.print( + f"[dim]💡 Tip: To set custom hyperparameters not in this list, use the exact parameter name from the chain metadata." + ) + console.print( + f"[dim] Example: [bold]btcli sudo set --netuid {netuid} --param custom_param_name --value 123[/bold]" + ) + console.print( + f"[dim] The parameter name must match exactly as defined in the chain's AdminUtils pallet metadata." + ) + console.print( + f"[dim]📚 For detailed documentation, visit: [link]https://docs.bittensor.com[/link]" + ) return True diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index c21008bb5..765e58af1 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -83,6 +83,13 @@ def test_hyperparams_setting(local_chain, wallet_setup): hp = {} for hyperparam in all_hyperparams: hp[hyperparam["hyperparameter"]] = hyperparam["value"] + # Verify new metadata fields are present in JSON output + assert "description" in hyperparam, f"Missing description for {hyperparam['hyperparameter']}" + assert "side_effects" in hyperparam, f"Missing side_effects for {hyperparam['hyperparameter']}" + assert "owner_settable" in hyperparam, f"Missing owner_settable for {hyperparam['hyperparameter']}" + assert "docs_link" in hyperparam, f"Missing docs_link for {hyperparam['hyperparameter']}" + # Verify description is not empty (unless it's a parameter without metadata) + assert isinstance(hyperparam["description"], str), f"Description should be string for {hyperparam['hyperparameter']}" for key, (_, sudo_only) in HYPERPARAMS.items(): if key in hp.keys() and sudo_only == RootSudoOnly.FALSE: if isinstance(hp[key], bool): diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 4f5346207..e19396eae 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -499,6 +499,13 @@ def line(key: str) -> Union[str, bool]: # Parse all hyperparameters and single out max_burn in TAO all_hyperparams = json.loads(hyperparams.stdout) + # Verify new metadata fields are present in JSON output + for hyperparam in all_hyperparams: + assert "description" in hyperparam, f"Missing description for {hyperparam['hyperparameter']}" + assert "side_effects" in hyperparam, f"Missing side_effects for {hyperparam['hyperparameter']}" + assert "owner_settable" in hyperparam, f"Missing owner_settable for {hyperparam['hyperparameter']}" + assert "docs_link" in hyperparam, f"Missing docs_link for {hyperparam['hyperparameter']}" + assert isinstance(hyperparam["description"], str), f"Description should be string for {hyperparam['hyperparameter']}" max_burn_tao = next( filter(lambda x: x["hyperparameter"] == "max_burn", all_hyperparams) )["value"] @@ -518,9 +525,15 @@ def line(key: str) -> Union[str, bool]: ], ) hyperparams_json_output = json.loads(hyperparams_json.stdout) - max_burn_tao_from_json = next( + # Verify metadata fields are present in this JSON output too + max_burn_param = next( filter(lambda x: x["hyperparameter"] == "max_burn", hyperparams_json_output) - )["value"] + ) + assert "description" in max_burn_param, "Missing description for max_burn" + assert "side_effects" in max_burn_param, "Missing side_effects for max_burn" + assert "owner_settable" in max_burn_param, "Missing owner_settable for max_burn" + assert "docs_link" in max_burn_param, "Missing docs_link for max_burn" + max_burn_tao_from_json = max_burn_param["value"] assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(100.0) # Change max_burn hyperparameter to 10 TAO @@ -587,11 +600,17 @@ def line(key: str) -> Union[str, bool]: ], ) updated_hyperparams_json_output = json.loads(updated_hyperparams_json.stdout) - max_burn_tao_from_json = next( + # Verify metadata fields are still present after update + max_burn_updated = next( filter( lambda x: x["hyperparameter"] == "max_burn", updated_hyperparams_json_output ) - )["value"] + ) + assert "description" in max_burn_updated, "Missing description for max_burn after update" + assert "side_effects" in max_burn_updated, "Missing side_effects for max_burn after update" + assert "owner_settable" in max_burn_updated, "Missing owner_settable for max_burn after update" + assert "docs_link" in max_burn_updated, "Missing docs_link for max_burn after update" + max_burn_tao_from_json = max_burn_updated["value"] assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(10.0) change_yuma3_hyperparam = exec_command_alice( From 84d01fae2147557c4ccf14f701213d01e6b119f7 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 02:54:20 +0100 Subject: [PATCH 02/10] Upgrade hyperlink --- bittensor_cli/src/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a4a1a9e5c..6d181dab5 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -901,13 +901,13 @@ class RootSudoOnly(Enum): "description": "High bound of the alpha range for stake calculations.", "side_effects": "Affects the upper bound of alpha conversion in staking mechanisms. Set via alpha_values parameter.", "owner_settable": True, - "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alpha-values", + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alphasigmoidsteepness", }, "alpha_low": { "description": "Low bound of the alpha range for stake calculations.", "side_effects": "Affects the lower bound of alpha conversion in staking mechanisms. Set via alpha_values parameter.", "owner_settable": True, - "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alpha-values", + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alphasigmoidsteepness", }, "max_weight_limit": { "description": "Maximum number of weight connections a neuron can have with other neurons.", @@ -919,7 +919,7 @@ class RootSudoOnly(Enum): "description": "Whether the subnet is currently active and operational.", "side_effects": "When inactive, the subnet cannot process requests or participate in network operations. Set via 'btcli subnets start' command.", "owner_settable": True, - "docs_link": "docs.learnbittensor.org/subnets/managing-subnets#starting-subnets", + "docs_link": "docs.learnbittensor.org/subnets/managing-subnets#subnetisactive", }, "yuma_version": { "description": "Version of the Yuma consensus mechanism.", From 202f9ff511c95c9a0cdcd3c5fbc77e050f0707e0 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 11:10:40 +0100 Subject: [PATCH 03/10] Fixed lint error --- bittensor_cli/src/commands/sudo.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 0578d954a..9714a18c2 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -368,7 +368,7 @@ async def set_hyperparameter_extrinsic( to_sudo_or_not_to_sudo = True # default to sudo true when no-prompt is set else: to_sudo_or_not_to_sudo = Confirm.ask( - f"This hyperparam can be executed as sudo or not. Do you want to execute as sudo [y] or not [n]?" + "This hyperparam can be executed as sudo or not. Do you want to execute as sudo [y] or not [n]?" ) if to_sudo_or_not_to_sudo: call = await substrate.compose_call( @@ -830,7 +830,6 @@ async def get_hyperparameters( if show_extended: # Get metadata for this hyperparameter metadata = HYPERPARAMS_METADATA.get(param, {}) - owner_settable = metadata.get("owner_settable", False) description = metadata.get("description", "No description available.") # Check actual ownership from HYPERPARAMS @@ -880,23 +879,23 @@ async def get_hyperparameters( console.print(table) if show_extended: console.print( - f"\n[dim]💡 Tip: Use [bold]btcli sudo set --param --value [/bold] to modify hyperparameters." + "\n[dim]💡 Tip: Use [bold]btcli sudo set --param --value [/bold] to modify hyperparameters." ) console.print( - f"[dim]💡 Tip: Subnet owners can set parameters marked '[green]Yes[/green]'. " - f"Parameters marked '[red]No (Root Only)[/red]' require root sudo access." + "[dim]💡 Tip: Subnet owners can set parameters marked '[green]Yes[/green]'. " + "Parameters marked '[red]No (Root Only)[/red]' require root sudo access." ) console.print( - f"[dim]💡 Tip: To set custom hyperparameters not in this list, use the exact parameter name from the chain metadata." + "[dim]💡 Tip: To set custom hyperparameters not in this list, use the exact parameter name from the chain metadata." ) console.print( f"[dim] Example: [bold]btcli sudo set --netuid {netuid} --param custom_param_name --value 123[/bold]" ) console.print( - f"[dim] The parameter name must match exactly as defined in the chain's AdminUtils pallet metadata." + "[dim] The parameter name must match exactly as defined in the chain's AdminUtils pallet metadata." ) console.print( - f"[dim]📚 For detailed documentation, visit: [link]https://docs.bittensor.com[/link]" + "[dim]📚 For detailed documentation, visit: [link]https://docs.bittensor.com[/link]" ) return True From 11a2c5dfe71da5acb26959905603e536e3c0fa4f Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 15:23:39 +0100 Subject: [PATCH 04/10] Updated JSONDecoder to fix lint error --- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/commands/sudo.py | 34 +++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6d181dab5..a4080f94c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -919,7 +919,7 @@ class RootSudoOnly(Enum): "description": "Whether the subnet is currently active and operational.", "side_effects": "When inactive, the subnet cannot process requests or participate in network operations. Set via 'btcli subnets start' command.", "owner_settable": True, - "docs_link": "docs.learnbittensor.org/subnets/managing-subnets#subnetisactive", + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#subnetisactive", }, "yuma_version": { "description": "Version of the Yuma consensus mechanism.", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 9714a18c2..a98227946 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,5 +1,6 @@ import asyncio import json +import re from typing import TYPE_CHECKING, Union, Optional, Type from async_substrate_interface import AsyncExtrinsicReceipt @@ -862,19 +863,36 @@ async def get_hyperparameters( table.add_row(" " + param, value, norm_value) else: metadata = HYPERPARAMS_METADATA.get(param, {}) + # Sanitize all string fields for JSON output - remove control characters + description = metadata.get("description", "No description available.") + side_effects = metadata.get("side_effects", "No side effects documented.") + docs_link = metadata.get("docs_link", "") + + # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space + # Then collapse multiple spaces into single space + # This ensures valid JSON output + description = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(description)) + side_effects = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(side_effects)) + docs_link = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(docs_link)) + # Collapse multiple spaces + description = ' '.join(description.split()) + side_effects = ' '.join(side_effects.split()) + docs_link = ' '.join(docs_link.split()) + dict_out.append( { - "hyperparameter": param, + "hyperparameter": str(param), "value": value, "normalized_value": norm_value, - "owner_settable": metadata.get("owner_settable", False), - "description": metadata.get("description", "No description available."), - "side_effects": metadata.get("side_effects", "No side effects documented."), - "docs_link": metadata.get("docs_link", ""), + "owner_settable": bool(metadata.get("owner_settable", False)), + "description": description, + "side_effects": side_effects, + "docs_link": docs_link, } ) if json_output: - json_console.print(json.dumps(dict_out)) + # Use ensure_ascii=True to properly escape all non-ASCII and control characters + json_console.print(json.dumps(dict_out, ensure_ascii=True)) else: console.print(table) if show_extended: @@ -948,7 +966,7 @@ async def get_senate( ) dict_output.append({"name": member_name, "ss58_address": ss58_address}) if json_output: - json_console.print(json.dumps(dict_output)) + json_console.print(json.dumps(dict_output, ensure_ascii=True)) return console.print(table) @@ -1041,7 +1059,7 @@ async def proposals( } ) if json_output: - json_console.print(json.dumps(dict_output)) + json_console.print(json.dumps(dict_output, ensure_ascii=True)) console.print(table) console.print( "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]" From 1b3244aef031b52fd5a651f5f956c8e71b11bf06 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 15:38:33 +0100 Subject: [PATCH 05/10] Fixed ruff and e2e error --- bittensor_cli/cli.py | 39 ++++++++++++--------- bittensor_cli/src/__init__.py | 7 ++-- bittensor_cli/src/commands/sudo.py | 32 ++++++++++------- tests/e2e_tests/test_hyperparams_setting.py | 20 ++++++++--- tests/e2e_tests/test_staking_sudo.py | 36 ++++++++++++++----- 5 files changed, 89 insertions(+), 45 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7ee235f52..349c93162 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6369,15 +6369,15 @@ def sudo_set( Used to set hyperparameters for a specific subnet. This command allows subnet owners to modify hyperparameters such as its tempo, emission rates, and other hyperparameters. - + When listing hyperparameters, descriptions, ownership information, and side-effects are displayed to help you make informed decisions. - + You can also set custom hyperparameters not in the standard list by using the exact parameter name from the chain metadata. EXAMPLE [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 - + [green]$[/green] btcli sudo set --netuid 1 --param custom_param_name --value 123 """ self.verbosity_handler(quiet, verbose, json_output, prompt) @@ -6402,10 +6402,11 @@ def sudo_set( [field.name for field in fields(SubnetHyperparameters)] ) console.print("Available hyperparameters:\n") - + # Create a table to show hyperparameters with descriptions from rich.table import Table, Column from rich import box + param_table = Table( Column("[white]#", style="dim", width=4), Column("[white]HYPERPARAMETER", style=COLORS.SU.HYPERPARAMETER), @@ -6415,11 +6416,11 @@ def sudo_set( show_edge=False, pad_edge=False, ) - + for idx, param in enumerate(hyperparam_list, start=1): metadata = HYPERPARAMS_METADATA.get(param, {}) description = metadata.get("description", "No description available.") - + # Check ownership from HYPERPARAMS _, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE)) if root_sudo == RootSudoOnly.TRUE: @@ -6428,44 +6429,50 @@ def sudo_set( owner_settable_str = "[yellow]Maybe[/yellow]" else: owner_settable_str = "[green]Yes[/green]" - + param_table.add_row( str(idx), f"[bold]{param}[/bold]", owner_settable_str, description, ) - + console.print(param_table) console.print() - + choice = IntPrompt.ask( "Enter the [bold]number[/bold] of the hyperparameter", choices=[str(i) for i in range(1, len(hyperparam_list) + 1)], show_choices=False, ) param_name = hyperparam_list[choice - 1] - + # Show additional info for selected parameter metadata = HYPERPARAMS_METADATA.get(param_name, {}) if metadata: console.print(f"\n[bold cyan]Selected:[/bold cyan] {param_name}") - description = metadata.get('description', 'No description available.') - docs_link = metadata.get('docs_link', '') + description = metadata.get("description", "No description available.") + docs_link = metadata.get("docs_link", "") if docs_link: # Show description text followed by clickable blue [link] at the end # Use Rich Text to create clickable [link] with blue color from rich.text import Text + desc_text = Text(f"{description} ") - desc_text.append("[link]", style=f"link https://{docs_link} bright_blue underline") + desc_text.append( + "[link]", + style=f"link https://{docs_link} bright_blue underline", + ) console.print(desc_text) else: console.print(f"{description}") - side_effects = metadata.get('side_effects', '') + side_effects = metadata.get("side_effects", "") if side_effects: console.print(f"[dim]Side Effects:[/dim] {side_effects}") if docs_link: - console.print(f"[dim]📚 Docs:[/dim] [link]https://{docs_link}[/link]\n") + console.print( + f"[dim]📚 Docs:[/dim] [link]https://{docs_link}[/link]\n" + ) if param_name in ["alpha_high", "alpha_low"]: if not prompt: @@ -6564,7 +6571,7 @@ def sudo_get( ): """ Shows a list of the hyperparameters for the specified subnet. - + Displays hyperparameter values along with descriptions, ownership information (which parameters can be set by subnet owners vs root sudo), and side-effects. EXAMPLE diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a4080f94c..4bd5cc599 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -687,7 +687,10 @@ class RootSudoOnly(Enum): # They are derived or set via other mechanisms "alpha_high": ("", RootSudoOnly.FALSE), # Derived from alpha_values "alpha_low": ("", RootSudoOnly.FALSE), # Derived from alpha_values - "max_weight_limit": ("sudo_set_max_weight_limit", RootSudoOnly.FALSE), # Alias for max_weights_limit + "max_weight_limit": ( + "sudo_set_max_weight_limit", + RootSudoOnly.FALSE, + ), # Alias for max_weights_limit "subnet_is_active": ("", RootSudoOnly.FALSE), # Set via btcli subnets start "yuma_version": ("", RootSudoOnly.FALSE), # Related to yuma3_enabled } @@ -879,7 +882,7 @@ class RootSudoOnly(Enum): "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#userliquidityenabled", }, "bonds_reset_enabled": { - "description": "Enable or disable periodic bond resets..", + "description": "Enable or disable periodic bond resets.", "side_effects": "Enabling provides periodic bond resets, preventing bond accumulation. Disabling allows bonds to accumulate.", "owner_settable": True, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#bondsresetenabled", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index a98227946..8291301b8 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -771,7 +771,10 @@ async def sudo_set_hyperparameter( async def get_hyperparameters( - subtensor: "SubtensorInterface", netuid: int, json_output: bool = False, show_descriptions: bool = True + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, + show_descriptions: bool = True, ) -> bool: """View hyperparameters of a subnetwork.""" print_verbose("Fetching hyperparameters") @@ -832,7 +835,7 @@ async def get_hyperparameters( # Get metadata for this hyperparameter metadata = HYPERPARAMS_METADATA.get(param, {}) description = metadata.get("description", "No description available.") - + # Check actual ownership from HYPERPARAMS _, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE)) if root_sudo == RootSudoOnly.TRUE: @@ -841,17 +844,20 @@ async def get_hyperparameters( owner_settable_str = "[yellow]Maybe (Owner/Sudo)[/yellow]" else: owner_settable_str = "[green]Yes[/green]" - + # Format description with docs link if available docs_link = metadata.get("docs_link", "") if docs_link: # Use Rich Text to create description with clickable bright blue [link] at the end description_text = Text(f"{description} ") - description_text.append("[link]", style=f"link https://{docs_link} bright_blue underline") + description_text.append( + "[link]", + style=f"link https://{docs_link} bright_blue underline", + ) description_with_link = description_text else: description_with_link = description - + table.add_row( " " + param, value, @@ -867,18 +873,18 @@ async def get_hyperparameters( description = metadata.get("description", "No description available.") side_effects = metadata.get("side_effects", "No side effects documented.") docs_link = metadata.get("docs_link", "") - + # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space # Then collapse multiple spaces into single space # This ensures valid JSON output - description = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(description)) - side_effects = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(side_effects)) - docs_link = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(docs_link)) + description = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(description)) + side_effects = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(side_effects)) + docs_link = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(docs_link)) # Collapse multiple spaces - description = ' '.join(description.split()) - side_effects = ' '.join(side_effects.split()) - docs_link = ' '.join(docs_link.split()) - + description = " ".join(description.split()) + side_effects = " ".join(side_effects.split()) + docs_link = " ".join(docs_link.split()) + dict_out.append( { "hyperparameter": str(param), diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 765e58af1..5775ff2f5 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -84,12 +84,22 @@ def test_hyperparams_setting(local_chain, wallet_setup): for hyperparam in all_hyperparams: hp[hyperparam["hyperparameter"]] = hyperparam["value"] # Verify new metadata fields are present in JSON output - assert "description" in hyperparam, f"Missing description for {hyperparam['hyperparameter']}" - assert "side_effects" in hyperparam, f"Missing side_effects for {hyperparam['hyperparameter']}" - assert "owner_settable" in hyperparam, f"Missing owner_settable for {hyperparam['hyperparameter']}" - assert "docs_link" in hyperparam, f"Missing docs_link for {hyperparam['hyperparameter']}" + assert "description" in hyperparam, ( + f"Missing description for {hyperparam['hyperparameter']}" + ) + assert "side_effects" in hyperparam, ( + f"Missing side_effects for {hyperparam['hyperparameter']}" + ) + assert "owner_settable" in hyperparam, ( + f"Missing owner_settable for {hyperparam['hyperparameter']}" + ) + assert "docs_link" in hyperparam, ( + f"Missing docs_link for {hyperparam['hyperparameter']}" + ) # Verify description is not empty (unless it's a parameter without metadata) - assert isinstance(hyperparam["description"], str), f"Description should be string for {hyperparam['hyperparameter']}" + assert isinstance(hyperparam["description"], str), ( + f"Description should be string for {hyperparam['hyperparameter']}" + ) for key, (_, sudo_only) in HYPERPARAMS.items(): if key in hp.keys() and sudo_only == RootSudoOnly.FALSE: if isinstance(hp[key], bool): diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index e19396eae..e76ff1627 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -501,11 +501,21 @@ def line(key: str) -> Union[str, bool]: all_hyperparams = json.loads(hyperparams.stdout) # Verify new metadata fields are present in JSON output for hyperparam in all_hyperparams: - assert "description" in hyperparam, f"Missing description for {hyperparam['hyperparameter']}" - assert "side_effects" in hyperparam, f"Missing side_effects for {hyperparam['hyperparameter']}" - assert "owner_settable" in hyperparam, f"Missing owner_settable for {hyperparam['hyperparameter']}" - assert "docs_link" in hyperparam, f"Missing docs_link for {hyperparam['hyperparameter']}" - assert isinstance(hyperparam["description"], str), f"Description should be string for {hyperparam['hyperparameter']}" + assert "description" in hyperparam, ( + f"Missing description for {hyperparam['hyperparameter']}" + ) + assert "side_effects" in hyperparam, ( + f"Missing side_effects for {hyperparam['hyperparameter']}" + ) + assert "owner_settable" in hyperparam, ( + f"Missing owner_settable for {hyperparam['hyperparameter']}" + ) + assert "docs_link" in hyperparam, ( + f"Missing docs_link for {hyperparam['hyperparameter']}" + ) + assert isinstance(hyperparam["description"], str), ( + f"Description should be string for {hyperparam['hyperparameter']}" + ) max_burn_tao = next( filter(lambda x: x["hyperparameter"] == "max_burn", all_hyperparams) )["value"] @@ -606,10 +616,18 @@ def line(key: str) -> Union[str, bool]: lambda x: x["hyperparameter"] == "max_burn", updated_hyperparams_json_output ) ) - assert "description" in max_burn_updated, "Missing description for max_burn after update" - assert "side_effects" in max_burn_updated, "Missing side_effects for max_burn after update" - assert "owner_settable" in max_burn_updated, "Missing owner_settable for max_burn after update" - assert "docs_link" in max_burn_updated, "Missing docs_link for max_burn after update" + assert "description" in max_burn_updated, ( + "Missing description for max_burn after update" + ) + assert "side_effects" in max_burn_updated, ( + "Missing side_effects for max_burn after update" + ) + assert "owner_settable" in max_burn_updated, ( + "Missing owner_settable for max_burn after update" + ) + assert "docs_link" in max_burn_updated, ( + "Missing docs_link for max_burn after update" + ) max_burn_tao_from_json = max_burn_updated["value"] assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(10.0) From 3a3a80cd7838ca434e2759fe88862f661ce2b4ec Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 16:01:07 +0100 Subject: [PATCH 06/10] Fixed review --- bittensor_cli/cli.py | 14 ++------- bittensor_cli/src/commands/sudo.py | 50 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 349c93162..d1f4ebf81 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6404,8 +6404,6 @@ def sudo_set( console.print("Available hyperparameters:\n") # Create a table to show hyperparameters with descriptions - from rich.table import Table, Column - from rich import box param_table = Table( Column("[white]#", style="dim", width=4), @@ -6426,7 +6424,7 @@ def sudo_set( if root_sudo == RootSudoOnly.TRUE: owner_settable_str = "[red]No (Root Only)[/red]" elif root_sudo == RootSudoOnly.COMPLICATED: - owner_settable_str = "[yellow]Maybe[/yellow]" + owner_settable_str = "[yellow]COMPLICATED[/yellow]" else: owner_settable_str = "[green]Yes[/green]" @@ -6455,15 +6453,9 @@ def sudo_set( docs_link = metadata.get("docs_link", "") if docs_link: # Show description text followed by clickable blue [link] at the end - # Use Rich Text to create clickable [link] with blue color - from rich.text import Text - - desc_text = Text(f"{description} ") - desc_text.append( - "[link]", - style=f"link https://{docs_link} bright_blue underline", + console.print( + f"{description} [bright_blue underline link=https://{docs_link}]link[/]" ) - console.print(desc_text) else: console.print(f"{description}") side_effects = metadata.get("side_effects", "") diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8291301b8..f52696745 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -8,7 +8,6 @@ from rich import box from rich.table import Column, Table from rich.prompt import Confirm -from rich.text import Text from scalecodec import GenericCall from bittensor_cli.src import ( @@ -770,6 +769,20 @@ async def sudo_set_hyperparameter( return success, err_msg, ext_id +def _sanitize_json_string(value: Union[str, int, float, bool, None]) -> Union[str, int, float, bool, None]: + """Sanitize string values for JSON output by removing control characters. + + Non-string values are returned as-is. + """ + if isinstance(value, str): + # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space + sanitized = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", value) + # Collapse multiple spaces into single space + sanitized = " ".join(sanitized.split()) + return sanitized + return value + + async def get_hyperparameters( subtensor: "SubtensorInterface", netuid: int, @@ -841,20 +854,17 @@ async def get_hyperparameters( if root_sudo == RootSudoOnly.TRUE: owner_settable_str = "[red]No (Root Only)[/red]" elif root_sudo == RootSudoOnly.COMPLICATED: - owner_settable_str = "[yellow]Maybe (Owner/Sudo)[/yellow]" + owner_settable_str = "[yellow]COMPLICATED (Owner/Sudo)[/yellow]" else: owner_settable_str = "[green]Yes[/green]" # Format description with docs link if available docs_link = metadata.get("docs_link", "") if docs_link: - # Use Rich Text to create description with clickable bright blue [link] at the end - description_text = Text(f"{description} ") - description_text.append( - "[link]", - style=f"link https://{docs_link} bright_blue underline", + # Use Rich markup to create description with clickable bright blue [link] at the end + description_with_link = ( + f"{description} [bright_blue underline link=https://{docs_link}]link[/]" ) - description_with_link = description_text else: description_with_link = description @@ -874,26 +884,16 @@ async def get_hyperparameters( side_effects = metadata.get("side_effects", "No side effects documented.") docs_link = metadata.get("docs_link", "") - # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space - # Then collapse multiple spaces into single space - # This ensures valid JSON output - description = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(description)) - side_effects = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(side_effects)) - docs_link = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(docs_link)) - # Collapse multiple spaces - description = " ".join(description.split()) - side_effects = " ".join(side_effects.split()) - docs_link = " ".join(docs_link.split()) - + # Sanitize all string values to ensure valid JSON output dict_out.append( { - "hyperparameter": str(param), - "value": value, - "normalized_value": norm_value, + "hyperparameter": _sanitize_json_string(str(param)), + "value": _sanitize_json_string(value), + "normalized_value": _sanitize_json_string(norm_value), "owner_settable": bool(metadata.get("owner_settable", False)), - "description": description, - "side_effects": side_effects, - "docs_link": docs_link, + "description": _sanitize_json_string(description), + "side_effects": _sanitize_json_string(side_effects), + "docs_link": _sanitize_json_string(docs_link), } ) if json_output: From c5b5cb1aea4e50c1286e11e676343b2daea613b6 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 16:01:07 +0100 Subject: [PATCH 07/10] Fix JSON sanitization and simplify Rich markup for links - Add _sanitize_json_string helper to sanitize control characters in JSON output - Simplify Rich link markup using direct syntax instead of Text.append - Change 'Maybe' to 'COMPLICATED' for clearer hyperparameter ownership display - Remove unused Text import --- bittensor_cli/cli.py | 14 ++------- bittensor_cli/src/commands/sudo.py | 50 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 349c93162..d1f4ebf81 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6404,8 +6404,6 @@ def sudo_set( console.print("Available hyperparameters:\n") # Create a table to show hyperparameters with descriptions - from rich.table import Table, Column - from rich import box param_table = Table( Column("[white]#", style="dim", width=4), @@ -6426,7 +6424,7 @@ def sudo_set( if root_sudo == RootSudoOnly.TRUE: owner_settable_str = "[red]No (Root Only)[/red]" elif root_sudo == RootSudoOnly.COMPLICATED: - owner_settable_str = "[yellow]Maybe[/yellow]" + owner_settable_str = "[yellow]COMPLICATED[/yellow]" else: owner_settable_str = "[green]Yes[/green]" @@ -6455,15 +6453,9 @@ def sudo_set( docs_link = metadata.get("docs_link", "") if docs_link: # Show description text followed by clickable blue [link] at the end - # Use Rich Text to create clickable [link] with blue color - from rich.text import Text - - desc_text = Text(f"{description} ") - desc_text.append( - "[link]", - style=f"link https://{docs_link} bright_blue underline", + console.print( + f"{description} [bright_blue underline link=https://{docs_link}]link[/]" ) - console.print(desc_text) else: console.print(f"{description}") side_effects = metadata.get("side_effects", "") diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8291301b8..f52696745 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -8,7 +8,6 @@ from rich import box from rich.table import Column, Table from rich.prompt import Confirm -from rich.text import Text from scalecodec import GenericCall from bittensor_cli.src import ( @@ -770,6 +769,20 @@ async def sudo_set_hyperparameter( return success, err_msg, ext_id +def _sanitize_json_string(value: Union[str, int, float, bool, None]) -> Union[str, int, float, bool, None]: + """Sanitize string values for JSON output by removing control characters. + + Non-string values are returned as-is. + """ + if isinstance(value, str): + # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space + sanitized = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", value) + # Collapse multiple spaces into single space + sanitized = " ".join(sanitized.split()) + return sanitized + return value + + async def get_hyperparameters( subtensor: "SubtensorInterface", netuid: int, @@ -841,20 +854,17 @@ async def get_hyperparameters( if root_sudo == RootSudoOnly.TRUE: owner_settable_str = "[red]No (Root Only)[/red]" elif root_sudo == RootSudoOnly.COMPLICATED: - owner_settable_str = "[yellow]Maybe (Owner/Sudo)[/yellow]" + owner_settable_str = "[yellow]COMPLICATED (Owner/Sudo)[/yellow]" else: owner_settable_str = "[green]Yes[/green]" # Format description with docs link if available docs_link = metadata.get("docs_link", "") if docs_link: - # Use Rich Text to create description with clickable bright blue [link] at the end - description_text = Text(f"{description} ") - description_text.append( - "[link]", - style=f"link https://{docs_link} bright_blue underline", + # Use Rich markup to create description with clickable bright blue [link] at the end + description_with_link = ( + f"{description} [bright_blue underline link=https://{docs_link}]link[/]" ) - description_with_link = description_text else: description_with_link = description @@ -874,26 +884,16 @@ async def get_hyperparameters( side_effects = metadata.get("side_effects", "No side effects documented.") docs_link = metadata.get("docs_link", "") - # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space - # Then collapse multiple spaces into single space - # This ensures valid JSON output - description = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(description)) - side_effects = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(side_effects)) - docs_link = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", str(docs_link)) - # Collapse multiple spaces - description = " ".join(description.split()) - side_effects = " ".join(side_effects.split()) - docs_link = " ".join(docs_link.split()) - + # Sanitize all string values to ensure valid JSON output dict_out.append( { - "hyperparameter": str(param), - "value": value, - "normalized_value": norm_value, + "hyperparameter": _sanitize_json_string(str(param)), + "value": _sanitize_json_string(value), + "normalized_value": _sanitize_json_string(norm_value), "owner_settable": bool(metadata.get("owner_settable", False)), - "description": description, - "side_effects": side_effects, - "docs_link": docs_link, + "description": _sanitize_json_string(description), + "side_effects": _sanitize_json_string(side_effects), + "docs_link": _sanitize_json_string(docs_link), } ) if json_output: From e2c83bc0082e50a175e3d260d98ce2e919062b15 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 19:11:13 +0100 Subject: [PATCH 08/10] Fix JSON output and e2e test issues for hyperparameter commands - Fix empty JSON output for special hyperparameters (alpha_high, alpha_low, yuma_version, subnet_is_active) when used with --no-prompt flag - Add proper JSON error responses for unsupported parameter combinations - Fix Rich console initialization to disable colors/ANSI codes in pytest environment to prevent JSON parsing errors - Fix test_hyperparams_setting to skip parameters that cannot be set with --no-prompt (alpha_high, alpha_low, subnet_is_active, yuma_version) - Fix test loop structure to prevent duplicate parameter setting attempts - Fix Ruff formatting issues across multiple files - Ensure all JSON output uses sys.stdout.write() directly to avoid Rich formatting interference --- bittensor_cli/cli.py | 117 +++++++++++++++----- bittensor_cli/src/__init__.py | 4 +- bittensor_cli/src/bittensor/utils.py | 16 ++- bittensor_cli/src/commands/sudo.py | 55 +++++++-- tests/e2e_tests/test_hyperparams_setting.py | 7 +- 5 files changed, 156 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d1f4ebf81..ff5b83f80 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6468,11 +6468,23 @@ def sudo_set( if param_name in ["alpha_high", "alpha_low"]: if not prompt: - err_console.print( - f"[{COLORS.SU.HYPERPARAM}]alpha_high[/{COLORS.SU.HYPERPARAM}] and " - f"[{COLORS.SU.HYPERPARAM}]alpha_low[/{COLORS.SU.HYPERPARAM}] " - f"values cannot be set with `--no-prompt`" + err_msg = ( + f"alpha_high and alpha_low values cannot be set with `--no-prompt`. " + f"They must be set together via the alpha_values parameter." ) + if json_output: + json_str = json.dumps( + {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + err_console.print( + f"[{COLORS.SU.HYPERPARAM}]alpha_high[/{COLORS.SU.HYPERPARAM}] and " + f"[{COLORS.SU.HYPERPARAM}]alpha_low[/{COLORS.SU.HYPERPARAM}] " + f"values cannot be set with `--no-prompt`" + ) return False param_name = "alpha_values" low_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_low')}") @@ -6480,10 +6492,22 @@ def sudo_set( param_value = f"{low_val},{high_val}" if param_name == "yuma_version": if not prompt: - err_console.print( - f"[{COLORS.SU.HYPERPARAM}]yuma_version[/{COLORS.SU.HYPERPARAM}]" - f" is set using a different hyperparameter, and thus cannot be set with `--no-prompt`" + err_msg = ( + "yuma_version is set using a different hyperparameter (yuma3_enabled), " + "and thus cannot be set with `--no-prompt`" ) + if json_output: + json_str = json.dumps( + {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + err_console.print( + f"[{COLORS.SU.HYPERPARAM}]yuma_version[/{COLORS.SU.HYPERPARAM}]" + f" is set using a different hyperparameter, and thus cannot be set with `--no-prompt`" + ) return False if Confirm.ask( f"[{COLORS.SU.HYPERPARAM}]yuma_version[/{COLORS.SU.HYPERPARAM}] can only be used to toggle Yuma 3. " @@ -6498,10 +6522,22 @@ def sudo_set( else: return False if param_name == "subnet_is_active": - err_console.print( - f"[{COLORS.SU.HYPERPARAM}]subnet_is_active[/{COLORS.SU.HYPERPARAM}] " - f"is set by using {arg__('btcli subnets start')} command." + err_msg = ( + "subnet_is_active is set by using the 'btcli subnets start' command, " + "not via sudo set" ) + if json_output: + json_str = json.dumps( + {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + err_console.print( + f"[{COLORS.SU.HYPERPARAM}]subnet_is_active[/{COLORS.SU.HYPERPARAM}] " + f"is set by using {arg__('btcli subnets start')} command." + ) return False if not param_value: @@ -6529,29 +6565,58 @@ def sudo_set( f"param_name: {param_name}\n" f"param_value: {param_value}" ) - result, err_msg, ext_id = self._run_command( - sudo.sudo_set_hyperparameter( - wallet=wallet, - subtensor=self.initialize_chain(network), - netuid=netuid, - proxy=proxy, - param_name=param_name, - param_value=param_value, - prompt=prompt, - json_output=json_output, - ) - ) if json_output: - json_console.print( - json.dumps( + try: + result, err_msg, ext_id = self._run_command( + sudo.sudo_set_hyperparameter( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + param_name=param_name, + param_value=param_value, + prompt=prompt, + json_output=json_output, + ) + ) + json_str = json.dumps( { "success": result, "err_msg": err_msg, "extrinsic_identifier": ext_id, - } + }, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + return result + except Exception as e: + # Ensure JSON output even on exceptions + json_str = json.dumps( + { + "success": False, + "err_msg": str(e), + "extrinsic_identifier": None, + }, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + raise + else: + result, err_msg, ext_id = self._run_command( + sudo.sudo_set_hyperparameter( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + param_name=param_name, + param_value=param_value, + prompt=prompt, + json_output=json_output, ) ) - return result + return result def sudo_get( self, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 4bd5cc599..2bed524c5 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -687,7 +687,7 @@ class RootSudoOnly(Enum): # They are derived or set via other mechanisms "alpha_high": ("", RootSudoOnly.FALSE), # Derived from alpha_values "alpha_low": ("", RootSudoOnly.FALSE), # Derived from alpha_values - "max_weight_limit": ( + "max_weights_limit": ( "sudo_set_max_weight_limit", RootSudoOnly.FALSE, ), # Alias for max_weights_limit @@ -912,7 +912,7 @@ class RootSudoOnly(Enum): "owner_settable": True, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alphasigmoidsteepness", }, - "max_weight_limit": { + "max_weights_limit": { "description": "Maximum number of weight connections a neuron can have with other neurons.", "side_effects": "Limits the maximum out-degree of the network graph, affecting network topology and consensus.", "owner_settable": True, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index ca4b56099..98103078d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -4,6 +4,7 @@ import math import os import sqlite3 +import sys import webbrowser from contextlib import contextmanager from pathlib import Path @@ -42,10 +43,17 @@ GLOBAL_MAX_SUBNET_COUNT = 4096 MEV_SHIELD_PUBLIC_KEY_SIZE = 1184 -console = Console() -json_console = Console() -err_console = Console(stderr=True) -verbose_console = Console(quiet=True) +# Detect if we're in a test environment (pytest captures stdout, making it non-TTY) +# or if NO_COLOR is set, disable colors +# Also check for pytest environment variables +_is_pytest = "pytest" in sys.modules or os.getenv("PYTEST_CURRENT_TEST") is not None +_no_color = os.getenv("NO_COLOR", "") != "" or not sys.stdout.isatty() or _is_pytest +# Force no terminal detection when in pytest or when stdout is not a TTY +_force_terminal = False if (_is_pytest or not sys.stdout.isatty()) else None +console = Console(no_color=_no_color, force_terminal=_force_terminal) +json_console = Console(markup=False, highlight=False, force_terminal=False, no_color=True) +err_console = Console(stderr=True, no_color=_no_color, force_terminal=_force_terminal) +verbose_console = Console(quiet=True, no_color=_no_color, force_terminal=_force_terminal) jinja_env = Environment( loader=PackageLoader("bittensor_cli", "src/bittensor/templates"), diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index f52696745..cdb69f9e2 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,6 +1,7 @@ import asyncio import json import re +import sys from typing import TYPE_CHECKING, Union, Optional, Type from async_substrate_interface import AsyncExtrinsicReceipt @@ -753,7 +754,15 @@ async def sudo_set_hyperparameter( f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. " f"Value is {param_value} but must be {value}" ) - err_console.print(err_msg) + if json_output: + json_str = json.dumps( + {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + ensure_ascii=True, + ) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + err_console.print(err_msg) return False, err_msg, None if json_output: prompt = False @@ -791,14 +800,35 @@ async def get_hyperparameters( ) -> bool: """View hyperparameters of a subnetwork.""" print_verbose("Fetching hyperparameters") - if not await subtensor.subnet_exists(netuid): - print_error(f"Subnet with netuid {netuid} does not exist.") - return False - subnet, subnet_info = await asyncio.gather( - subtensor.get_subnet_hyperparameters(netuid), subtensor.subnet(netuid) - ) - if subnet_info is None: - print_error(f"Subnet with netuid {netuid} does not exist.") + try: + if not await subtensor.subnet_exists(netuid): + error_msg = f"Subnet with netuid {netuid} does not exist." + if json_output: + json_str = json.dumps({"error": error_msg}, ensure_ascii=True) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + print_error(error_msg) + return False + subnet, subnet_info = await asyncio.gather( + subtensor.get_subnet_hyperparameters(netuid), subtensor.subnet(netuid) + ) + if subnet_info is None: + error_msg = f"Subnet with netuid {netuid} does not exist." + if json_output: + json_str = json.dumps({"error": error_msg}, ensure_ascii=True) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + print_error(error_msg) + return False + except Exception as e: + if json_output: + json_str = json.dumps({"error": str(e)}, ensure_ascii=True) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + else: + raise return False # Determine if we should show extended info (descriptions, ownership) @@ -898,7 +928,12 @@ async def get_hyperparameters( ) if json_output: # Use ensure_ascii=True to properly escape all non-ASCII and control characters - json_console.print(json.dumps(dict_out, ensure_ascii=True)) + # Write directly to stdout to avoid any Rich Console formatting + import sys + json_str = json.dumps(dict_out, ensure_ascii=True) + sys.stdout.write(json_str + "\n") + sys.stdout.flush() + return True else: console.print(table) if show_extended: diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 5775ff2f5..679c2c4e5 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -100,8 +100,13 @@ def test_hyperparams_setting(local_chain, wallet_setup): assert isinstance(hyperparam["description"], str), ( f"Description should be string for {hyperparam['hyperparameter']}" ) + + # Skip parameters that cannot be set with --no-prompt + SKIP_PARAMS = {"alpha_high", "alpha_low", "subnet_is_active", "yuma_version"} + for key, (_, sudo_only) in HYPERPARAMS.items(): - if key in hp.keys() and sudo_only == RootSudoOnly.FALSE: + print(f"key: {key}, sudo_only: {sudo_only}") + if key in hp.keys() and sudo_only == RootSudoOnly.FALSE and key not in SKIP_PARAMS: if isinstance(hp[key], bool): new_val = not hp[key] elif isinstance(hp[key], int): From ba97c98d435589c913d314594fd647d1fd980bf8 Mon Sep 17 00:00:00 2001 From: SmartDever02 Date: Thu, 11 Dec 2025 20:08:15 +0100 Subject: [PATCH 09/10] Fixed ruff lint error --- bittensor_cli/cli.py | 18 +++++++++++++++--- bittensor_cli/src/bittensor/utils.py | 8 ++++++-- bittensor_cli/src/commands/sudo.py | 11 ++++++----- tests/e2e_tests/test_hyperparams_setting.py | 10 +++++++--- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ff5b83f80..973ce74bc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6474,7 +6474,11 @@ def sudo_set( ) if json_output: json_str = json.dumps( - {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + { + "success": False, + "err_msg": err_msg, + "extrinsic_identifier": None, + }, ensure_ascii=True, ) sys.stdout.write(json_str + "\n") @@ -6498,7 +6502,11 @@ def sudo_set( ) if json_output: json_str = json.dumps( - {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + { + "success": False, + "err_msg": err_msg, + "extrinsic_identifier": None, + }, ensure_ascii=True, ) sys.stdout.write(json_str + "\n") @@ -6528,7 +6536,11 @@ def sudo_set( ) if json_output: json_str = json.dumps( - {"success": False, "err_msg": err_msg, "extrinsic_identifier": None}, + { + "success": False, + "err_msg": err_msg, + "extrinsic_identifier": None, + }, ensure_ascii=True, ) sys.stdout.write(json_str + "\n") diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 98103078d..bffb4f822 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -51,9 +51,13 @@ # Force no terminal detection when in pytest or when stdout is not a TTY _force_terminal = False if (_is_pytest or not sys.stdout.isatty()) else None console = Console(no_color=_no_color, force_terminal=_force_terminal) -json_console = Console(markup=False, highlight=False, force_terminal=False, no_color=True) +json_console = Console( + markup=False, highlight=False, force_terminal=False, no_color=True +) err_console = Console(stderr=True, no_color=_no_color, force_terminal=_force_terminal) -verbose_console = Console(quiet=True, no_color=_no_color, force_terminal=_force_terminal) +verbose_console = Console( + quiet=True, no_color=_no_color, force_terminal=_force_terminal +) jinja_env = Environment( loader=PackageLoader("bittensor_cli", "src/bittensor/templates"), diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index cdb69f9e2..86f034e67 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -778,9 +778,11 @@ async def sudo_set_hyperparameter( return success, err_msg, ext_id -def _sanitize_json_string(value: Union[str, int, float, bool, None]) -> Union[str, int, float, bool, None]: +def _sanitize_json_string( + value: Union[str, int, float, bool, None], +) -> Union[str, int, float, bool, None]: """Sanitize string values for JSON output by removing control characters. - + Non-string values are returned as-is. """ if isinstance(value, str): @@ -892,9 +894,7 @@ async def get_hyperparameters( docs_link = metadata.get("docs_link", "") if docs_link: # Use Rich markup to create description with clickable bright blue [link] at the end - description_with_link = ( - f"{description} [bright_blue underline link=https://{docs_link}]link[/]" - ) + description_with_link = f"{description} [bright_blue underline link=https://{docs_link}]link[/]" else: description_with_link = description @@ -930,6 +930,7 @@ async def get_hyperparameters( # Use ensure_ascii=True to properly escape all non-ASCII and control characters # Write directly to stdout to avoid any Rich Console formatting import sys + json_str = json.dumps(dict_out, ensure_ascii=True) sys.stdout.write(json_str + "\n") sys.stdout.flush() diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 679c2c4e5..b17dfee86 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -100,13 +100,17 @@ def test_hyperparams_setting(local_chain, wallet_setup): assert isinstance(hyperparam["description"], str), ( f"Description should be string for {hyperparam['hyperparameter']}" ) - + # Skip parameters that cannot be set with --no-prompt SKIP_PARAMS = {"alpha_high", "alpha_low", "subnet_is_active", "yuma_version"} - + for key, (_, sudo_only) in HYPERPARAMS.items(): print(f"key: {key}, sudo_only: {sudo_only}") - if key in hp.keys() and sudo_only == RootSudoOnly.FALSE and key not in SKIP_PARAMS: + if ( + key in hp.keys() + and sudo_only == RootSudoOnly.FALSE + and key not in SKIP_PARAMS + ): if isinstance(hp[key], bool): new_val = not hp[key] elif isinstance(hp[key], int): From 9c02ad35a0cc167c3f9d31c9c4d8424521ef13fd Mon Sep 17 00:00:00 2001 From: aka James4u Date: Fri, 12 Dec 2025 10:57:40 -0800 Subject: [PATCH 10/10] Only owner can set Kappa --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 2bed524c5..b66f2a8eb 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -710,7 +710,7 @@ class RootSudoOnly(Enum): "kappa": { "description": "Kappa determines the scaling factor for consensus calculations.", "side_effects": "Modifying kappa changes how validator votes are weighted in consensus mechanisms.", - "owner_settable": False, + "owner_settable": True, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#kappa", }, "immunity_period": {