From 40c3d999f04aa8a7b8a12d3c299097228c97353d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ram=C3=ADrez?= Date: Wed, 10 Jan 2024 13:42:25 +0100 Subject: [PATCH] [dev] Added env variable to append other custom command location (#15436) * Added internal env variable _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH to append custom commands location to the Conan one * Added extra comments --- conan/cli/cli.py | 58 +++++++++++-------- .../command_v2/custom_commands_test.py | 32 ++++++++++ 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/conan/cli/cli.py b/conan/cli/cli.py index 772dc4e03df..40183dceb68 100644 --- a/conan/cli/cli.py +++ b/conan/cli/cli.py @@ -47,35 +47,43 @@ def _add_commands(self): for k, v in self._commands.items(): # Fill groups data too self._groups[v.group].append(k) - custom_commands_path = HomePaths(self._conan_api.cache_folder).custom_commands_path - if not os.path.isdir(custom_commands_path): - return + conan_custom_commands_path = HomePaths(self._conan_api.cache_folder).custom_commands_path + # Important! This variable should be only used for testing/debugging purpose + developer_custom_commands_path = os.getenv("_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH") + # Notice that in case of having same custom commands file names, the developer one has + # preference over the Conan default location because of the sys.path.append(xxxx) + custom_commands_folders = [developer_custom_commands_path, conan_custom_commands_path] \ + if developer_custom_commands_path else [conan_custom_commands_path] + + for custom_commands_path in custom_commands_folders: + if not os.path.isdir(custom_commands_path): + return - sys.path.append(custom_commands_path) - for module in pkgutil.iter_modules([custom_commands_path]): - module_name = module[1] - if module_name.startswith("cmd_"): - try: - self._add_command(module_name, module_name.replace("cmd_", "")) - except Exception as e: - ConanOutput().error(f"Error loading custom command '{module_name}.py': {e}", - error_type="exception") - # layers - for folder in os.listdir(custom_commands_path): - layer_folder = os.path.join(custom_commands_path, folder) - sys.path.append(layer_folder) - if not os.path.isdir(layer_folder): - continue - for module in pkgutil.iter_modules([layer_folder]): + sys.path.append(custom_commands_path) + for module in pkgutil.iter_modules([custom_commands_path]): module_name = module[1] if module_name.startswith("cmd_"): - module_path = f"{folder}.{module_name}" try: - self._add_command(module_path, module_name.replace("cmd_", ""), - package=folder) + self._add_command(module_name, module_name.replace("cmd_", "")) except Exception as e: - ConanOutput().error(f"Error loading custom command {module_path}: {e}", + ConanOutput().error(f"Error loading custom command '{module_name}.py': {e}", error_type="exception") + # layers + for folder in os.listdir(custom_commands_path): + layer_folder = os.path.join(custom_commands_path, folder) + sys.path.append(layer_folder) + if not os.path.isdir(layer_folder): + continue + for module in pkgutil.iter_modules([layer_folder]): + module_name = module[1] + if module_name.startswith("cmd_"): + module_path = f"{folder}.{module_name}" + try: + self._add_command(module_path, module_name.replace("cmd_", ""), + package=folder) + except Exception as e: + ConanOutput().error(f"Error loading custom command {module_path}: {e}", + error_type="exception") def _add_command(self, import_path, method_name, package=None): try: @@ -84,7 +92,9 @@ def _add_command(self, import_path, method_name, package=None): if command_wrapper.doc: name = f"{package}:{command_wrapper.name}" if package else command_wrapper.name self._commands[name] = command_wrapper - self._groups[command_wrapper.group].append(name) + # Avoiding duplicated command help messages + if name not in self._groups[command_wrapper.group]: + self._groups[command_wrapper.group].append(name) for name, value in getmembers(imported_module): if isinstance(value, ConanSubCommand): if name.startswith("{}_".format(method_name)): diff --git a/conans/test/integration/command_v2/custom_commands_test.py b/conans/test/integration/command_v2/custom_commands_test.py index 9c33e936cc2..d9050bc53f6 100644 --- a/conans/test/integration/command_v2/custom_commands_test.py +++ b/conans/test/integration/command_v2/custom_commands_test.py @@ -1,7 +1,9 @@ import os import textwrap +from conans.test.utils.test_files import temp_folder from conans.test.utils.tools import TestClient +from conans.util.env import environment_update class TestCustomCommands: @@ -254,3 +256,33 @@ def mycommand(conan_api, parser, *args, **kwargs): client.run("danimtb:mycommand") foldername = os.path.basename(client.cache_folder) assert f'Conan cache folder from cmd_mycode: {foldername}' in client.out + + def test_custom_command_from_other_location(self): + """ + Tests that setting developer env variable ``_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH`` + will append that folder to the Conan custom command default location. + """ + myhello = textwrap.dedent(""" + from conan.api.output import cli_out_write + from conan.cli.command import conan_command + + @conan_command(group="custom commands") + def hello(conan_api, parser, *args, **kwargs): + ''' + My Hello doc + ''' + cli_out_write("Hello {}!") + """) + + client = TestClient() + my_local_layer_path = temp_folder(path_with_spaces=False) + layer_path = os.path.join(client.cache_folder, 'extensions', 'commands') + client.save({os.path.join(layer_path, 'cmd_hello.py'): myhello.format("world")}) + client.save({"cmd_hello.py": myhello.format("Overridden")}, path=my_local_layer_path) + with environment_update({"_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH": my_local_layer_path}): + client.run("hello") + # Local commands have preference over Conan custom ones if they collide + assert "Hello Overridden!" in client.out + # Without the variable it only loads the default custom commands location + client.run("hello") + assert "Hello world!" in client.out