click icon indicating copy to clipboard operation
click copied to clipboard

Extend Complex Application docs with Lazy Loading

Open sirosen opened this issue 1 year ago • 0 comments

Some meta/background about this PR:

We've been using click extremely happily at my work for years now (6 years, I think?). At time of writing, we have a CLI package built with click with 94 commands and 26 groups. Perhaps unsurprisingly, it has become pretty slow to start because so much work happens just to import all of the commands.

We did a body of work around lazy-loading of libraries, especially requests, which cut down import times a lot. But when I timed our --help invocations, I found they were still taking 0.3s. i.e. Fast enough to be satisfactory, but slow enough that a human can notice the lag. Introducing lazy importing of subcommands cut the time down to 0.1s, which is fast enough that it qualifies anecdotally as "instantaneous". Assuming the times are comparable for completions, that meant we still had laggy completion support until we introduced the lazy subcommand loader.

I've read the solutions in #945 , and I never felt comfortable with them for our usage. With the caveat that there's obviously some strong authorship bias here, I decided to open this PR with a slightly simplified version of our solution as a doc addition in the Complex Applications section. The "important" example part is ~30 LOC, but of course it needs a lot of explanation, prose, and usage examples.

I've tried to keep this small and readable, without placing an unfair burden on click to put too much example code into the docs. Arguably, this could have been shown in an even simpler way (without the lazy_subcommands dict, and just some static data). But at the same time, it seems better to have a more general solution documented to make it clear that you just need to hook logic into get_command and list_commands.

I'm open to feedback, including possibly rejecting this PR in favor of something smaller and more targeted. The only thing I don't want to do is produce an extension library which provides this for people to use -- I'm already stretched pretty thin and simply can't commit to supporting such a thing.


Resolves #945

The goal of this example is to show how one might subclass Group, overriding get_command and list_commands with some importlib usage and a dict, and produce a viable lazy-loading solution.

One significant benefit of offering an example is that it provides space to explain how and when get_command is invoked. i.e. Users might be surprised to realize that subcommands are resolved during completion scenarios.

At the risk of diverging a small amount into what some would consider "general deferred import semantics in python", the section closes out with an example of a click.command decorated function which contains a deferred import. For codebases which have significant at-import-time work (e.g. importing requests or urllib3), this strategy is probably more useful than lazy loading of subcommands.

Other variations on this same solution are possible, e.g. using importlib.load_module(module_name, self.callback.__module__) to handle imports relative to the definition of the callback function. However, any further work in this space is left as an exercise to readers of the doc.

Importantly, unlike some solutions already discussed in #945, nothing about this is application-specific. The example LazyGroup implementation does not encode any knowledge about the structure of the codebase in which it is used, which means that users can copy-paste the example and expect it to work (within reason). Because this also introduces the risk of reckless copy-paste by novice users, a warning is included in the doc that is meant to point at a certain level of application maturity which should be reached before this strategy can be safely applied.

sirosen avatar Sep 01 '22 14:09 sirosen