Flask 源码解析-2.CLI 模块

0. 上文

上一篇在 setup() 配置的最后提到了控制台入口函数 flask.cli:main

entry_points={"console_scripts": ["flask = flask.cli:main"]},

本文继续探索 flask 的 cli 源码 (command line interface),即通过命令行动态交互使用 flask,而不是启动 flask 直接运行程序。

1. main 函数

通过入口函数找到对应程序,在源码 src/flask/cli 中的 main 函数。

def main(as_module=False):
    # TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed
    cli.main(args=sys.argv[1:], prog_name="python -m flask" if as_module else None)

main 函数只有一个参数 as_module 表明是否是模块化启动,默认值是否。程序中只有一行,通过 cli 来启动,cli 是 FlaskGroup 的一个 instance,main 是 FlaskGroup 的一个重要 method,这些内容后文展开。
在启动过程中传入了两个参数,args 和 prog_name。args 调用了内置系统函数获得命令行中传入的参数,argv[1:] 的含义是命令行第一个空格后的参数,例如 python -m flask 0 代表的是 python,args[1:] 获得了 -m 和 flask。prog_name 根据 as_module 设置,如果 as_module 为真则设置为 python -m flask,否则为 None。

2. cli instance

上文提到 cli 是 FlaskGroup 的一个 instance,接下来要探讨 cli 的实例化和 FlaskGroup 类。

cli = FlaskGroup(
    help="""\
A general utility script for Flask applications.

Provides commands from Flask, extensions, and the application. Loads the
application defined in the FLASK_APP environment variable, or from a wsgi.py
file. Setting the FLASK_ENV environment variable to 'development' will enable
debug mode.

\b
  {prefix}{cmd} FLASK_APP=hello.py
  {prefix}{cmd} FLASK_ENV=development
  {prefix}flask run
""".format(
        cmd="export" if os.name == "posix" else "set",
        prefix="$ " if os.name == "posix" else "> ",
    )
)

help 参数用来提示开发者如何使用交互界面,显示内容根据 OS 还进行了调整,"posix" 的使用 export 来设置环境变量,$ 作为提示符,其他使用 set 来设置环境变量,> 作为提示符。

image

触发 help 显示,提示开发者如何使用参数。
image

根据提示加入命令行参数,这里查询了 flask version。

3. FlaskGroup 初始化

FlaskGroup 继承于 AppGroup,主要作用是补充 command line 设置参数,通常开发者不会使用这个类,大部分程序执行的是 AppGroup。

class FlaskGroup(AppGroup):
        def __init__(
        self,
        add_default_commands=True,
        create_app=None,
        add_version_option=True,
        load_dotenv=True,
        set_debug_flag=True,
        **extra
    ):
        params = list(extra.pop("params", None) or ())

        if add_version_option:
            params.append(version_option)

        AppGroup.__init__(self, params=params, **extra)
        self.create_app = create_app
        self.load_dotenv = load_dotenv
        self.set_debug_flag = set_debug_flag

        if add_default_commands:
            self.add_command(run_command)
            self.add_command(shell_command)
            self.add_command(routes_command)

        self._loaded_plugin_commands = False

上一节中的 help 参数是通过 FlaskGroup init 方法的 **extra 传入的。

  • add_default_commands 参数用来控制是否从命令行读取配置参数,所有的 params 都是从命令行读取通过 **extra 传入。params = list(extra.pop("params", None) or ())
  • add_version_option 参数用来控制是否支持命令行 --version 查询。如果支持,则在 params 中添加 version 信息。params.append(version_option),其中 version_option 是一个全局变量
    version_option = click.Option(
        ["--version"],
        help="Show the flask version",
        expose_value=False,
        callback=get_version,
        is_flag=True,
        is_eager=True,
    )
    
    click 是 python 的内置函数,这里不深入探讨,这段程序的含义是配置了 version 信息。
  • create_app 参数用来控制是否从命令行传入控制参数后回调 app。
  • load_dotenv 参数用来控制是否从最近的 .env.flaskenv 文件读取设置环境变量,如果为真则会根据发现的第一个文件设置工作路径。
  • set_debug_flag 参数用来在交互环境下设置 debug 模式。

初始化的过程是:

  1. 先从 **extra 获取 params 参数;
  2. 如果 add_version_option 为真,则添加设置 version 信息;
  3. 从 AppGroup 实例化程序,传入 paramsextra 参数;
  4. 设置 create_app, load_dotenv, set_debug_flag
  5. 如果 add_default_commands 为真,则添加三个方法,run_command, shell_command, routes_command,添加方法的实现是 python 源码,这里不做探讨;
  6. 设置 _loaded_plugin_commands 为假

4. AppGroup

上节提到 FlaskGroup 继承自 AppGroup,AppGroup 继承自 click.Group,这是 python 源码,这里不做探讨。

class AppGroup(click.Group):
    def command(self, *args, **kwargs):
        wrap_for_ctx = kwargs.pop("with_appcontext", True)

        def decorator(f):
            if wrap_for_ctx:
                f = with_appcontext(f)
            return click.Group.command(self, *args, **kwargs)(f)

        return decorator

    def group(self, *args, **kwargs):
        kwargs.setdefault("cls", AppGroup)
        return click.Group.group(self, *args, **kwargs)

AppGroup 重写了 click.Group 的 commandgroup 方法。

  • command 重写和原方法类似,不过加入了回调的装饰器 with_appcontext,这种写法是函数式编程,传入一个函数 f 再返回一个函数 decorator,实现对内部函数的回调。

    def with_appcontext(f):
        @click.pass_context
        def decorator(__ctx, *args, **kwargs):
            with __ctx.ensure_object(ScriptInfo).load_app().app_context():
                return __ctx.invoke(f, *args, **kwargs)
    
        return update_wrapper(decorator, f)
    

    实现了在脚本中加载程序应用的上下文环境,源码中 shell_command 和 routes_command 使用了该装饰器。

  • group 重写和原方法类似,只是指定了 AppGroupgroup class

5.FlaskGroup 方法

FlaskGroup 同时定义了四种方法:
_load_plugin_commands 实现从插件加载指令;
get_command 重写了 python 中 click.Group 的 get_command 方法,加入了命令行传入的参数;
list_command 重写了 python 中 click.Group 的 list_command 方法,实现对 application 和 built-in 命令的列表;
main 方法是主要逻辑控制方法,全局设置程序是从 CLI 启动的,所有方法实现 CLI 预设方式,忽略一些错误报警。

class FlaskGroup(AppGroup):
    def main(self, *args, **kwargs):
        os.environ["FLASK_RUN_FROM_CLI"] = "true"
        if get_load_dotenv(self.load_dotenv):
            load_dotenv()

        obj = kwargs.get("obj")

        if obj is None:
            obj = ScriptInfo(
                create_app=self.create_app, set_debug_flag=self.set_debug_flag
            )

        kwargs["obj"] = obj
        kwargs.setdefault("auto_envvar_prefix", "FLASK")
        return super(FlaskGroup, self).main(*args, **kwargs)

先设置全局变量 FLASK_RUN_FROM_CLI 为真,如果需要从 .env 或者 .flaskenv 加载环境变量则加载。获取传入参数中的 obj,如果存在,则增加 create_app, set_debug_flag 控制,将所有环境变量的前缀默认设置为 FLASK,最后调用父类方法实例化。

6.总结

最后使用一种脑图总结 CLI 模块的全部逻辑关系

image

7.相关文章

Flask 源码解析-1.安装配置

你可能感兴趣的:(Flask 源码解析-2.CLI 模块)