From 3402c1535268cf860c5fb91dd5d4e403da4cf45a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 14 Feb 2025 19:00:04 +0200 Subject: [PATCH 1/8] Allows setting hyperparams arbitrarily, as long as they exist in the AdminUtils pallet. --- bittensor_cli/src/commands/stake/add.py | 8 +- bittensor_cli/src/commands/stake/remove.py | 4 +- bittensor_cli/src/commands/subnets/subnets.py | 8 +- bittensor_cli/src/commands/sudo.py | 120 +++++++++++++----- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index cbc1ae24..4aba39e6 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -107,9 +107,7 @@ async def safe_stake_extrinsic( ) return else: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return else: await response.process_events() @@ -180,9 +178,7 @@ async def stake_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) except SubstrateRequestException as e: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return else: await response.process_events() diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index a8d364b5..8d462978 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -666,9 +666,7 @@ async def _safe_unstake_extrinsic( ) return else: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return await response.process_events() diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index e5d397e0..f4e01486 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -889,8 +889,8 @@ async def show_root(): total_emission_per_block = 0 for netuid_ in range(len(all_subnets)): subnet = all_subnets[netuid_] - emission_on_subnet = ( - root_state.emission_history[netuid_][idx] / subnet.tempo + emission_on_subnet = root_state.emission_history[netuid_][idx] / ( + subnet.tempo or 1 ) total_emission_per_block += subnet.alpha_to_tao( Balance.from_rao(emission_on_subnet) @@ -2135,9 +2135,7 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str title = "Subnet Identity" if not await subtensor.subnet_exists(netuid): - print_error( - f"Subnet {netuid} does not exist." - ) + print_error(f"Subnet {netuid} does not exist.") raise typer.Exit() with console.status( diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8530f3f9..64395e67 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -18,6 +18,8 @@ normalize_hyperparameters, unlock_key, blocks_to_duration, + float_to_u64, + float_to_u16, ) if TYPE_CHECKING: @@ -70,6 +72,44 @@ def allowed_value( return True, value +def search_metadata( + param_name: str, netuid: int, metadata +) -> tuple[bool, Optional[dict]]: + """ + Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used + as call parameters for setting this hyperparameter. + + Args: + param_name: the name of the hyperparameter + netuid: the specified netuid + metadata: the subtensor.substrate.metadata + + Returns: + (success, dict of call params) + + """ + arg_types = {"bool": bool, "u16": float_to_u16, "u64": float_to_u64} + + call_crafter = {"netuid": netuid} + + for pallet in metadata.pallets: + if pallet.name == "AdminUtils": + for call in pallet.calls: + if call.name == param_name: + if "netuid" not in [x.name for x in call.args]: + return False, None + for arg in call.args: + if arg.name == "netuid": + continue + raw_val = input( + f"Enter a value for field '{arg.name}' with type '{arg.typeName}'" + ) + call_crafter[arg.name] = arg_types[arg.typeName](raw_val) + return True, call_crafter + else: + return False, None + + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", @@ -110,44 +150,53 @@ async def set_hyperparameter_extrinsic( if not unlock_key(wallet).success: return False + arbitrary_extrinsic = False + extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False)) if extrinsic is None: - err_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") - return False + arbitrary_extrinsic, call_params = search_metadata( + parameter, netuid, subtensor.substrate.metadata + ) + if not arbitrary_extrinsic: + err_console.print( + ":cross_mark: [red]Invalid hyperparameter specified.[/red]" + ) + return False with console.status( f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", spinner="earth", ): - substrate = subtensor.substrate - extrinsic_params = await substrate.get_metadata_call_function( - "AdminUtils", extrinsic - ) - call_params: dict[str, Union[str, bool, float]] = {"netuid": netuid} - - # if input value is a list, iterate through the list and assign values - if isinstance(value, list): - # Ensure that there are enough values for all non-netuid parameters - non_netuid_fields = [ - param["name"] - for param in extrinsic_params["fields"] - if "netuid" not in param["name"] - ] - - if len(value) < len(non_netuid_fields): - raise ValueError( - "Not enough values provided in the list for all parameters" - ) - - call_params.update( - {str(name): val for name, val in zip(non_netuid_fields, value)} + if not arbitrary_extrinsic: + substrate = subtensor.substrate + extrinsic_params = await substrate.get_metadata_call_function( + "AdminUtils", extrinsic ) + call_params = {"netuid": netuid} + + # if input value is a list, iterate through the list and assign values + if isinstance(value, list): + # Ensure that there are enough values for all non-netuid parameters + non_netuid_fields = [ + param["name"] + for param in extrinsic_params["fields"] + if "netuid" not in param["name"] + ] + + if len(value) < len(non_netuid_fields): + raise ValueError( + "Not enough values provided in the list for all parameters" + ) - else: - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] - call_params[str(value_argument["name"])] = value + call_params.update( + {str(name): val for name, val in zip(non_netuid_fields, value)} + ) + + else: + value_argument = extrinsic_params["fields"][ + len(extrinsic_params["fields"]) - 1 + ] + call_params[str(value_argument["name"])] = value # create extrinsic call call_ = await substrate.compose_call( @@ -167,11 +216,16 @@ async def set_hyperparameter_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - + elif arbitrary_extrinsic: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" + ) # Successful registration, final check for membership else: console.print( - f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" ) return True @@ -481,7 +535,9 @@ async def sudo_set_hyperparameter( "commit_reveal_weights_enabled", "liquid_alpha_enabled", ]: - normalized_value = param_value.lower() in ["true", "True", "1"] + normalized_value = param_value.lower() in ["true", "1"] + elif param_value in ("True", "False"): + normalized_value = bool(param_value) else: normalized_value = param_value From a6abc8e051adc4d5ede4090b990ada5fa74e0fd4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 18:23:40 +0200 Subject: [PATCH 2/8] More efficient method. --- bittensor_cli/src/commands/sudo.py | 33 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 64395e67..ca1eb7e5 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -73,7 +73,7 @@ def allowed_value( def search_metadata( - param_name: str, netuid: int, metadata + param_name: str, value: Union[str, bool, float, list[float]], netuid: int, metadata ) -> tuple[bool, Optional[dict]]: """ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used @@ -81,6 +81,7 @@ def search_metadata( Args: param_name: the name of the hyperparameter + value: the value to set the hyperparameter netuid: the specified netuid metadata: the subtensor.substrate.metadata @@ -88,7 +89,19 @@ def search_metadata( (success, dict of call params) """ + + def type_converter_with_retry(type_, val, arg_name): + try: + if val is None: + val = input( + f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}'" + ) + return arg_types[type_](val) + except ValueError: + return type_converter_with_retry(type_, None, arg_name) + arg_types = {"bool": bool, "u16": float_to_u16, "u64": float_to_u64} + arg_type_output = {"bool": bool, "u16": float, "u64": float} call_crafter = {"netuid": netuid} @@ -98,13 +111,17 @@ def search_metadata( if call.name == param_name: if "netuid" not in [x.name for x in call.args]: return False, None - for arg in call.args: - if arg.name == "netuid": - continue - raw_val = input( - f"Enter a value for field '{arg.name}' with type '{arg.typeName}'" + call_args = [arg for arg in call.args if arg.name != "netuid"] + if len(call_args) == 1: + arg = call_args[0] + call_crafter[arg.name] = type_converter_with_retry( + arg.typeName, value, arg.name ) - call_crafter[arg.name] = arg_types[arg.typeName](raw_val) + else: + for arg in call_args: + call_crafter[arg.name] = type_converter_with_retry( + arg.typeName, None, arg.name + ) return True, call_crafter else: return False, None @@ -155,7 +172,7 @@ async def set_hyperparameter_extrinsic( extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False)) if extrinsic is None: arbitrary_extrinsic, call_params = search_metadata( - parameter, netuid, subtensor.substrate.metadata + parameter, value, netuid, subtensor.substrate.metadata ) if not arbitrary_extrinsic: err_console.print( From b8ae46e463d9a8b22e20d876838dc2b091a9887d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 19 Feb 2025 22:12:45 +0200 Subject: [PATCH 3/8] Made it actually work. --- bittensor_cli/cli.py | 5 ++-- bittensor_cli/src/commands/subnets/subnets.py | 4 +-- bittensor_cli/src/commands/sudo.py | 28 +++++++++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 70ff7bd5..012926c0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3229,10 +3229,11 @@ def stake_remove( ask_for=[WO.NAME, WO.PATH], ) else: - if not hotkey_ss58_address and not wallet_hotkey: + if not hotkey_ss58_address and not wallet_hotkey: hotkey_or_ss58 = Prompt.ask( "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim](or enter 'all' to unstake from all hotkeys)[/dim]", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + default=self.config.get("wallet_hotkey") + or defaults.wallet.hotkey, ) else: hotkey_or_ss58 = hotkey_ss58_address or wallet_hotkey diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 39ad2944..f4e01486 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -889,8 +889,8 @@ async def show_root(): total_emission_per_block = 0 for netuid_ in range(len(all_subnets)): subnet = all_subnets[netuid_] - emission_on_subnet = ( - root_state.emission_history[netuid_][idx] / (subnet.tempo or 1) + emission_on_subnet = root_state.emission_history[netuid_][idx] / ( + subnet.tempo or 1 ) total_emission_per_block += subnet.alpha_to_tao( Balance.from_rao(emission_on_subnet) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index ca1eb7e5..446e8071 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -111,16 +111,19 @@ def type_converter_with_retry(type_, val, arg_name): if call.name == param_name: if "netuid" not in [x.name for x in call.args]: return False, None - call_args = [arg for arg in call.args if arg.name != "netuid"] + call_args = [ + arg for arg in call.args if arg.value["name"] != "netuid" + ] if len(call_args) == 1: - arg = call_args[0] - call_crafter[arg.name] = type_converter_with_retry( - arg.typeName, value, arg.name + arg = call_args[0].value + call_crafter[arg["name"]] = type_converter_with_retry( + arg["typeName"], value, arg["name"] ) else: - for arg in call_args: - call_crafter[arg.name] = type_converter_with_retry( - arg.typeName, None, arg.name + for arg_ in call_args: + arg = arg_.value + call_crafter[arg["name"]] = type_converter_with_retry( + arg["typeName"], None, arg["name"] ) return True, call_crafter else: @@ -170,10 +173,11 @@ async def set_hyperparameter_extrinsic( arbitrary_extrinsic = False extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False)) - if extrinsic is None: + if not extrinsic: arbitrary_extrinsic, call_params = search_metadata( parameter, value, netuid, subtensor.substrate.metadata ) + extrinsic = parameter if not arbitrary_extrinsic: err_console.print( ":cross_mark: [red]Invalid hyperparameter specified.[/red]" @@ -184,8 +188,8 @@ async def set_hyperparameter_extrinsic( f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", spinner="earth", ): + substrate = subtensor.substrate if not arbitrary_extrinsic: - substrate = subtensor.substrate extrinsic_params = await substrate.get_metadata_call_function( "AdminUtils", extrinsic ) @@ -554,7 +558,10 @@ async def sudo_set_hyperparameter( ]: normalized_value = param_value.lower() in ["true", "1"] elif param_value in ("True", "False"): - normalized_value = bool(param_value) + normalized_value = { + "True": True, + "False": False, + }[param_value] else: normalized_value = param_value @@ -565,7 +572,6 @@ async def sudo_set_hyperparameter( f"Value is {normalized_value} but must be {value}" ) return - success = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value ) From 3bd79b81325084443a8b4d22d653006c91a99c32 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 19 Feb 2025 22:59:09 +0200 Subject: [PATCH 4/8] Update "network_registration_allowed" to match the SubnetHyperparameters "registration_allowed" --- 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 0a6d09a3..e0156309 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -672,7 +672,7 @@ class WalletValidationTypes(Enum): "commit_reveal_weights_enabled": ("sudo_set_commit_reveal_weights_enabled", False), "alpha_values": ("sudo_set_alpha_values", False), "liquid_alpha_enabled": ("sudo_set_liquid_alpha_enabled", False), - "network_registration_allowed": ("sudo_set_network_registration_allowed", False), + "registration_allowed": ("sudo_set_network_registration_allowed", False), "network_pow_registration_allowed": ( "sudo_set_network_pow_registration_allowed", False, From 87034b25bd24d958658254484e89d053d4877bcb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 19 Feb 2025 23:40:33 +0200 Subject: [PATCH 5/8] Readability improved --- bittensor_cli/cli.py | 12 ++++++++---- bittensor_cli/src/commands/sudo.py | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 012926c0..32d76d73 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -26,6 +26,7 @@ WalletValidationTypes as WV, Constants, COLOR_PALETTE, + HYPERPARAMS, ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance @@ -3984,7 +3985,7 @@ def sudo_set( param_name: str = typer.Option( "", "--param", "--parameter", help="The subnet hyperparameter to set" ), - param_value: str = typer.Option( + param_value: Optional[str] = typer.Option( "", "--value", help="Value to set the hyperparameter to." ), quiet: bool = Options.quiet, @@ -4033,9 +4034,12 @@ def sudo_set( param_value = f"{low_val},{high_val}" if not param_value: - param_value = Prompt.ask( - f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" - ) + if HYPERPARAMS.get(param_name): + param_value = Prompt.ask( + f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" + ) + else: + param_value = None wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 446e8071..77c664ab 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -184,11 +184,15 @@ async def set_hyperparameter_extrinsic( ) return False + substrate = subtensor.substrate + msg_value = value if not arbitrary_extrinsic else call_params with console.status( - f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", + f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{msg_value}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", spinner="earth", ): - substrate = subtensor.substrate if not arbitrary_extrinsic: extrinsic_params = await substrate.get_metadata_call_function( "AdminUtils", extrinsic From 1a91049caebf3cc15f71e2ca56f18c27b01d089c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 19 Feb 2025 23:42:32 +0200 Subject: [PATCH 6/8] Typing. --- bittensor_cli/src/commands/sudo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 77c664ab..f997a572 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -135,7 +135,7 @@ async def set_hyperparameter_extrinsic( wallet: "Wallet", netuid: int, parameter: str, - value: Union[str, bool, float, list[float]], + value: Optional[Union[str, bool, float, list[float]]], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: @@ -549,7 +549,7 @@ async def sudo_set_hyperparameter( subtensor: "SubtensorInterface", netuid: int, param_name: str, - param_value: str, + param_value: Optional[str], ): """Set subnet hyperparameters.""" From ebc24044c83ee0eadd58907d68d2670725048721 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 20 Feb 2025 00:11:41 +0200 Subject: [PATCH 7/8] String to bool conversion. --- bittensor_cli/src/commands/sudo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index f997a572..4026138a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -90,6 +90,12 @@ def search_metadata( """ + def string_to_bool(val) -> bool: + try: + return {"true": True, "1": True, "0": False, "false": False}[val.lower()] + except KeyError: + return ValueError + def type_converter_with_retry(type_, val, arg_name): try: if val is None: @@ -100,8 +106,8 @@ def type_converter_with_retry(type_, val, arg_name): except ValueError: return type_converter_with_retry(type_, None, arg_name) - arg_types = {"bool": bool, "u16": float_to_u16, "u64": float_to_u64} - arg_type_output = {"bool": bool, "u16": float, "u64": float} + arg_types = {"bool": string_to_bool, "u16": float_to_u16, "u64": float_to_u64} + arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"} call_crafter = {"netuid": netuid} From a88531d70209064c51848cc2bb51b310c1e8f7fc Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 20 Feb 2025 00:13:15 +0200 Subject: [PATCH 8/8] Spacing --- bittensor_cli/src/commands/sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4026138a..78106ae3 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -100,7 +100,7 @@ def type_converter_with_retry(type_, val, arg_name): try: if val is None: val = input( - f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}'" + f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}': " ) return arg_types[type_](val) except ValueError: