Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change CLI backend to typer #76

Closed
xmnlab opened this issue Dec 25, 2023 · 5 comments
Closed

Change CLI backend to typer #76

xmnlab opened this issue Dec 25, 2023 · 5 comments

Comments

@xmnlab
Copy link
Member

xmnlab commented Dec 25, 2023

initial idea about how it could be implemented:

import typer
import click

app = typer.Typer()

# Example targets dictionary
targets = {
    "clean.all": {},
    "tests.unit": {"args": {"testname": {"type": "str", "default": "tests/"}}},
    "tests.linter": {},
}

def type_mapper(type_name):
    """
    Maps a string representation of a type to the actual Python type.

    Parameters
    ----------
    type_name : str
        The string representation of the type.

    Returns
    -------
    type
        The corresponding Python type.
    """
    type_mapping = {
        'str': str,
        'int': int,
        'float': float,
        'bool': bool
        # Add more mappings as needed
    }
    return type_mapping.get(type_name, str)

def create_dynamic_command(name, args):
    """
    Dynamically create a Typer command with Click options.

    Parameters
    ----------
    name : str
        Name of the command.
    args : dict
        Arguments for the command.
    """
    @app.command(name=name)
    def dynamic_command(**kwargs):
        typer.echo(f"Executing {name} with arguments: {kwargs}")

    for arg_name, arg_details in args.get('args', {}).items():
        arg_type = type_mapper(arg_details.get("type", "str"))
        default_value = arg_details.get("default", None)
        
        # Create a Click option and apply it to the dynamic_command
        click_option = click.option(f'--{arg_name}', default=default_value, type=arg_type)
        dynamic_command = click_option(dynamic_command)

    return dynamic_command

# Add dynamically created commands to Typer app
for name, args in targets.items():
    create_dynamic_command(name, args)

if __name__ == "__main__":
    app()

this is not the final version of the code, instead it is an example that shows that it would be possible

@xmnlab
Copy link
Member Author

xmnlab commented Dec 25, 2023

@abhijeetSaroha if you create a script with this code (for example, testtyper.py), you can test it locally using this:

$ python testtyper.py --help

@xmnlab
Copy link
Member Author

xmnlab commented Dec 25, 2023

this example still need changes in order to recognize well the args for each target

@xmnlab
Copy link
Member Author

xmnlab commented Dec 25, 2023

this is an example how to have it working also with the args:

import typer
import click

app = typer.Typer()

# Example targets dictionary
targets = {
    "clean.all": {
        "help": "clean all temporary files"
    },
    "tests.unit": {
        "help": "unit tests",
        "args": {
            "testname": {
                "type": "str",
                "default": "tests",
                "help": "set the file name of the test file."
            }
        }
    },
    "tests.linter": {
        "help": "run linter",
    },
}

def type_mapper(type_name):
    """
    Maps a string representation of a type to the actual Python type.

    Parameters
    ----------
    type_name : str
        The string representation of the type.

    Returns
    -------
    type
        The corresponding Python type.
    """
    type_mapping = {
        'str': str,
        'int': int,
        'float': float,
        'bool': bool
        # Add more mappings as needed
    }
    return type_mapping.get(type_name, str)

def apply_click_options(command_function, options):
    """
    Apply Click options to a Typer command function.

    Parameters
    ----------
    command_function : callable
        The Typer command function to which options will be applied.
    options : dict
        A dictionary of options to apply.

    Returns
    -------
    callable
        The command function with options applied.
    """
    for opt_name, opt_details in options.items():
        click_option = click.option(
            f'--{opt_name}',
            default=opt_details.get('default'),
            type=type_mapper(opt_details.get('type', 'str')),
            help=opt_details.get('help', '')
        )
        print()
        command_function = click_option(command_function)

    return command_function

def create_dynamic_command(name, args):
    """
    Dynamically create a Typer command with the specified options.

    Parameters
    ----------
    name : str
        The command name.
    args : dict
        The command arguments and options.
    """

    args_str = "" if not args.get('args', {}) else ",".join([
        f"{name}: {spec['type']}" + ('' if not spec.get('default') else f'= \"{spec["default"]}\"')
        for name, spec in args.get('args', {}).items()
    ])

    decorator = app.command(
        name=name,
        help=args['help']
    )

    function_code = (
        f"def dynamic_command({args_str}):\n"
        "    typer.echo(f'Executing ' + name)\n"
        "\n"
    )

    local_vars = {}
    exec(function_code, globals(), local_vars)
    dynamic_command = decorator(local_vars["dynamic_command"])

    # Apply Click options to the Typer command
    if 'args' in args:
        dynamic_command = apply_click_options(dynamic_command, args['args'])

    return dynamic_command

# Add dynamically created commands to Typer app
for name, args in targets.items():
    create_dynamic_command(name, args)

if __name__ == "__main__":
    app()

it is a very dirty example, for example for default it is just working for strings, but it should work for other types as well
it would need a lot of refactoring to have it properly working ..

@xmnlab
Copy link
Member Author

xmnlab commented Dec 25, 2023

def create_args_string(args):
    args_rendered = []

    for name, spec in args.get('args', {}).items():
        arg_str = f"{name}: {spec['type']}"

        if not spec.get('default'):
            args_rendered.append(arg_str)
            continue

        if spec['type'] == "str":
            arg_str += f'= \"{spec["default"]}\"'
        else:
            arg_str += f'= {spec["default"]}'
        args_rendered.append(arg_str)

    return "".join(args_rendered)

@xmnlab
Copy link
Member Author

xmnlab commented Jan 12, 2024

resolved by #82

@xmnlab xmnlab closed this as completed Jan 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant