click icon indicating copy to clipboard operation
click copied to clipboard

Documentation doesn't describe "lazy loading of subcommands at runtime"

Open ncraike opened this issue 6 years ago • 4 comments

The documentation home page includes:

Click in three points:

  • arbitrary nesting of commands
  • automatic help page generation
  • supports lazy loading of subcommands at runtime

How is "lazy-loading of subcommands at runtime" supported? The various methods of creating command groups and attaching sub-commands described in the quickstart guide all require importing the sub-command first.

The closest I can find is a demonstration of how lazy-loading of commands from plugins might be implemented with a custom multi-command class. Is this the "lazy loading of subcommands at runtime" which the documentation describes?

ncraike avatar Mar 08 '18 06:03 ncraike

I think so. Another way do do lazy loading would be something like this: https://github.com/indico/indico/blob/master/indico/cli/util.py#L95 / https://github.com/indico/indico/blob/master/indico/cli/core.py#L68

ThiefMaster avatar Mar 08 '18 06:03 ThiefMaster

Yes this would be a good thing. In larger implementation long import times especially effect the snappiness of shell completion. (my project with 16 command groups, takes 2.5 seconds for a completion step....)

scoopex avatar Jan 16 '22 17:01 scoopex

I also found this quite misleading, can this third point be removed from the docs? There doesn't seem to be anything in Click that does this out of the box

BernardZhao avatar Feb 28 '22 21:02 BernardZhao

I've implemented lazy sub-commands in my way, FYI.


import sys
from importlib import import_module

from my_project.db import alembic
import click


def import_from_string(spec):
    """
    Thanks to https://github.com/encode/django-rest-framework/blob/master/rest_framework/settings.py#L170
    Example:
        import_from_string('django_filters.rest_framework.DjangoFilterBackend')
        engine = conf['ENGINE']
        engine = import_from_string(engine) if isinstance(engine, six.string_types) else engine
    """
    try:
        # Nod to tastypie's use of importlib.
        parts = spec.split('.')
        module_path, class_name = '.'.join(parts[:-1]), parts[-1]
        module = import_module(module_path)
        return getattr(module, class_name)
    except (ImportError, AttributeError) as e:
        msg = "Could not import '%s'. %s: %s." % (spec, e.__class__.__name__, e)
        raise ImportError(msg)


@click.group()
def manager():
    """MyProject Management Tools."""


def get_db_cmd():
    db_cmd = alembic.get_click_cli("db")
    db_cmd.help = 'Design database schema with confidence.'
    return db_cmd


cmd_factories = dict((
    ('db', get_db_cmd),
    ('dev', lambda: import_from_string('my_project.commands.dev.dev')),
    ('shell', lambda: import_from_string('my_project.commands.shell.shell')),
    ('psql', lambda: import_from_string('my_project.commands.psql.psql')),
    ('protoc', lambda: import_from_string('my_project.commands.protoc.protoc')),
    ('cache', lambda: import_from_string('my_project.commands.cache.cache')),
    ('test', lambda: import_from_string('my_project.commands.testing.test')),
    ...
))

if __name__ == '__main__':
    if len(sys.argv) > 1 and (cmd_name := sys.argv[1]) in cmd_factories:
        # Construct sub-command only as needed.
        manager.add_command(cmd_factories[cmd_name](), name=cmd_name)
    else:
        # For user can list all sub-commands.
        for cmd_name, fct in cmd_factories.items():
            manager.add_command(fct(), name=cmd_name)

    manager()

wonderbeyond avatar Apr 03 '22 15:04 wonderbeyond

Fixed by https://github.com/pallets/click/pull/2348, can be found at https://click.palletsprojects.com/complex/#lazily-loading-subcommands

davidism avatar Jun 30 '23 19:06 davidism