是否可以将 Flask 中的 "app factory" 模式与 Click CLI 应用程序一起使用?

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

假设我有一个带有许多不同命令的大型 CLI 应用程序(想想,例如 image-magick)。

我想将这个应用程序组织成模块等。所以,某处会有一个大师click.group

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

if __name__ == "__main__":
    my_app()

可以在定义命令的每个模块中导入:

from main import my_app

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

问题是我 运行 陷入循环导入问题,因为 main.py 文件对 command_x.py 一无所知,我必须在调用主要部分之前导入它。

这种情况在 Flask 中也会发生,通常是应用工厂模式处理的。通常你会在视图之前创建应用程序:

app = Flask("my_app")

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

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

在应用工厂模式中,您推迟 "registration" 蓝图:

# blueprints.py
blueprint = Blueprint(yaddayadda)

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

并创建一个知道如何构建应用程序和注册蓝图的工厂:

#app_factory.py
from blueprints import view_x

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

然后您可以为 运行 应用程序创建一个脚本:

#main.py

from app_factory import create_app

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

Click 可以使用类似的模式吗?我可以只创建一个 "click app"(可能扩展 click.Group),我在其中注册 "controllers" 这是单独的命令吗?

好的,我想了想,下面的方法似乎可行。这可能不是最终解决方案,但它似乎是第一步。

我可以扩展 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)

和命令 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)

这允许您在单独的模块中定义命令:

# commands.py

from my_click_classes import command

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

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

和单独模块中的"app":

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()

或者甚至使用 "app factory" 模式。

虽然这可能不是最终的解决方案。如果你们能看到任何改进的方法,请告诉我。

可能晚了,但我也在寻找将命令放入单独模块的解决方案。只需使用装饰器从模块注入命令:

#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()

分离的命令可以使用来自 click 的常用装饰器。

#commands.py
import click

@click.command()
def my_command():
    pass