继之前的《tox 教程》和新翻译的《nox文档》之后,我们继续聊聊Python任务自动化的话题。nox的作者去年在PyconUS做了一个名为《Break the Cycle: Three excellent Python tools to automate repetitive tasks》的分享(B站查看地址:https://b23.tv/av86640235),她介绍了三个任务自动化工具:tox、nox和invoke,本期的主题article恰好是最后一次调用。1.调用可以做什么?Invoke从著名的远程部署工具Fabric中分离出来,与paramiko一起是Fabric的两个核心基础组件。除了作为命令行工具,它更侧重于“任务执行”,可以对任务进行标记和组织,通过CLI(command-lineinterface,即命令行界面)和shell命令来执行任务。它也是一个任务自动化工具。invoke在侧重点上和我们之前介绍的tox/nox不同:tox/nox主要是做打包、测试、持续集成等方面的自动化(当然他们能做的不止这些)invoke更通用,可以用于任何需要“执行任务”的场景。它可以是不相关的任务组或具有顺序依赖性的分步工作流程。Invoke在Github上有2.7Kstars,很受欢迎,接下来看看它是怎么用的?2.如何调用?首先,安装很简单:pipinstallinvoke。其次,有以下几个简单使用的元素:任务文件。创建一个tasks.py文件。@task装饰器。为函数添加@task装饰器,将函数标记为任务,接受invoke的调度管理。上下文参数。给被修饰函数添加一个上下文参数(contextargument),注意必须是第一个参数,命名约定可以是c或者ctx或者context。命令行执行。在命令行执行invoke--list查看所有任务,运行invokexxx执行名为xxx的任务。命令行中的“invoke”可以缩写为“inv”。这是一个简单的例子:#filename:tasks.pyfrominvokeimporttask@taskdefhello(c):print("Helloworld!")@taskdefgreet(c,name):c.run(f"echo{name}来on!)在上面的代码中,我们定义了两个任务:“hello”任务调用了Python内置的print函数,会打印出一个字符串“Helloworld!”greet任务调用run()方法,可以执行shell命令,本例中还可以接收一个参数,在shell命令中echo可以理解为打印,所以这也是一个打印任务,会打印出“xxxcomeon!”(xxx是我们传递的参数)上面的代码写在tasks.py文件中,首先从invokeimporttask导入装饰器,@task装饰器可以不带参数也可以带parameters(见下一节),被它修饰的函数是一个task,上下文参数(即上例中的“c”)必须显式指定,如果缺少这个参数,执行过程中会抛出异常execution:"TypeError:TasksmusthaveaninitialContextargument!"然后在同级目录的tasks.py文件中,打开命令行窗口,执行命令。如果在执行位置找不到任务文件,会报错:“找不到任何名为‘tasks’的集合!”正常下在某些情况下,您可以通过执行inv--list或inv-l查看所有任务的列表(按字母顺序排序):>>>inv-lAvailabletasks:greethello我们依次执行这两个任务。传递参数时,可以默认按位置参数传递参数,也可以指定关键字传递参数。结果是:>>>invhelloHelloworld!>>>invgreet武汉加油!>>>invgreet--name="武汉"武汉加油!当缺少参数时,报错:'greet'didnotreceivedrequiredpositionalarguments:'name';当有冗余参数时,报错:Noideawhat'???'是!3.invoke怎么用好?介绍完invoke的简单用法,我们知道了它需要的几个元素,也大致知道了它的使用步骤,接下来就是它的其他用法了。3.1添加帮助信息在上面的例子中,“inv-l”只能看到任务名称,缺少必要的辅助信息。为了增强可读性,我们可以这样写:@task(help={'name':'Aparamfortest'})defgreet(c,name):"""Atestforshellcommand.第二行。"""c.run(f"echo{name}Comeon!")其中,docstring的第一行一行内容将作为摘录显示在"inv-l"的查询结果中,在"inv--help"中会对应显示@task的完整内容和帮助内容:>>>inv-lAvailabletasks:greetshell命令测试。>>>inv--helpgreetUsage:inv[oke][--core-opts]greet[--options][此处的其他任务...]Docstring:对shell命令的测试。Secondline.Options:-nSTRING,--name=STRINGAparamfortest3.2任务的分解与组合通常一个大任务可以分解成一组小任务,反之也可以将一系列小任务串联起来成一项大任务。在对任务进行分解、抽象、组合时,这里有两种思路:内部分解和外部统一:只定义一个@task任务作为整体的任务入口,实际的处理逻辑可以抽象成多个方法,但外部没有感知他们多点呈现,单点总结:定义多个@task任务,外部可以分别感知和调用它们,同时组合相关任务。当调用一个任务时,其他相关的任务也同时执行任务的第一个思想很容易理解,易于实现和使用,但它的缺点是缺乏灵活性,很难单独执行一些/一些子任务。适用于相对独立的单任务,通常不用invoke就可以搞定(使用invoke的好处是有命令行支持)。第二种思路更加灵活,既方便了单个任务的执行,也方便了多个任务的组合执行。其实这个场景才是invoke价值最大的场景。那么,invoke是如何实现分步任务的组合呢?可以在@task装饰器的“pre”和“post”参数中指定,分别代表pre-task和post-task:@taskdefclean(c):c.run("echoclean")@taskdefmessage(c):c.run("echomessage")@task(pre=[clean],post=[message])defbuild(c):c.run("echobuild")clean和消息任务用作subtasks并且可以分开也可以作为构建任务的前任务和后任务的组合:>>>invcleanclean>>>invmessagemessage>>>invbuildcleanbuildmessage这两个参数是列表类型,多任务可以设置。此外,默认情况下,@task装饰器的位置参数将被视为前置任务。按照上面的代码,我们写一个:@task(clean,message)deftest(c):c.run("echotest")然后执行,你会发现这两个参数都被当做前置任务:>>>invtestcleanmessagetest3.3模块拆分与整合如果要管理很多相对独立的大型任务,或者需要多个团队维护各自的任务,那么就需要对tasks.py进行拆分与整合。比如有多个tasks.py,都是比较完整和独立的任务模块,把所有内容放在一个文件中不方便。那么,如何对它们进行有效的整合和管理呢?invoke对此提供支持。首先,只保留一个名为“tasks.py”的文件,其次,在此文件中导入其他重命名的任务文件,最后,使用invoke的Collection类将它们关联起来。我们将本文中的第一个示例文件重命名为task1.py,并新建一个tasks.py文件,内容如下:("echodeploy")namespace=Collection(task1,deploy)每个py文件都有一个独立的命名空间,这里我们可以使用Collection创建一个新的命名空间来实现对所有任务的统一管理。效果如下:>>>inv-lAvailabletasks:deploytask1.greettask1.hello>>>invdeploydeploy>>>invtask1.helloHelloworld!>>>invtask1.greet武汉加油武汉!关于不同任务模块的导入、嵌套、混合、别名等诸多细节,请参考官方文档。3.4交互操作某些任务可能需要交互输入,比如需要输入“y”,只有按下回车键才会继续执行。如果在任务执行期间需要人工参与,则任务自动化的能力会大大降低。invoke提供程序运行过程中的监控能力,可以监控stdout和stderr,支持在stdin中输入必要的信息。例如,假设一个任务(excitable-program)在执行时会提示“Areyouready?[y/n]”,只有输入“y”并按下回车键后,才会进行后续操作。然后,在代码中指定responses参数的内容,只要监听到匹配信息,程序就会自动执行相应的操作:responses={r"Areyouready?\[y/n\]":"y\n"}ctx.run("excitable-program",responses=responses)responses为字典类型,键值对分别为监控内容和响应内容。需要注意的是,key-value会被当作正则表达式处理,所以像本例中的方括号必须先进行转义。3.5作为命令行工具库Python有很多好用的命令行工具库,比如标准库中的argparse,Flask作者点击开源,Google开源fire等,调用即可也可用作命令行工具库。(PS:Prodesire的一位同学写了《Python命令行之旅》系列文章,详细介绍了其他几个命令行工具库的使用方法,大部分转载于公众号《Python猫》,有兴趣的同学可以查看历史文章。)其实,Fabric项目最初是将invoke分离到一个单独的库中,只是让它承担解析命令行和执行子命令的任务。因此,invoke除了作为一个自动化的任务管理工具,还可以用来开发命令行工具。官方文档中给出了一个例子,我们可以了解它的基本用法。假设我们要开发一个tester工具供用户使用pipinstalltester安装,这个工具提供了两个执行命令:testerunit和testerintegration。这两个子命令需要在tasks.py文件中定义:#tasks.pyfrominvokeimporttask@taskdefunit(c):print("Runningunittests!")@taskdefintegration(c):print("Runningintegrationtests!")然后在程序入口文件中引入:#main.pyfrominvokeimportCollection,Programfromtesterimporttasksprogram=Program(namespace=Collection.from_module(tasks),version='0.1.0')最后声明入口在包文件中Function:#setup.pysetup(name='tester',version='0.1.0',packages=['tester'],install_requires=['invoke'],entry_points={'console_scripts':['tester=tester.main:program.run']})这样打包发布的库是一个功能齐全的命令行工具:$tester--versionTester0.1.0$tester--helpUsage:tester[--core-opts]
