From 5a9077a59ba5f12ca9f193481609f47c4e83e1f3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 12 Dec 2024 12:46:17 -0500 Subject: [PATCH 1/7] first pass at using `@chia_command` without a group --- chia/_tests/cmds/test_cmd_framework.py | 44 +++++++++++----------- chia/_tests/wallet/test_signer_protocol.py | 10 ++--- chia/cmds/chia.py | 2 - chia/cmds/cmd_classes.py | 32 +++++++++++++++- chia/cmds/plotnft.py | 34 ++++++++--------- chia/cmds/signer.py | 33 ++++++++-------- chia/cmds/wallet.py | 6 +++ 7 files changed, 98 insertions(+), 63 deletions(-) diff --git a/chia/_tests/cmds/test_cmd_framework.py b/chia/_tests/cmds/test_cmd_framework.py index 04a7e6fcf445..e2089dc6fb44 100644 --- a/chia/_tests/cmds/test_cmd_framework.py +++ b/chia/_tests/cmds/test_cmd_framework.py @@ -37,7 +37,7 @@ def new_run(self: Any) -> None: dict_compare_with_ignore_context(asdict(cmd), asdict(self)) # type: ignore[call-overload] setattr(mock_type, "run", new_run) - chia_command(_cmd, "_", "", "")(mock_type) + chia_command(group=_cmd, name="_", short_help="", help="")(mock_type) runner = CliRunner() result = runner.invoke(_cmd, ["_", *args], catch_exceptions=False, obj=obj) @@ -49,12 +49,12 @@ def test_cmd_bases() -> None: def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD: def run(self) -> None: print("syncronous") - @chia_command(cmd, "temp_cmd_async", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_async", short_help="blah", help="n/a") class TempCMDAsync: async def run(self) -> None: print("asyncronous") @@ -96,7 +96,7 @@ def test_option_loading() -> None: def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD: some_option: int = option("-o", "--some-option", required=True, type=int) choices: list[str] = option("--choice", multiple=True, type=str) @@ -104,7 +104,7 @@ class TempCMD: def run(self) -> None: print(self.some_option) - @chia_command(cmd, "temp_cmd_2", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_2", short_help="blah", help="n/a") class TempCMD2: some_option: int = option("-o", "--some-option", required=True, type=int, default=13) @@ -146,7 +146,7 @@ def test_context_requirement() -> None: def cmd(ctx: click.Context) -> None: ctx.obj = {"foo": "bar"} - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD: context: Context @@ -164,7 +164,7 @@ def run(self) -> None: # Test that other variables named context are disallowed with pytest.raises(ValueError, match="context"): - @chia_command(cmd, "shouldnt_work", "blah", help="n/a") + @chia_command(group=cmd, name="shouldnt_work", short_help="blah", help="n/a") class BadCMD: context: int @@ -176,7 +176,7 @@ def test_typing() -> None: def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD: integer: int = option("--integer", default=1, required=False) text: str = option("--text", default="1", required=False) @@ -208,7 +208,7 @@ def run(self) -> None: ... ) # Test optional - @chia_command(cmd, "temp_cmd_optional", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_optional", short_help="blah", help="n/a") class TempCMDOptional: optional: Optional[int] = option("--optional", required=False) @@ -220,7 +220,7 @@ def run(self) -> None: ... # Test optional failure with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_optional_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_optional_bad", short_help="blah", help="n/a") class TempCMDOptionalBad2: optional: Optional[int] = option("--optional", required=True) @@ -228,20 +228,20 @@ def run(self) -> None: ... with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_optional_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_optional_bad", short_help="blah", help="n/a") class TempCMDOptionalBad3: optional: Optional[int] = option("--optional", default="string", required=False) def run(self) -> None: ... - @chia_command(cmd, "temp_cmd_optional_fine", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_optional_fine", short_help="blah", help="n/a") class TempCMDOptionalBad4: optional: Optional[int] = option("--optional", default=None, required=False) def run(self) -> None: ... # Test multiple - @chia_command(cmd, "temp_cmd_sequence", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_sequence", short_help="blah", help="n/a") class TempCMDSequence: sequence: Sequence[int] = option("--sequence", multiple=True) @@ -253,7 +253,7 @@ def run(self) -> None: ... # Test sequence failure with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_sequence_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_sequence_bad", short_help="blah", help="n/a") class TempCMDSequenceBad: sequence: Sequence[int] = option("--sequence") @@ -261,7 +261,7 @@ def run(self) -> None: ... with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_sequence_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_sequence_bad", short_help="blah", help="n/a") class TempCMDSequenceBad2: sequence: int = option("--sequence", multiple=True) @@ -269,7 +269,7 @@ def run(self) -> None: ... with pytest.raises(ValueError): - @chia_command(cmd, "temp_cmd_sequence_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_sequence_bad", short_help="blah", help="n/a") class TempCMDSequenceBad3: sequence: Sequence[int] = option("--sequence", default=[1, 2, 3], multiple=True) @@ -277,7 +277,7 @@ def run(self) -> None: ... with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_sequence_bad", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_sequence_bad", short_help="blah", help="n/a") class TempCMDSequenceBad4: sequence: Sequence[int] = option("--sequence", default=(1, 2, "3"), multiple=True) @@ -286,7 +286,7 @@ def run(self) -> None: ... # Test invalid type with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_bad_type", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_bad_type", short_help="blah", help="n/a") class TempCMDBadType: sequence: list[int] = option("--sequence") @@ -295,20 +295,20 @@ def run(self) -> None: ... # Test invalid default with pytest.raises(TypeError): - @chia_command(cmd, "temp_cmd_bad_default", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_bad_default", short_help="blah", help="n/a") class TempCMDBadDefault: integer: int = option("--int", default="string") def run(self) -> None: ... # Test bytes parsing - @chia_command(cmd, "temp_cmd_bad_bytes", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_bad_bytes", short_help="blah", help="n/a") class TempCMDBadBytes: blob: bytes = option("--blob", required=True) def run(self) -> None: ... - @chia_command(cmd, "temp_cmd_bad_bytes32", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd_bad_bytes32", short_help="blah", help="n/a") class TempCMDBadBytes32: blob32: bytes32 = option("--blob32", required=True) @@ -354,7 +354,7 @@ async def test_wallet_rpc_helper(wallet_environments: WalletTestFramework) -> No def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD: rpc_info: NeedsWalletRPC diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index 83552b210c56..f7f22a8b7e6a 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -752,7 +752,7 @@ def test_transactions_in() -> None: def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD(TransactionsIn): def run(self) -> None: assert self.transaction_bundle == TransactionBundle([STD_TX]) @@ -771,7 +771,7 @@ def test_transactions_out() -> None: def cmd() -> None: pass - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD(TransactionsOut): def run(self) -> None: self.handle_transaction_output([STD_TX]) @@ -824,7 +824,7 @@ def cmd() -> None: coin = Coin(bytes32.zeros, bytes32.zeros, uint64(13)) - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD(SPIn): def run(self) -> None: assert self.read_sp_input(Coin) == [coin, coin] @@ -881,7 +881,7 @@ def cmd() -> None: coin = Coin(bytes32.zeros, bytes32.zeros, uint64(0)) coin_bytes = byte_serialize_clvm_streamable(coin) - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD(SPOut): def run(self) -> None: self.handle_clvm_output([coin, coin]) @@ -930,7 +930,7 @@ def cmd() -> None: bytes_to_encode = b"foo bar qat qux bam bat" - @chia_command(cmd, "temp_cmd", "blah", help="n/a") + @chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a") class TempCMD(QrCodeDisplay): def run(self) -> None: self.display_qr_codes([bytes_to_encode, bytes_to_encode]) diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 3d323505f07f..543002d1d743 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -143,8 +143,6 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: def main() -> None: - import chia.cmds.signer # noqa - cli() diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 0945fe3d781d..e58ae1cf66d2 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -227,7 +227,8 @@ def _convert_class_to_function(cls: type[ChiaCommand]) -> SyncCmd: @dataclass_transform() def chia_command( - cmd: click.Group, + *, + group: Optional[click.Group] = None, name: str, short_help: str, help: str, @@ -246,12 +247,39 @@ def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: kw_only=True, )(cls) - cmd.command(name, short_help=short_help, help=help)(_convert_class_to_function(wrapped_cls)) + metadata = Metadata( + command=click.command( + name=name, + short_help=short_help, + help=help, + )(_convert_class_to_function(wrapped_cls)) + ) + + setattr(wrapped_cls, _chia_command_metadata_attribute, metadata) + if group is not None: + group.add_command(metadata.command) + return wrapped_cls return _chia_command +_chia_command_metadata_attribute = f"_{__name__.replace('.', '_')}_{chia_command.__qualname__}_metadata" + + +@dataclass(frozen=True, slots=True) +class Metadata: + command: click.Command + + +def get_chia_command_metadata(cls: type[ChiaCommand]) -> Metadata: + metadata: Optional[Metadata] = getattr(cls, _chia_command_metadata_attribute, None) + if metadata is None: + raise Exception(f"Class is not a chia command: {cls}") + + return metadata + + @dataclass_transform() def command_helper(cls: type[Any]) -> type[Any]: if sys.version_info < (3, 10): # stuff below 3.10 doesn't support kw_only diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 20e47c9e052a..a4e1fef36b3b 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -24,9 +24,9 @@ def plotnft_cmd(ctx: click.Context) -> None: @chia_command( - plotnft_cmd, - "show", - "Show plotnft information", + group=plotnft_cmd, + name="show", + short_help="Show plotnft information", help="Show plotnft information", ) class ShowPlotNFTCMD: @@ -48,8 +48,8 @@ async def run(self) -> None: @chia_command( - plotnft_cmd, - "get_login_link", + group=plotnft_cmd, + name="get_login_link", short_help="Create a login link for a pool", help="Create a login link for a pool. The farmer must be running. Use 'plotnft show' to get the launcher id.", ) @@ -69,8 +69,8 @@ async def run(self) -> None: # They will therefore not work with observer-only functionality # NOTE: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @chia_command( - plotnft_cmd, - "create", + group=plotnft_cmd, + name="create", short_help="Create a plot NFT", help="Create a plot NFT.", ) @@ -116,8 +116,8 @@ async def run(self) -> None: # NOTE: tx_endpoint @chia_command( - plotnft_cmd, - "join", + group=plotnft_cmd, + name="join", short_help="Join a plot NFT to a Pool", help="Join a plot NFT to a Pool.", ) @@ -153,8 +153,8 @@ async def run(self) -> None: # NOTE: tx_endpoint @chia_command( - plotnft_cmd, - "leave", + group=plotnft_cmd, + name="leave", short_help="Leave a pool and return to self-farming", help="Leave a pool and return to self-farming.", ) @@ -187,8 +187,8 @@ async def run(self) -> None: @chia_command( - plotnft_cmd, - "inspect", + group=plotnft_cmd, + name="inspect", short_help="Get Detailed plotnft information as JSON", help="Get Detailed plotnft information as JSON", ) @@ -207,8 +207,8 @@ async def run(self) -> None: # NOTE: tx_endpoint @chia_command( - plotnft_cmd, - "claim", + group=plotnft_cmd, + name="claim", short_help="Claim rewards from a plot NFT", help="Claim rewards from a plot NFT", ) @@ -239,8 +239,8 @@ async def run(self) -> None: @chia_command( - plotnft_cmd, - "change_payout_instructions", + group=plotnft_cmd, + name="change_payout_instructions", short_help="Change the payout instructions for a pool.", help="Change the payout instructions for a pool. Use 'plotnft show' to get the launcher id.", ) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index b5afcd803a10..42e25e2227a2 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -17,7 +17,6 @@ from chia.cmds.cmd_classes import NeedsWalletRPC, chia_command, command_helper, option from chia.cmds.cmds_util import TransactionBundle -from chia.cmds.wallet import wallet_cmd from chia.rpc.util import ALL_TRANSLATION_LAYERS from chia.rpc.wallet_request_types import ( ApplySignatures, @@ -38,7 +37,7 @@ def _clear_screen() -> None: os.system("cls" if os.name == "nt" else "clear") -@wallet_cmd.group("signer", help="Get information for an external signer") +@click.group("signer", help="Get information for an external signer") def signer_cmd() -> None: pass # pragma: no cover @@ -209,10 +208,10 @@ def handle_clvm_output(self, outputs: list[Streamable]) -> None: @chia_command( - signer_cmd, - "gather_signing_info", - "gather signer information", - "Gather the information from a transaction that a signer needs in order to create a signature", + group=signer_cmd, + name="gather_signing_info", + short_help="gather signer information", + help="Gather the information from a transaction that a signer needs in order to create a signature", ) class GatherSigningInfoCMD: sp_out: SPOut @@ -233,7 +232,12 @@ async def run(self) -> None: self.sp_out.handle_clvm_output([signing_instructions]) -@chia_command(signer_cmd, "apply_signatures", "apply signatures", "Apply a signer's signatures to a transaction bundle") +@chia_command( + group=signer_cmd, + name="apply_signatures", + short_help="apply signatures", + help="Apply a signer's signatures to a transaction bundle", +) class ApplySignaturesCMD: txs_out: TransactionsOut sp_in: SPIn @@ -272,10 +276,10 @@ async def run(self) -> None: @chia_command( - signer_cmd, - "execute_signing_instructions", - "execute signing instructions", - "Given some signing instructions, return signing responses", + group=signer_cmd, + name="execute_signing_instructions", + short_help="execute signing instructions", + help="Given some signing instructions, return signing responses", ) class ExecuteSigningInstructionsCMD: sp_out: SPOut @@ -299,10 +303,9 @@ async def run(self) -> None: @chia_command( - wallet_cmd, - "push_transactions", - "push transaction bundle", - "Push a transaction bundle to the wallet to send to the network", + name="push_transactions", + short_help="push transaction bundle", + help="Push a transaction bundle to the wallet to send to the network", ) class PushTransactionsCMD: txs_in: TransactionsIn diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index aef8f2a80029..f9642368616c 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -9,6 +9,7 @@ from chia.cmds import options from chia.cmds.check_wallet_db import help_text as check_help_text +from chia.cmds.cmd_classes import get_chia_command_metadata from chia.cmds.cmds_util import timelock_args, tx_out_cmd from chia.cmds.coins import coins_cmd from chia.cmds.param_types import ( @@ -19,6 +20,7 @@ CliAmount, cli_amount_none, ) +from chia.cmds.signer import PushTransactionsCMD, signer_cmd from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.conditions import ConditionValidTimes @@ -34,6 +36,10 @@ def wallet_cmd(ctx: click.Context) -> None: pass +wallet_cmd.add_command(signer_cmd) +wallet_cmd.add_command(get_chia_command_metadata(PushTransactionsCMD).command) + + @wallet_cmd.command("get_transaction", help="Get a transaction") @click.option( "-wp", From 07e22d7244027e88a2e21257600c78d38f41716c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 12 Dec 2024 14:01:48 -0500 Subject: [PATCH 2/7] less slots --- chia/cmds/cmd_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index e58ae1cf66d2..597158e0edc8 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -267,7 +267,7 @@ def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: _chia_command_metadata_attribute = f"_{__name__.replace('.', '_')}_{chia_command.__qualname__}_metadata" -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True) class Metadata: command: click.Command From 302e6913b91f91f6d02120a3294d67f7968cabc2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 13 Dec 2024 10:33:24 -0500 Subject: [PATCH 3/7] both options --- chia/cmds/chia.py | 2 ++ chia/cmds/cmd_classes.py | 31 +++++++++++++++++- chia/cmds/services.py | 57 ++++++++++++++++++++++++++++++++++ chia/server/start_full_node.py | 11 ++----- chia/util/config.py | 3 +- pyproject.toml | 2 +- 6 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 chia/cmds/services.py diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 543002d1d743..edd1ac712235 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -23,6 +23,7 @@ from chia.cmds.plots import plots_cmd from chia.cmds.plotters import plotters_cmd from chia.cmds.rpc import rpc_cmd +from chia.cmds.services import services_group from chia.cmds.show import show_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd @@ -140,6 +141,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: cli.add_command(completion) cli.add_command(dao_cmd) cli.add_command(dev_cmd) +cli.add_command(services_group) def main() -> None: diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 597158e0edc8..ac15bc2258ec 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -43,6 +43,26 @@ def option(*param_decls: str, **kwargs: Any) -> Any: return field( metadata=dict( + click_call=click.option, + option_args=dict( + param_decls=tuple(param_decls), + **kwargs, + ), + ), + default=kwargs.get("default", default_default), + ) + + +def argument(*param_decls: str, **kwargs: Any) -> Any: + if sys.version_info < (3, 10): # versions < 3.10 don't know about kw_only and they complain about lacks of defaults + # Can't get coverage on this because we only test on one version + default_default = None # pragma: no cover + else: + default_default = MISSING + + return field( + metadata=dict( + click_call=click.argument, option_args=dict( param_decls=tuple(param_decls), **kwargs, @@ -160,6 +180,7 @@ def _generate_command_parser(cls: type[ChiaCommand]) -> _CommandParsingStage: needs_context = True kwarg_names.append(field_name) elif "option_args" in _field.metadata: + click_call = _field.metadata["click_call"] option_args: dict[str, Any] = {"multiple": False, "required": False} option_args.update(_field.metadata["option_args"]) @@ -201,8 +222,12 @@ def _generate_command_parser(cls: type[ChiaCommand]) -> _CommandParsingStage: type_arg = option_args["type"] kwarg_names.append(field_name) + if click_call is click.argument: + is_multiple = option_args.pop("multiple") + if is_multiple: + option_args["nargs"] = -1 option_decorators.append( - click.option( + click_call( *option_args["param_decls"], field_name, type=type_arg, @@ -232,6 +257,8 @@ def chia_command( name: str, short_help: str, help: str, + add_help_option: bool = True, + ignore_unknown_options: bool = False, ) -> Callable[[type[ChiaCommand]], type[ChiaCommand]]: def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: # The type ignores here are largely due to the fact that the class information is not preserved after being @@ -252,6 +279,8 @@ def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: name=name, short_help=short_help, help=help, + add_help_option=add_help_option, + context_settings={"ignore_unknown_options": ignore_unknown_options}, )(_convert_class_to_function(wrapped_cls)) ) diff --git a/chia/cmds/services.py b/chia/cmds/services.py new file mode 100644 index 000000000000..ed9ab255980b --- /dev/null +++ b/chia/cmds/services.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import click + +from chia.cmds.cmd_classes import Context, argument, chia_command + + +@click.group( + name="services", + short_help="directly run Chia services", + help="""Directly run Chia services without launching them through the daemon. This + can be useful sometimes during development for launching with a debugger, or + when you want to use systemd or similar to manage the service processes. + """, +) +def services_group() -> None: + pass # pragma: no cover + + +@chia_command( + group=services_group, + name="full-node", + short_help="Start a full node", + # presently unused + help="Start a full node", + add_help_option=False, + ignore_unknown_options=True, +) +class FullNode: + context: Context + args: list[str] = argument(type=click.UNPROCESSED, multiple=True) + + def run(self) -> None: + import sys + + from chia.server.start_full_node import main + + click_context = click.get_current_context() + sys.argv = [click_context.command_path, *self.args] + main(root_path=self.context["root_path"]) + + +# full_node_command = get_chia_command_metadata(FullNode).command + + +# TODO: or instead of all other chia command changes, just add this and skip it +@services_group.command("full-node", add_help_option=False, context_settings={"ignore_unknown_options": True}) +@click.argument("args", nargs=-1, type=click.UNPROCESSED) +@click.pass_context +def full_node_command(ctx: click.Context, args: list[str]) -> None: + import sys + + from chia.server.start_full_node import main + + # hack since main() uses load_config_cli() which uses argparse + sys.argv = [ctx.command_path, *args] + main(root_path=ctx.obj["root_path"]) diff --git a/chia/server/start_full_node.py b/chia/server/start_full_node.py index c628f2a8b094..8d8902f10b24 100644 --- a/chia/server/start_full_node.py +++ b/chia/server/start_full_node.py @@ -2,7 +2,6 @@ import os import pathlib -import sys from multiprocessing import freeze_support from typing import Any, Optional @@ -18,7 +17,6 @@ from chia.types.aliases import FullNodeService from chia.util.chia_logging import initialize_service_logging from chia.util.config import get_unresolved_peer_infos, load_config, load_config_cli -from chia.util.default_root import resolve_root_path from chia.util.ints import uint16 from chia.util.task_timing import maybe_manage_task_instrumentation @@ -90,14 +88,13 @@ async def async_main(service_config: dict[str, Any], root_path: pathlib.Path) -> return 0 -def main() -> int: +def main(root_path: pathlib.Path, args: Optional[list[str]] = None) -> int: freeze_support() - root_path = resolve_root_path(override=None) with maybe_manage_task_instrumentation( enable=os.environ.get(f"CHIA_INSTRUMENT_{SERVICE_NAME.upper()}") is not None ): - service_config = load_config_cli(root_path, "config.yaml", SERVICE_NAME) + service_config = load_config_cli(root_path, "config.yaml", SERVICE_NAME, args=args) target_peer_count = service_config.get("target_peer_count", 40) - service_config.get( "target_outbound_peer_count", 8 ) @@ -106,7 +103,3 @@ def main() -> int: if not service_config.get("use_chia_loop_policy", True): target_peer_count = None return async_run(coro=async_main(service_config, root_path=root_path), connection_limit=target_peer_count) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/chia/util/config.py b/chia/util/config.py index 2859d4b6655e..c2d2703f9abe 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -158,6 +158,7 @@ def load_config_cli( filename: str, sub_config: Optional[str] = None, fill_missing_services: bool = False, + args: Optional[list[str]] = None, ) -> dict[str, Any]: """ Loads configuration from the specified filename, in the config directory, @@ -176,7 +177,7 @@ def load_config_cli( prop_type: Callable = str2bool if type(value) is bool else type(value) # type: ignore parser.add_argument(f"--{prop_name}", type=prop_type, dest=prop_name) - for key, value in vars(parser.parse_args()).items(): + for key, value in vars(parser.parse_args(args=args)).items(): if value is not None: flattened_props[key] = value diff --git a/pyproject.toml b/pyproject.toml index 92c11fe0f250..008fc72dcd9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ packages = [{ include = "chia"}, { include = "mozilla-ca/cacert.pem" }] chia = "chia.cmds.chia:main" chia_daemon = "chia.daemon.server:main" chia_wallet = "chia.server.start_wallet:main" -chia_full_node = "chia.server.start_full_node:main" +chia_full_node = "chia.cmds.services:full_node_command" chia_harvester = "chia.server.start_harvester:main" chia_farmer = "chia.server.start_farmer:main" chia_introducer = "chia.server.start_introducer:main" From bfbbc222a2829a97b3e9d80b33ab9936dba3ba43 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 13 Dec 2024 11:32:26 -0500 Subject: [PATCH 4/7] fixup tests --- chia/_tests/core/services/test_services.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/chia/_tests/core/services/test_services.py b/chia/_tests/core/services/test_services.py index 4f91e6dc8d22..b2f807fc5a44 100644 --- a/chia/_tests/core/services/test_services.py +++ b/chia/_tests/core/services/test_services.py @@ -84,20 +84,20 @@ async def test_daemon_terminates(signal_number: signal.Signals, chia_root: ChiaR @pytest.mark.parametrize(argnames="signal_number", argvalues=sendable_termination_signals) @pytest.mark.parametrize( - argnames=["create_service", "module_path", "service_config_name"], + argnames=["create_service", "module_args", "service_config_name"], argvalues=[ - [DataLayerRpcClient.create_as_context, "chia.server.start_data_layer", "data_layer"], - [FarmerRpcClient.create_as_context, "chia.server.start_farmer", "farmer"], - [FullNodeRpcClient.create_as_context, "chia.server.start_full_node", "full_node"], - [HarvesterRpcClient.create_as_context, "chia.server.start_harvester", "harvester"], - [WalletRpcClient.create_as_context, "chia.server.start_wallet", "wallet"], - [None, "chia.server.start_introducer", "introducer"], + [DataLayerRpcClient.create_as_context, ["chia.server.start_data_layer"], "data_layer"], + [FarmerRpcClient.create_as_context, ["chia.server.start_farmer"], "farmer"], + [FullNodeRpcClient.create_as_context, ["chia", "services", "full-node"], "full_node"], + [HarvesterRpcClient.create_as_context, ["chia.server.start_harvester"], "harvester"], + [WalletRpcClient.create_as_context, ["chia.server.start_wallet"], "wallet"], + [None, ["chia.server.start_introducer"], "introducer"], # TODO: fails... make it not do that - # [None, "chia.seeder.start_crawler", "crawler"], - [None, "chia.server.start_timelord", "timelord"], + # [None, ["chia.seeder.start_crawler"], "crawler"], + [None, ["chia.server.start_timelord"], "timelord"], pytest.param( None, - "chia.timelord.timelord_launcher", + ["chia.timelord.timelord_launcher"], "timelord_launcher", marks=pytest.mark.skipif( sys.platform in {"win32", "cygwin"}, @@ -115,7 +115,7 @@ async def test_services_terminate( signal_number: signal.Signals, chia_root: ChiaRoot, create_service: Optional[CreateServiceProtocol], - module_path: str, + module_args: list[str], service_config_name: str, ) -> None: with lock_and_load_config(root_path=chia_root.path, filename="config.yaml") as config: @@ -145,7 +145,7 @@ async def test_services_terminate( process = exit_stack.enter_context( closing_chia_root_popen( chia_root=chia_root, - args=[sys.executable, "-m", module_path], + args=[sys.executable, "-m", *module_args], ) ) From 1965e56750b3d1359733c5b39ac3bf4178db2029 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 13 Dec 2024 11:35:18 -0500 Subject: [PATCH 5/7] message --- chia/server/start_full_node.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chia/server/start_full_node.py b/chia/server/start_full_node.py index 8d8902f10b24..585a78ad621c 100644 --- a/chia/server/start_full_node.py +++ b/chia/server/start_full_node.py @@ -2,6 +2,7 @@ import os import pathlib +import sys from multiprocessing import freeze_support from typing import Any, Optional @@ -103,3 +104,7 @@ def main(root_path: pathlib.Path, args: Optional[list[str]] = None) -> int: if not service_config.get("use_chia_loop_policy", True): target_peer_count = None return async_run(coro=async_main(service_config, root_path=root_path), connection_limit=target_peer_count) + + +if __name__ == "__main__": + sys.exit("module no longer directly callable, see the new command group `chia services`") From e614e4569e1d5e3193ab8c9b55158a0b1dfee5f3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 13 Dec 2024 14:35:38 -0500 Subject: [PATCH 6/7] undo some --- chia/cmds/cmd_classes.py | 31 +------------------------------ chia/cmds/services.py | 29 ----------------------------- 2 files changed, 1 insertion(+), 59 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index ac15bc2258ec..597158e0edc8 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -43,26 +43,6 @@ def option(*param_decls: str, **kwargs: Any) -> Any: return field( metadata=dict( - click_call=click.option, - option_args=dict( - param_decls=tuple(param_decls), - **kwargs, - ), - ), - default=kwargs.get("default", default_default), - ) - - -def argument(*param_decls: str, **kwargs: Any) -> Any: - if sys.version_info < (3, 10): # versions < 3.10 don't know about kw_only and they complain about lacks of defaults - # Can't get coverage on this because we only test on one version - default_default = None # pragma: no cover - else: - default_default = MISSING - - return field( - metadata=dict( - click_call=click.argument, option_args=dict( param_decls=tuple(param_decls), **kwargs, @@ -180,7 +160,6 @@ def _generate_command_parser(cls: type[ChiaCommand]) -> _CommandParsingStage: needs_context = True kwarg_names.append(field_name) elif "option_args" in _field.metadata: - click_call = _field.metadata["click_call"] option_args: dict[str, Any] = {"multiple": False, "required": False} option_args.update(_field.metadata["option_args"]) @@ -222,12 +201,8 @@ def _generate_command_parser(cls: type[ChiaCommand]) -> _CommandParsingStage: type_arg = option_args["type"] kwarg_names.append(field_name) - if click_call is click.argument: - is_multiple = option_args.pop("multiple") - if is_multiple: - option_args["nargs"] = -1 option_decorators.append( - click_call( + click.option( *option_args["param_decls"], field_name, type=type_arg, @@ -257,8 +232,6 @@ def chia_command( name: str, short_help: str, help: str, - add_help_option: bool = True, - ignore_unknown_options: bool = False, ) -> Callable[[type[ChiaCommand]], type[ChiaCommand]]: def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: # The type ignores here are largely due to the fact that the class information is not preserved after being @@ -279,8 +252,6 @@ def _chia_command(cls: type[ChiaCommand]) -> type[ChiaCommand]: name=name, short_help=short_help, help=help, - add_help_option=add_help_option, - context_settings={"ignore_unknown_options": ignore_unknown_options}, )(_convert_class_to_function(wrapped_cls)) ) diff --git a/chia/cmds/services.py b/chia/cmds/services.py index ed9ab255980b..795a5689c296 100644 --- a/chia/cmds/services.py +++ b/chia/cmds/services.py @@ -2,8 +2,6 @@ import click -from chia.cmds.cmd_classes import Context, argument, chia_command - @click.group( name="services", @@ -17,33 +15,6 @@ def services_group() -> None: pass # pragma: no cover -@chia_command( - group=services_group, - name="full-node", - short_help="Start a full node", - # presently unused - help="Start a full node", - add_help_option=False, - ignore_unknown_options=True, -) -class FullNode: - context: Context - args: list[str] = argument(type=click.UNPROCESSED, multiple=True) - - def run(self) -> None: - import sys - - from chia.server.start_full_node import main - - click_context = click.get_current_context() - sys.argv = [click_context.command_path, *self.args] - main(root_path=self.context["root_path"]) - - -# full_node_command = get_chia_command_metadata(FullNode).command - - -# TODO: or instead of all other chia command changes, just add this and skip it @services_group.command("full-node", add_help_option=False, context_settings={"ignore_unknown_options": True}) @click.argument("args", nargs=-1, type=click.UNPROCESSED) @click.pass_context From 6bb47711dcfef66415b8a7fc99154424e56a7291 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 13 Dec 2024 14:38:41 -0500 Subject: [PATCH 7/7] touchup some commented code --- chia/_tests/core/services/test_services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/_tests/core/services/test_services.py b/chia/_tests/core/services/test_services.py index b2f807fc5a44..f9d862b81d6d 100644 --- a/chia/_tests/core/services/test_services.py +++ b/chia/_tests/core/services/test_services.py @@ -105,9 +105,9 @@ async def test_daemon_terminates(signal_number: signal.Signals, chia_root: ChiaR ), ), # TODO: fails... starts creating plots etc - # [None, "chia.simulator.start_simulator", "simulator"], + # [None, ["chia.simulator.start_simulator"], "simulator"], # TODO: fails... make it not do that - # [None, "chia.data_layer.data_layer_server", "data_layer"], + # [None, ["chia.data_layer.data_layer_server"], "data_layer"], ], ) @pytest.mark.anyio