From 4fbbf1164d181b5e5090fe5a6693df853684227d Mon Sep 17 00:00:00 2001 From: Maxim Kolyubyakin Date: Sun, 1 Nov 2026 11:39:31 +0100 Subject: [PATCH] perf(core): import a command group only when it runs Every ccli command imported all eight command groups first, and with them WeasyPrint, the extension runtime and Excel support: about 1,800 modules for `ccli --help`. Every command paid that time, and every command failed on machines without Cairo and Pango, although only the PDF reports need them. The root group now knows the built-in groups by name and short help, and imports a group only when it runs. Every output stays exactly the same. One test checks the table against the groups, another that `ccli --help` imports none of them. Third-party plugins load as before. On an M1 Pro with connect-openapi-client 35.0: `ccli --help` 1.7 s -> 1.1 s, `ccli product --help` 1.6 s -> 1.1 s. Most of what remains is the client's import of inflect, which is fixed separately in connect-openapi-client. --- connect/cli/core/base.py | 46 +++++++++++++++++++++- connect/cli/core/plugins.py | 23 ++++++++++- tests/core/test_plugins.py | 77 ++++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/connect/cli/core/base.py b/connect/cli/core/base.py index 2e7e1ad0..6c17866e 100644 --- a/connect/cli/core/base.py +++ b/connect/cli/core/base.py @@ -46,6 +46,50 @@ def decorator(f): return decorator +class LazyGroup(click.Group): + """A group that imports a subgroup only when it is called. + + Until then, `ccli --help` lists the subgroup by the short help it was added with. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.lazy_commands = {} + + def add_lazy_command(self, name, short_help, entrypoint): + if name not in self.commands: + self.lazy_commands[name] = (short_help, entrypoint) + + def list_commands(self, ctx): + return sorted({*super().list_commands(ctx), *self.lazy_commands}) + + def get_command(self, ctx, cmd_name): + if cmd_name in self.lazy_commands: + _, entrypoint = self.lazy_commands[cmd_name] + self.add_command(entrypoint.load()(), cmd_name) + del self.lazy_commands[cmd_name] + return super().get_command(ctx, cmd_name) + + def format_commands(self, ctx, formatter): + commands = [] + for name in self.list_commands(ctx): + if name in self.lazy_commands: + commands.append((name, self.lazy_commands[name][0])) + continue + cmd = super().get_command(ctx, name) + if cmd is not None and not cmd.hidden: + commands.append((name, cmd)) + if commands: + limit = formatter.width - 6 - max(len(name) for name, _ in commands) + with formatter.section('Commands'): + formatter.write_dl( + [ + (name, cmd if isinstance(cmd, str) else cmd.get_short_help_str(limit)) + for name, cmd in commands + ], + ) + + def group(name=None, **attrs): attrs.setdefault('cls', CCliGroup) return click.command(name, **attrs) @@ -59,7 +103,7 @@ def print_version(ctx, param, value): ctx.exit() -@click.group(context_settings={'help_option_names': ['-h', '--help']}) +@click.group(cls=LazyGroup, context_settings={'help_option_names': ['-h', '--help']}) @click.option( '--version', is_flag=True, diff --git a/connect/cli/core/plugins.py b/connect/cli/core/plugins.py index 0a9d9fe4..39f3e539 100644 --- a/connect/cli/core/plugins.py +++ b/connect/cli/core/plugins.py @@ -7,6 +7,22 @@ from connect.cli.core.utils import iter_entry_points +# The built-in command groups by entry point: their command name and the short help that +# `ccli --help` shows. Knowing them up front, ccli imports a group only when it runs: some +# pull in heavy libraries, such as WeasyPrint for the PDF reports. +# tests/core/test_plugins.py checks this table against the groups themselves. +BUILTIN_GROUPS = { + 'commerce': ('commerce', 'Manage commerce definitions.'), + 'customer': ('customer', 'Export/synchronize customers.'), + 'locales': ('locale', 'List all locales available.'), + 'play': ('play', 'Play connect scripts.'), + 'product': ('product', 'Manage product definitions.'), + 'project': ('project', 'Manage project definitions.'), + 'report': ('report', 'Reports execution and development.'), + 'translation': ('translation', 'Manage translations.'), +} + + def load_plugins(cli): @click.group(name='plugin', short_help='Third party plugins.') def grp_plugins(): @@ -15,7 +31,12 @@ def grp_plugins(): has_3rd_party_plugins = False for entrypoint in iter_entry_points('connect.cli.plugins'): - if entrypoint.value.startswith('connect.cli.plugins.'): + if ( + entrypoint.value.startswith('connect.cli.plugins.') + and entrypoint.name in BUILTIN_GROUPS + ): + cli.add_lazy_command(*BUILTIN_GROUPS[entrypoint.name], entrypoint) + elif entrypoint.value.startswith('connect.cli.plugins.'): command_fn = entrypoint.load() cli.add_command(command_fn()) else: diff --git a/tests/core/test_plugins.py b/tests/core/test_plugins.py index 6a6db346..a1fda7b4 100644 --- a/tests/core/test_plugins.py +++ b/tests/core/test_plugins.py @@ -1,8 +1,12 @@ +import subprocess +import sys from importlib.metadata import EntryPoint import click -from connect.cli.core.plugins import load_plugins +from connect.cli.core.base import LazyGroup +from connect.cli.core.plugins import BUILTIN_GROUPS, load_plugins +from connect.cli.core.utils import iter_entry_points def test_load_plugins(mocker): @@ -60,3 +64,74 @@ def test_load_no_external(mocker): assert 'internal' in cli.commands assert 'plugin' not in cli.commands + + +def test_builtin_groups_match_the_installed_plugins(): + entrypoints = { + ep.name: ep + for ep in iter_entry_points('connect.cli.plugins') + if ep.value.startswith('connect.cli.plugins.') + } + + assert set(entrypoints) == set(BUILTIN_GROUPS) + for ep_name, (name, short_help) in BUILTIN_GROUPS.items(): + grp = entrypoints[ep_name].load()() + assert (grp.name, grp.short_help) == (name, short_help) + + +def test_load_plugins_adds_builtin_groups_lazily(mocker): + cli = LazyGroup() + cli.add_command(click.Command('account', short_help='Manage accounts.')) + cli.add_command(click.Command('secret', hidden=True)) + grp_product = click.Group('product', short_help='Manage product definitions.') + load = mocker.patch.object(EntryPoint, 'load', return_value=lambda: grp_product) + mocker.patch( + 'connect.cli.core.plugins.iter_entry_points', + return_value=iter( + [ + EntryPoint('product', 'connect.cli.plugins.product.commands:get_group', None), + ], + ), + ) + + load_plugins(cli) + ctx = click.Context(cli) + help_text = cli.get_help(ctx) + + assert 'product' not in cli.commands + assert cli.list_commands(ctx) == ['account', 'product', 'secret'] + assert 'Manage accounts.' in help_text + assert 'Manage product definitions.' in help_text + assert 'secret' not in help_text + load.assert_not_called() + + assert cli.get_command(ctx, 'product') is grp_product + assert cli.get_command(ctx, 'product') is grp_product + load.assert_called_once() + assert cli.lazy_commands == {} + + cli.add_lazy_command('product', 'Manage product definitions.', None) + assert cli.lazy_commands == {} + + +def test_lazy_group_without_commands_has_no_commands_section(): + cli = LazyGroup() + + assert 'Commands' not in cli.get_help(click.Context(cli)) + + +def test_help_imports_no_command_group(): + code = ( + 'import sys\n' + "sys.argv = ['ccli', '--help']\n" + 'from connect.cli.ccli import main\n' + 'main()\n' + "print(sorted(m for m in sys.modules if m.startswith('connect.cli.plugins.')), file=sys.stderr)\n" + ) + + result = subprocess.run( + [sys.executable, '-c', code], capture_output=True, text=True, check=True + ) + + assert 'Manage product definitions.' in result.stdout + assert result.stderr.strip().splitlines()[-1] == '[]'