diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index ad79d4a5cc..fe8865c730 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from . import click_wrap from .interfaces import ( IPluginPaths, ITrayAddon, @@ -16,6 +17,8 @@ from .base import ( __all__ = ( + "click_wrap", + "IPluginPaths", "ITrayAddon", "ITrayAction", diff --git a/client/ayon_core/addon/click_wrap.py b/client/ayon_core/addon/click_wrap.py new file mode 100644 index 0000000000..ffb9c61fa1 --- /dev/null +++ b/client/ayon_core/addon/click_wrap.py @@ -0,0 +1,365 @@ +"""Simplified wrapper for 'click' python module. + +Module 'click' is used as main cli handler in AYON/OpenPype. Addons can +register their own subcommands with options. This wrapper allows to define +commands and options as with 'click', but without any dependency. + +Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype +is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+). +And updating 'click' would break other DCCs. + +How to use it? If you already have cli commands defined in addon, just replace +'click' with 'click_wrap' and it should work and modify your addon's cli +method to convert 'click_wrap' object to 'click' object. + +Before +```python +import click +from ayon_core.modules import OpenPypeModule + + +class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group(ExampleAddon.name, help="Example addon") +def cli_main(): + pass + + +@cli_main.command(help="Example command") +@click.option("--arg1", help="Example argument 1", default="default1") +@click.option("--arg2", help="Example argument 2", is_flag=True) +def mycommand(arg1, arg2): + print(arg1, arg2) +``` + +Now +``` +from ayon_core import click_wrap +from ayon_core.modules import OpenPypeModule + + +class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + click_group.add_command(cli_main.to_click_obj()) + + +@click_wrap.group(ExampleAddon.name, help="Example addon") +def cli_main(): + pass + + +@cli_main.command(help="Example command") +@click_wrap.option("--arg1", help="Example argument 1", default="default1") +@click_wrap.option("--arg2", help="Example argument 2", is_flag=True) +def mycommand(arg1, arg2): + print(arg1, arg2) +``` + + +Added small enhancements: +- most of the methods can be used as chained calls +- functions/methods 'command' and 'group' can be used in a way that + first argument is callback function and the rest are arguments + for click + +Example: + ```python + from ayon_core import click_wrap + from ayon_core.modules import OpenPypeModule + + + class ExampleAddon(OpenPypeModule): + name = "example" + + def cli(self, click_group): + # Define main command (name 'example') + main = click_wrap.group( + self._cli_main, name=self.name, help="Example addon" + ) + # Add subcommand (name 'mycommand') + ( + main.command( + self._cli_command, name="mycommand", help="Example command" + ) + .option( + "--arg1", help="Example argument 1", default="default1" + ) + .option( + "--arg2", help="Example argument 2", is_flag=True, + ) + ) + # Convert main command to click object and add it to parent group + click_group.add_command(main.to_click_obj()) + + def _cli_main(self): + pass + + def _cli_command(self, arg1, arg2): + print(arg1, arg2) + ``` + + ```shell + openpype_console addon example mycommand --arg1 value1 --arg2 + ``` +""" + +import collections + +FUNC_ATTR_NAME = "__ayon_cli_options__" + + +class Command(object): + def __init__(self, func, *args, **kwargs): + # Command function + self._func = func + # Command definition arguments + self._args = args + # Command definition kwargs + self._kwargs = kwargs + # Both 'options' and 'arguments' are stored to the same variable + # - keep order of options and arguments + self._options = getattr(func, FUNC_ATTR_NAME, []) + + def to_click_obj(self): + """Converts this object to click object. + + Returns: + click.Command: Click command object. + """ + return convert_to_click(self) + + # --- Methods for 'convert_to_click' function --- + def get_args(self): + """ + Returns: + tuple: Command definition arguments. + """ + return self._args + + def get_kwargs(self): + """ + Returns: + dict[str, Any]: Command definition kwargs. + """ + return self._kwargs + + def get_func(self): + """ + Returns: + Function: Function to invoke on command trigger. + """ + return self._func + + def iter_options(self): + """ + Yields: + tuple[str, tuple, dict]: Option type name with args and kwargs. + """ + for item in self._options: + yield item + # ----------------------------------------------- + + def add_option(self, *args, **kwargs): + return self.add_option_by_type("option", *args, **kwargs) + + def add_argument(self, *args, **kwargs): + return self.add_option_by_type("argument", *args, **kwargs) + + option = add_option + argument = add_argument + + def add_option_by_type(self, option_name, *args, **kwargs): + self._options.append((option_name, args, kwargs)) + return self + + +class Group(Command): + def __init__(self, func, *args, **kwargs): + super(Group, self).__init__(func, *args, **kwargs) + # Store sub-groupd and sub-commands to the same variable + self._commands = [] + + # --- Methods for 'convert_to_click' function --- + def iter_commands(self): + for command in self._commands: + yield command + # ----------------------------------------------- + + def add_command(self, command): + """Add prepared command object as child. + + Args: + command (Command): Prepared command object. + """ + if command not in self._commands: + self._commands.append(command) + + def add_group(self, group): + """Add prepared group object as child. + + Args: + group (Group): Prepared group object. + """ + if group not in self._commands: + self._commands.append(group) + + def command(self, *args, **kwargs): + """Add child command. + + Returns: + Union[Command, Function]: New command object, or wrapper function. + """ + return self._add_new(Command, *args, **kwargs) + + def group(self, *args, **kwargs): + """Add child group. + + Returns: + Union[Group, Function]: New group object, or wrapper function. + """ + return self._add_new(Group, *args, **kwargs) + + def _add_new(self, target_cls, *args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + out = target_cls(_func, *args, **kwargs) + self._commands.append(out) + return out + + if func is not None: + return decorator(func) + return decorator + + +def convert_to_click(obj_to_convert): + """Convert wrapped object to click object. + + Args: + obj_to_convert (Command): Object to convert to click object. + + Returns: + click.Command: Click command object. + """ + import click + + commands_queue = collections.deque() + commands_queue.append((obj_to_convert, None)) + top_obj = None + while commands_queue: + item = commands_queue.popleft() + command_obj, parent_obj = item + if not isinstance(command_obj, Command): + raise TypeError( + "Invalid type '{}' expected 'Command'".format( + type(command_obj) + ) + ) + + if isinstance(command_obj, Group): + click_obj = ( + click.group( + *command_obj.get_args(), + **command_obj.get_kwargs() + )(command_obj.get_func()) + ) + + else: + click_obj = ( + click.command( + *command_obj.get_args(), + **command_obj.get_kwargs() + )(command_obj.get_func()) + ) + + for item in command_obj.iter_options(): + option_name, args, kwargs = item + if option_name == "option": + click.option(*args, **kwargs)(click_obj) + elif option_name == "argument": + click.argument(*args, **kwargs)(click_obj) + else: + raise ValueError( + "Invalid option name '{}'".format(option_name) + ) + + if top_obj is None: + top_obj = click_obj + + if parent_obj is not None: + parent_obj.add_command(click_obj) + + if isinstance(command_obj, Group): + for command in command_obj.iter_commands(): + commands_queue.append((command, click_obj)) + + return top_obj + + +def group(*args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + return Group(_func, *args, **kwargs) + + if func is not None: + return decorator(func) + return decorator + + +def command(*args, **kwargs): + func = None + if args and callable(args[0]): + args = list(args) + func = args.pop(0) + args = tuple(args) + + def decorator(_func): + return Command(_func, *args, **kwargs) + + if func is not None: + return decorator(func) + return decorator + + +def argument(*args, **kwargs): + def decorator(func): + return _add_option_to_func( + func, "argument", *args, **kwargs + ) + return decorator + + +def option(*args, **kwargs): + def decorator(func): + return _add_option_to_func( + func, "option", *args, **kwargs + ) + return decorator + + +def _add_option_to_func(func, option_name, *args, **kwargs): + if isinstance(func, Command): + func.add_option_by_type(option_name, *args, **kwargs) + return func + + if not hasattr(func, FUNC_ATTR_NAME): + setattr(func, FUNC_ATTR_NAME, []) + cli_options = getattr(func, FUNC_ATTR_NAME) + cli_options.append((option_name, args, kwargs)) + return func diff --git a/client/ayon_core/modules/click_wrap.py b/client/ayon_core/modules/click_wrap.py index ffb9c61fa1..8f68de187a 100644 --- a/client/ayon_core/modules/click_wrap.py +++ b/client/ayon_core/modules/click_wrap.py @@ -1,365 +1 @@ -"""Simplified wrapper for 'click' python module. - -Module 'click' is used as main cli handler in AYON/OpenPype. Addons can -register their own subcommands with options. This wrapper allows to define -commands and options as with 'click', but without any dependency. - -Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype -is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+). -And updating 'click' would break other DCCs. - -How to use it? If you already have cli commands defined in addon, just replace -'click' with 'click_wrap' and it should work and modify your addon's cli -method to convert 'click_wrap' object to 'click' object. - -Before -```python -import click -from ayon_core.modules import OpenPypeModule - - -class ExampleAddon(OpenPypeModule): - name = "example" - - def cli(self, click_group): - click_group.add_command(cli_main) - - -@click.group(ExampleAddon.name, help="Example addon") -def cli_main(): - pass - - -@cli_main.command(help="Example command") -@click.option("--arg1", help="Example argument 1", default="default1") -@click.option("--arg2", help="Example argument 2", is_flag=True) -def mycommand(arg1, arg2): - print(arg1, arg2) -``` - -Now -``` -from ayon_core import click_wrap -from ayon_core.modules import OpenPypeModule - - -class ExampleAddon(OpenPypeModule): - name = "example" - - def cli(self, click_group): - click_group.add_command(cli_main.to_click_obj()) - - -@click_wrap.group(ExampleAddon.name, help="Example addon") -def cli_main(): - pass - - -@cli_main.command(help="Example command") -@click_wrap.option("--arg1", help="Example argument 1", default="default1") -@click_wrap.option("--arg2", help="Example argument 2", is_flag=True) -def mycommand(arg1, arg2): - print(arg1, arg2) -``` - - -Added small enhancements: -- most of the methods can be used as chained calls -- functions/methods 'command' and 'group' can be used in a way that - first argument is callback function and the rest are arguments - for click - -Example: - ```python - from ayon_core import click_wrap - from ayon_core.modules import OpenPypeModule - - - class ExampleAddon(OpenPypeModule): - name = "example" - - def cli(self, click_group): - # Define main command (name 'example') - main = click_wrap.group( - self._cli_main, name=self.name, help="Example addon" - ) - # Add subcommand (name 'mycommand') - ( - main.command( - self._cli_command, name="mycommand", help="Example command" - ) - .option( - "--arg1", help="Example argument 1", default="default1" - ) - .option( - "--arg2", help="Example argument 2", is_flag=True, - ) - ) - # Convert main command to click object and add it to parent group - click_group.add_command(main.to_click_obj()) - - def _cli_main(self): - pass - - def _cli_command(self, arg1, arg2): - print(arg1, arg2) - ``` - - ```shell - openpype_console addon example mycommand --arg1 value1 --arg2 - ``` -""" - -import collections - -FUNC_ATTR_NAME = "__ayon_cli_options__" - - -class Command(object): - def __init__(self, func, *args, **kwargs): - # Command function - self._func = func - # Command definition arguments - self._args = args - # Command definition kwargs - self._kwargs = kwargs - # Both 'options' and 'arguments' are stored to the same variable - # - keep order of options and arguments - self._options = getattr(func, FUNC_ATTR_NAME, []) - - def to_click_obj(self): - """Converts this object to click object. - - Returns: - click.Command: Click command object. - """ - return convert_to_click(self) - - # --- Methods for 'convert_to_click' function --- - def get_args(self): - """ - Returns: - tuple: Command definition arguments. - """ - return self._args - - def get_kwargs(self): - """ - Returns: - dict[str, Any]: Command definition kwargs. - """ - return self._kwargs - - def get_func(self): - """ - Returns: - Function: Function to invoke on command trigger. - """ - return self._func - - def iter_options(self): - """ - Yields: - tuple[str, tuple, dict]: Option type name with args and kwargs. - """ - for item in self._options: - yield item - # ----------------------------------------------- - - def add_option(self, *args, **kwargs): - return self.add_option_by_type("option", *args, **kwargs) - - def add_argument(self, *args, **kwargs): - return self.add_option_by_type("argument", *args, **kwargs) - - option = add_option - argument = add_argument - - def add_option_by_type(self, option_name, *args, **kwargs): - self._options.append((option_name, args, kwargs)) - return self - - -class Group(Command): - def __init__(self, func, *args, **kwargs): - super(Group, self).__init__(func, *args, **kwargs) - # Store sub-groupd and sub-commands to the same variable - self._commands = [] - - # --- Methods for 'convert_to_click' function --- - def iter_commands(self): - for command in self._commands: - yield command - # ----------------------------------------------- - - def add_command(self, command): - """Add prepared command object as child. - - Args: - command (Command): Prepared command object. - """ - if command not in self._commands: - self._commands.append(command) - - def add_group(self, group): - """Add prepared group object as child. - - Args: - group (Group): Prepared group object. - """ - if group not in self._commands: - self._commands.append(group) - - def command(self, *args, **kwargs): - """Add child command. - - Returns: - Union[Command, Function]: New command object, or wrapper function. - """ - return self._add_new(Command, *args, **kwargs) - - def group(self, *args, **kwargs): - """Add child group. - - Returns: - Union[Group, Function]: New group object, or wrapper function. - """ - return self._add_new(Group, *args, **kwargs) - - def _add_new(self, target_cls, *args, **kwargs): - func = None - if args and callable(args[0]): - args = list(args) - func = args.pop(0) - args = tuple(args) - - def decorator(_func): - out = target_cls(_func, *args, **kwargs) - self._commands.append(out) - return out - - if func is not None: - return decorator(func) - return decorator - - -def convert_to_click(obj_to_convert): - """Convert wrapped object to click object. - - Args: - obj_to_convert (Command): Object to convert to click object. - - Returns: - click.Command: Click command object. - """ - import click - - commands_queue = collections.deque() - commands_queue.append((obj_to_convert, None)) - top_obj = None - while commands_queue: - item = commands_queue.popleft() - command_obj, parent_obj = item - if not isinstance(command_obj, Command): - raise TypeError( - "Invalid type '{}' expected 'Command'".format( - type(command_obj) - ) - ) - - if isinstance(command_obj, Group): - click_obj = ( - click.group( - *command_obj.get_args(), - **command_obj.get_kwargs() - )(command_obj.get_func()) - ) - - else: - click_obj = ( - click.command( - *command_obj.get_args(), - **command_obj.get_kwargs() - )(command_obj.get_func()) - ) - - for item in command_obj.iter_options(): - option_name, args, kwargs = item - if option_name == "option": - click.option(*args, **kwargs)(click_obj) - elif option_name == "argument": - click.argument(*args, **kwargs)(click_obj) - else: - raise ValueError( - "Invalid option name '{}'".format(option_name) - ) - - if top_obj is None: - top_obj = click_obj - - if parent_obj is not None: - parent_obj.add_command(click_obj) - - if isinstance(command_obj, Group): - for command in command_obj.iter_commands(): - commands_queue.append((command, click_obj)) - - return top_obj - - -def group(*args, **kwargs): - func = None - if args and callable(args[0]): - args = list(args) - func = args.pop(0) - args = tuple(args) - - def decorator(_func): - return Group(_func, *args, **kwargs) - - if func is not None: - return decorator(func) - return decorator - - -def command(*args, **kwargs): - func = None - if args and callable(args[0]): - args = list(args) - func = args.pop(0) - args = tuple(args) - - def decorator(_func): - return Command(_func, *args, **kwargs) - - if func is not None: - return decorator(func) - return decorator - - -def argument(*args, **kwargs): - def decorator(func): - return _add_option_to_func( - func, "argument", *args, **kwargs - ) - return decorator - - -def option(*args, **kwargs): - def decorator(func): - return _add_option_to_func( - func, "option", *args, **kwargs - ) - return decorator - - -def _add_option_to_func(func, option_name, *args, **kwargs): - if isinstance(func, Command): - func.add_option_by_type(option_name, *args, **kwargs) - return func - - if not hasattr(func, FUNC_ATTR_NAME): - setattr(func, FUNC_ATTR_NAME, []) - cli_options = getattr(func, FUNC_ATTR_NAME) - cli_options.append((option_name, args, kwargs)) - return func +from ayon_core.addon.click_wrap import * diff --git a/client/ayon_core/modules/job_queue/module.py b/client/ayon_core/modules/job_queue/module.py index 78bb1f26fe..1cecd62de5 100644 --- a/client/ayon_core/modules/job_queue/module.py +++ b/client/ayon_core/modules/job_queue/module.py @@ -41,7 +41,8 @@ import json import copy import platform -from ayon_core.modules import OpenPypeModule, click_wrap +from ayon_core.addon import click_wrap +from ayon_core.modules import OpenPypeModule from ayon_core.settings import get_system_settings