From f422e30904df2cc0c5e44a4eb5fa8eaa216b8ca6 Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha <108522472+abhijeetSaroha@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:31:02 +0530 Subject: [PATCH] feat: Add support for interactive args (#112) --- .makim.yaml | 26 +++++++++++++++ src/makim/cli.py | 42 +++++++++++++----------- src/makim/core.py | 2 +- tests/smoke/.makim-interactive-args.yaml | 39 ++++++++++++++++++++++ 4 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 tests/smoke/.makim-interactive-args.yaml diff --git a/.makim.yaml b/.makim.yaml index 93de3c5..eba39ba 100644 --- a/.makim.yaml +++ b/.makim.yaml @@ -83,6 +83,7 @@ groups: - task: smoke-tests.dir-absolute-path - task: smoke-tests.dir-no-path - task: smoke-tests.dir-relative-path + - task: smoke-tests.interactive-args ci: help: Run all tasks used on CI @@ -346,6 +347,31 @@ groups: makim $VERBOSE_FLAG --file $MAKIM_FILE group-relative.task-absolute makim $VERBOSE_FLAG --file $MAKIM_FILE group-relative.task-relative + interactive-args: + help: Test makim with interactive-args + args: + verbose-mode: + help: Run the all the tests in verbose mode + type: bool + action: store_true + env: + MAKIM_FILE: tests/smoke/.makim-interactive-args.yaml + shell: bash + run: | + export VERBOSE_FLAG='${{ "--verbose" if args.verbose_mode else "" }}' + makim $VERBOSE_FLAG --file $MAKIM_FILE --help + makim $VERBOSE_FLAG --file $MAKIM_FILE --version + makim $VERBOSE_FLAG --file $MAKIM_FILE user.create --username johndoe --email johndoe@gmail.com --password johndoe + makim $VERBOSE_FLAG --file $MAKIM_FILE weather.forecast --city Delhi --country India + + RESULT=$(echo mycity | makim $VERBOSE_FLAG --file $MAKIM_FILE weather.forecast --country mycountry) + STRING_VALIDATION="Fetching weather forecast for mycity, mycountry..." + # Check if RESULT contains SUBSTRING + if [[ "$RESULT" != *"$STRING_VALIDATION"* ]]; then + echo "STRING_VALIDATION not found in RESULT." + exit 1 + fi + error: help: This group helps tests failure tasks tasks: diff --git a/src/makim/cli.py b/src/makim/cli.py index 51763fa..ebf47b4 100644 --- a/src/makim/cli.py +++ b/src/makim/cli.py @@ -172,7 +172,7 @@ def create_args_string(args: Dict[str, str]) -> str: args_rendered = [] arg_template = ( - '{arg_name}: {arg_type} = typer.Option(' + '{arg_name}: Optional[{arg_type}] = typer.Option(' '{default_value}, ' '"--{name_flag}", ' 'help="{help_text}"' @@ -184,9 +184,11 @@ def create_args_string(args: Dict[str, str]) -> str: name_clean = name.replace('-', '_') arg_type = normalize_string_type(spec.get('type', 'str')) help_text = spec.get('help', '') - default_value = '...' + default_value = 'None' - if not spec.get('required', False): + if not spec.get('required', False) and not spec.get( + 'interactive', False + ): default_value = spec.get('default', '') default_value = get_default_value_str(arg_type, default_value) @@ -206,7 +208,7 @@ def create_args_string(args: Dict[str, str]) -> str: def apply_click_options( - command_function: Callable, options: Dict[str, str] + command_function: Callable, options: Dict[str, Any] ) -> Callable: """ Apply Click options to a Typer command function. @@ -235,7 +237,9 @@ def apply_click_options( opt_args.update( { - 'default': opt_default, + 'default': None + if opt_data.get('interactive', False) + else opt_default, 'type': map_type_from_string(opt_type_str), 'help': opt_data.get('help', ''), 'show_default': True, @@ -265,9 +269,9 @@ def create_dynamic_command(name: str, args: Dict[str, str]) -> None: args_str = create_args_string(args) args_param_list = [f'"task": "{name}"'] - args_data = cast(Dict[str, str], args.get('args', {})) + args_data = cast(Dict[str, Dict[str, str]], args.get('args', {})) - for arg in list(args_data.keys()): + for arg, arg_details in args_data.items(): arg_clean = arg.replace('-', '_') args_param_list.append(f'"--{arg}": {arg_clean}') @@ -280,24 +284,24 @@ def create_dynamic_command(name: str, args: Dict[str, str]) -> None: rich_help_panel=group_name, ) - function_code = ( - f'def dynamic_command({args_str}):\n' - f' makim.run({args_param_str})\n' - '\n' - ) + function_code = f'def dynamic_command({args_str}):\n' + + # handle interactive prompts + for arg, arg_details in args_data.items(): + arg_clean = arg.replace('-', '_') + if arg_details.get('interactive', False): + function_code += f' if {arg_clean} is None:\n' + function_code += f" {arg_clean} = click.prompt('{arg}')\n" + + function_code += f' makim.run({args_param_str})\n' local_vars: Dict[str, Any] = {} - try: - exec(function_code, globals(), local_vars) - except Exception as e: - # breakpoint() - print(e) - print(function_code) + exec(function_code, globals(), local_vars) dynamic_command = decorator(local_vars['dynamic_command']) # Apply Click options to the Typer command if 'args' in args: - options_data = cast(Dict[str, str], args.get('args', {})) + options_data = cast(Dict[str, Dict[str, Any]], args.get('args', {})) dynamic_command = apply_click_options(dynamic_command, options_data) diff --git a/src/makim/core.py b/src/makim/core.py index 97aa8da..c63555f 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -541,7 +541,7 @@ def _run_command(self, args: dict): args_input[k_clean] = default input_flag = f'--{k}' - if args.get(input_flag): + if args.get(input_flag) is not None: if action == 'store_true': args_input[k_clean] = ( True if args[input_flag] is None else args[input_flag] diff --git a/tests/smoke/.makim-interactive-args.yaml b/tests/smoke/.makim-interactive-args.yaml new file mode 100644 index 0000000..d76664d --- /dev/null +++ b/tests/smoke/.makim-interactive-args.yaml @@ -0,0 +1,39 @@ +groups: + user: + tasks: + create: + help: Create a new user + args: + username: + type: str + help: The username for the new user + email: + type: str + help: The email address for the new user + interactive: true + password: + type: str + help: The password for the new user + interactive: true + run: | + echo "Creating user:" + echo "Username: ${{ args.username }}" + echo "Email: ${{ args.email }}" + echo "Password: ${{ args.password }}" + + weather: + tasks: + forecast: + help: Get the weather forecast for a location + args: + city: + type: str + help: Enter the city for weather forecast + interactive: true + country: + type: str + help: Enter the country for weather forecast + interactive: true + run: | + echo "Fetching weather forecast for ${{ args.city }}, ${{ args.country }}..." + echo "The weather in ${{ args.city }} is sunny with a high of 25°C today!"