Search code examples
pythonflaskcommand-line-interfacefactory-patternpython-click

Is it possible to use the "app factory" pattern from Flask with Click CLI applications?


Imagine I have a big CLI application with many different commands (think, for example image-magick).

I wanted to organize this application into modules and etc. So, there would be a master click.group somewhere:

#main.py file
@click.group()
def my_app():
    pass

if __name__ == "__main__":
    my_app()

that can be imported in each module that define a command:

from main import my_app

# command_x.py
@my_app.command() 
def command_x():
    pass

The problem is that I run into a circular import problem, since the main.py file knows nothing about command_x.py and I would have to import it before calling the main section.

This happens in Flask too and is usually dealt with the app factory pattern. Usually you would have the app being created before the views:

app = Flask("my_app")

@my_app.route("/")
def view_x():
   pass

if __name__ == "__main__":
    app.run()

In the app factory pattern you postpone the "registration" of the blueprints:

# blueprints.py
blueprint = Blueprint(yaddayadda)

@blueprint.route("/")
def view_x():
    pass

And make a factory that knows how to build the app and register the blueprints:

#app_factory.py
from blueprints import view_x

def create_app():
    app = Flask()
    view_x.init_app(app)
    return app

And you can then create a script to run the app:

#main.py

from app_factory import create_app

if __name__ == "__main__":
    app = create_app()
    app.run()

Can a similar pattern be used with Click? Could I just create a "click app" (maybe extending click.Group) where I register the "controllers" which are the individual commands?


Solution

  • Maybe late, but I was also searching for a solution to put commands to separate modules. Simply use a decorator to inject commands from modules:

    #main.py file
    import click
    import commands
    
    def lazyloader(f):
        # f is an instance of click.Group
        f.add_command(commands.my_command)
        return f
    
    @lazyloader
    @click.group()
    def my_app():
        pass
    
    if __name__ == "__main__":
        my_app()
    

    The separated command can use the usual decorators from click.

    #commands.py
    import click
    
    @click.command()
    def my_command():
        pass