Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 557
Rafael S. Calsaverini Avatar asked Apr 14 '15 15:04

Rafael S. Calsaverini


People also ask

How do I run a Flask app from the command line?

To run the app outside of the VS Code debugger, use the following steps from a terminal: Set an environment variable for FLASK_APP . On Linux and macOS, use export set FLASK_APP=webapp ; on Windows use set FLASK_APP=webapp . Navigate into the hello_app folder, then launch the program using python -m flask run .

What is Flask CLI?

cli module of the Flask project. FlaskGroup is a subclass of AppGroup that provides for loading more commands from a configured Flask app. Generally, only advanced use cases will need to use this class.

Which of the following methods registers a view function with a specific URL address?

route is a decorator used to match URLs to view functions in Flask apps.


2 Answers

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
like image 186
Karl Nickel Avatar answered Sep 28 '22 15:09

Karl Nickel


Ok, so I thought a little and it seems that the following could work. It's probably not a final solution but it seems to be an initial step.

I can extend the MultiCommand class:

# my_click_classes.py

import click

class ClickApp(click.MultiCommand):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.commands = {}

    def add_command(self, command_name, command):
        self.commands.update({command_name: command})

    def list_commands(self, ctx):
        return [name for name, _ in self.commands.items()]

    def get_command(self, ctx, name):
        return self.commands.get(name)

And the Command class:

class MyCommand(click.Command):

    def init_app(self, app):
        return app.add_command(self.name, self)

def mycommand(*args, **kwargs):
    return click.command(cls=MyCommand)

This allows you to have the commands defined in separated modules:

# commands.py

from my_click_classes import command

@command
def run():
    print("run!!!")

@command
def walk():
    print("walk...")

and the "app" in a separated module:

from my_click_classes import ClickApp
from commands import run, walk

app = ClickApp()
run.init_app(app)
walk.init_app(app)

if __name__ == '__main__':
   app()

Or even use the "app factory" pattern.

It maybe not a definitive solution though. If you guys can see any way to improve it, please let me know.

like image 30
Rafael S. Calsaverini Avatar answered Sep 28 '22 15:09

Rafael S. Calsaverini