diff --git a/conan/cli/cli.py b/conan/cli/cli.py index bd6c15a24ca..1e10b08358c 100644 --- a/conan/cli/cli.py +++ b/conan/cli/cli.py @@ -18,6 +18,7 @@ from conan.internal.cache.home_paths import HomePaths from conan import __version__ from conan.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError +from conans.util.files import load _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH = "_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH" @@ -37,6 +38,29 @@ def __init__(self, conan_api): self._groups = defaultdict(list) self._commands = {} + @staticmethod + def _warning_custom_commands_imports(custom_commands_path): + # Protect against usage of internal API + forbidden = ["from conan.internal", "from conan.tools", "from conan.test", "from conans"] + fails = {} + for root, _, fs in os.walk(custom_commands_path): + pyfiles = [os.path.join(root, f) for f in fs if f.endswith(".py")] + for f in pyfiles: + text = load(f) + for forbid in forbidden: + if forbid in text: + fails.setdefault(forbid, []).append(f) + if fails: + ConanOutput().warning("*" * 80) + for forbid, files in fails.items(): + ConanOutput().warning(f"Forbidden internal import'{forbid}' in:") + for f in files: + ConanOutput().warning(f" {f}") + ConanOutput().warning("") + ConanOutput().warning("These imports will change in future releases") + ConanOutput().warning("Please, use only public documented API in docs.conan.io") + ConanOutput().warning("*" * 80) + def add_commands(self): if Cli._builtin_commands is None: conan_cmd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands") @@ -61,6 +85,8 @@ def add_commands(self): if not os.path.isdir(custom_commands_path): return + self._warning_custom_commands_imports(custom_commands_path) + sys.path.append(custom_commands_path) for module in pkgutil.iter_modules([custom_commands_path]): module_name = module[1] diff --git a/test/integration/command_v2/custom_commands_test.py b/test/integration/command_v2/custom_commands_test.py index cea9c844b00..2449c55934e 100644 --- a/test/integration/command_v2/custom_commands_test.py +++ b/test/integration/command_v2/custom_commands_test.py @@ -469,3 +469,26 @@ def mycache(conan_api, parser, *args, **kwargs): c.run("mycache") # Does not break unexpectedly, caching is working assert "WARN: Cache invalidated, as expected: Invalid URL 'broken" in c.out + + +class TestCustomCommandsImports: + + def test_import_warning(self): + mycommand = textwrap.dedent(""" + from conan.cli.command import conan_command + from conan.internal.paths import DATA_YML + from conans.util.files import load + from conan.api.output import ConanOutput + + @conan_command() + def mycache(conan_api, parser, *args, **kwargs): + \""" this is a command with subcommands \""" + ConanOutput().info(f"DATA_YML: {DATA_YML}") + """) + + c = TestClient() + c.save_home({"extensions/commands/cmd_mycache.py": mycommand}) + c.run("mycache") + assert "WARN: Forbidden internal import'from conan.internal' in:" in c.out + assert "WARN: Forbidden internal import'from conans' in:" in c.out + assert "DATA_YML: conandata.yml"