click
click copied to clipboard
Documentation doesn't describe "lazy loading of subcommands at runtime"
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?
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
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....)
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
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()
Fixed by https://github.com/pallets/click/pull/2348, can be found at https://click.palletsprojects.com/complex/#lazily-loading-subcommands