从 Flask-script 迁移到 Click

Migrating from flask-script to click

Posted by Bryan on March 19, 2019

基础介绍

Flask-script 是一个适用于 Flask 框架的拓展,可以帮助更方便地写脚本,方便线上执行一些临时的代码。使用的流程如下所示:

首先你需要在 Flask 项目下新建一个manage.py 的脚本文件,此后增加临时执行的方法,比如下面增加一个hello 方法

# manage.py

from flask import Flask
from flask_script import Manager

app = Flask(__name__)
manager = Manager(app)

@manager.command
def hello():
    print "hello"

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

之后可以通过下面的命令直接执行hello 方法

python manage.py hello

通过 Flask-script 可以将临时执行的脚本统一管理,需要执行时,就可以很方便地运行起来

但是前一阵子做项目的版本升级,去确认了一下 Flask-script 的版本,发现没有新版本出现,而Flask-script 对应的 github 也显示项目不再维护了,而 Flask 项目也引入了新的拓展 Click 可以代替 Flask-script,于是替换之

Click 介绍

Click 是一个 Flask 项目引入的一个拓展包,可以帮助写出更漂亮的命令行接口,为了看下具体的使用体验,可以看下官方的小例子:

# hello.py

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

最后执行的结果如下所示:

$ python hello.py --count=3
Your name: John
Hello John!
Hello John!
Hello John!

可以看到执行上面的命令,接受两个参数,一个是--count 通过执行命令时手动添加参数来实现,另一个是name 通过执行时交互式获取输入。

可以看到通过 click 提供的装饰器,可以很方便地实现比较强大的交互式的功能,但是也注意到一个问题,之前的 Flask-script 可以在执行时通过参数指定执行的命令,而 Click 看起来是直接执行对应的命令,这样就在项目中,就没有办法很方便地任意执行命令了,那么有办法做到吗?这边先卖个关子,最后再进行解答

click.command

Click 中最基础的就是单个命令(command),通过装饰器@click.command() 可以将一个 Python 方法转换为一个 Click 命令。最简单的情况下如下所示:

# hello_world.py

import click

@click.command()
def hello():
    click.echo('Hello World!')
    
if __name__ == '__main__':
   hello()

而装饰为命令后需要执行此命令,使用 python hello_world.py ,即可执行此命令,最基础的情况下与直接执行此方法看起来没有什么区别

click.option

Click 中的 option 是命令中的参数获取方式,可以通过不同的形式获取输入参数。

最基础的情况下可以使用 option 表示一个基础的输入参数,使用类似如下所示:

@click.command()
@click.option('--n', default=1)
def dots(n):
    click.echo('.' * n)

上面的代码支持参数-—n, 执行时类似如下所示:

$ dots --n=2
..

除了最基础的情况外,还有一些更高级的 option 的用法:

  • 使用nargs 可以支持多个同样类型的参数,代码类似如下所示:
@click.option('--pos', nargs=2, type=float)
  • 使用类型元组支持多个不同类型的参数,代码如下所示:
@click.option('--item', type=(str, int))
  • 采用两个使用/ 分开的参数分别表示布尔真和假,代码如下所示:
@click.option('--shout/--no-shout', default=False)
  • 使用click.choice() 用于将参数从特定的列表中进行选择, 代码如下所示:
@click.option('--hash-type', type=click.Choice(['md5', 'sha1']))
  • 使用prompt 可以实现交互式输入值,代码如下所示:
@click.option('--name', prompt='Your name please')
  • 使用hide_input 可以实现隐藏输入,使用confirmation_prompt 可以实现两次输入并比对,适合密码输入的情况,代码如下所示:
@click.option('--password', prompt=True, hide_input=True,
              confirmation_prompt=True)
  • 使用envvar 可以获取环境变量中的值,代码如下所示:
@click.option('--username', envvar='USERNAME')
  • 使用IntRange 可以获取特定范围内的值,代码如下所示:
@click.option('--digit', type=click.IntRange(0, 10))

click.argument

argument 与 option 类似,argument 是位置参数 (positional argument) ,argument 支持 option 部分功能

最基础的情况下,使用与 option 类似,一般需要指定参数对应的类型,如果不指定,会是 STRING 类型。代码如下所示:

@click.command()
@click.argument('filename')
def touch(filename):
    click.echo(filename)

同样,option 也支持一些更高级的用法:

  • 支持可变参数,利用nargs 可以支持可变的参数,设置nargs = -1 表示不限数量的参数,代码如下所示:
@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def copy(src, dst):
    for fn in src:
        click.echo('move %s to folder %s' % (fn, dst))

通过上面的代码可以看到src 参数是可以支持多个的,dst 只支持一个,运行结果如下所示:

$ copy foo.txt bar.txt my_folder
move foo.txt to folder my_folder
move bar.txt to folder my_folder
  • 支持文件类型,代码如下所示:
@click.argument('input', type=click.File('rb'))
  • 支持文件路径类型,代码如下所示:
@click.argument('f', type=click.Path(exists=True))
  • 同样也支持环境变量,代码如下所示:
@click.argument('files', nargs=-1, type=click.Path())

命令组

可以将命令分组进行管理,在使用中可以使用@click.group() 装饰器指定组,此后可以使用组名构建子命令。同时可以设定组内命令的回调,组内的任意子命令执行时,都可以触发回调。使用如下所示:

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    click.echo('Debug mode is %s' % ('on' if debug else 'off'))

@cli.command()
def sync():
    click.echo('Synching')

在上面的代码中可以看到,使用@click.group() 装饰器装饰了cli() 方法,cli 可以理解为组名,后续即可使用@cli.command() 代替@click.command() 装饰sync() 方法,sync() 方法就是组内的子命令,而cli() 方法即为子命令执行后出发的回调。可以看到执行子命令sync() 方法后的现场如下所示:

$ tool.py --debug sync
Debug mode is on
Synching

可以看到上面的执行情况,子命令sync 执行时,不仅执行了sync() 方法中的代码,还执行了回调方法cli() 方法中的代码。

在使用命令组时,还可以在命令组和子命令之间进行参数的传递,此时可以使用@click.pass_context 装饰器进行,具体的细节可以查看官方文档

从 Flask-script 到 Click

从前面的介绍可以看到,Click 与 Flask-script 的使用极其类似,而且更灵活,更强大。但是直接使用@click.command() 看起来似乎是每次执行确定的单个命令,有没有办法实现类似 Flask-script 一样任意选择的命令呢 ?

答案是可以的,但是必须借助于命令组,建立一个命令组,执行的命令都是命令组的子命令,执行时就可以通过参数指定执行的命令了,因此最终替代的 Flask-script 的 Click 代码如下所示:

# manage.py

@click.group()
def cli():
    pass

@cli.command()
def hello():
    click.echo('hello')

if __name__ == '__main__':
    cli()

对于上面的代码,可以直接执行python manage.py hello 从而执行hello() 方法中的代码

对于 Flask 框架而言,可以使用 Flask 中继承自 click.group() 的类 FlaskGroup ,使用更便利。通过一番折腾,可以将 Flask-script 的依赖彻底去除了。