将 Invoke 作为库使用

虽然大部分文档涉及面向用户/CLI 的任务管理和命令执行用例,但 Invoke 的设计使其组成部分可以被高级用户独立使用——无论是开箱即用还是只需最少的额外工作。CLI 解析、子进程命令执行、任务组织等都是作为广泛分离的关注点编写的。

本文档概述了已知可行的用例(因为像 Fabric 这样的下游工具已经在使用它们)。

将 Invoke 的 CLI 模块重用作独立的二进制文件

主要用例是分发你自己的程序,该程序在底层使用 Invoke,绑定到不同的二进制名称,并且通常设置特定的任务 命名空间 作为默认值。(这在某种程度上类似于标准库中的 argparse。)在某些情况下,还需要删除、替换和/或添加核心 CLI 标志。

设置

假设你想分发名为 tester 的测试运行器,提供两个子命令 unitintegration,这样用户可以 pip install tester 并访问诸如 tester unittester integrationtester integration --fail-fast 等命令。

首先,与任何提供 CLI ‘二进制文件’的独立 Python 包一样,你需要在 setup.py 中告知你的入口点:

setup(
    name='tester',
    version='0.1.0',
    packages=['tester'],
    install_requires=['invoke'],
    entry_points={
        'console_scripts': ['tester = tester.main:program.run']
    }
)

备注

这只是示例片段,并不是完全有效的 setup.py;如果你不了解 Python 打包的工作原理,好的起点是 Python 打包用户指南

这里的内容并不是 Invoke 特有的——它是一种标准的方式,告诉 Python 安装 tester 脚本,该脚本执行 tester.main 模块中定义的 program 对象的 run 方法。

创建 Program

tester/main.py 中,首先导入 Invoke 的公共 CLI 功能:

from invoke import Program

然后定义在 setup.py 中引用的 program 对象,它是简单的 Program 来承担繁重的工作,首先给它版本号:

program = Program(version='0.1.0')

此时,安装 tester 将提供与 Invoke 的 内置 CLI 工具 相同的功能,只是命名为 tester 并暴露其自己的版本号:

$ tester --version
Tester 0.1.0
$ tester --help
Usage: tester [--core-opts] task1 [--task1-opts] ... taskN [--taskN-opts]

Core options:
    ... core Invoke options here ...

$ tester --list
Can't find any collection named 'tasks'!

这目前对没有太大帮助——还没有任何子命令(而且用户不关心任意的‘任务’,因此 Invoke 自己的默认 --help--list 输出并不合适)。

指定子命令

为了让 tester 暴露 unitintegration 子命令,需要在常规的 Invoke 任务模块或 命名空间 中定义它们。对于示例,将创建 tester/tasks.py (但正如你稍后将看到的,这也是任意的,可以是任何你喜欢的名称):

from invoke import task

@task
def unit(c):
    print("Running unit tests!")

@task
def integration(c):
    print("Running integration tests!")

构建名称空间 中所述,你可以随意安排这个模块——上面的代码片段为了简洁起见使用了隐式命名空间。

备注

重要的是要意识到这些‘子命令’并没有什么特别之处——你可以像使用普通 Invoke 一样轻松地运行它们,例如通过 invoke --collection=tester.tasks --list

现在是有用的部分:通过 namespace 关键字参数告诉我们的自定义 Program,这个任务命名空间应该用作 tester 的子命令:

from invoke import Collection, Program
from tester import tasks

program = Program(namespace=Collection.from_module(tasks), version='0.1.0')

结果?

$ tester --version
Tester 0.1.0
$ tester --help
Usage: tester [--core-opts] <subcommand> [--subcommand-opts] ...

Core options:
  ... core options here, minus task-related ones ...

Subcommands:
  unit
  integration

$ tester --list
No idea what '--list' is!
$ tester unit
Running unit tests!

Notice how the ‘usage’ line changed (to specify ‘subcommands’ instead of ‘tasks’); the list of specific subcommands is now printed as part of --help; and --list has been removed from the options.

You can enable tab-completion for your distinct binary and subcommands.

Modifying core parser arguments

A common need for this use case is tweaking the core parser arguments. Program makes it easy: default core Arguments are returned by Program.core_args. Extend this method’s return value with super and you’re done:

# Presumably, this is your setup.py-designated CLI module...

from invoke import Program, Argument

class MyProgram(Program):
    def core_args(self):
        core_args = super().core_args()
        extra_args = [
            Argument(names=('foo', 'f'), help="Foo the bars"),
            # ...
        ]
        return core_args + extra_args

program = MyProgram()

警告

We don’t recommend omitting any of the existing core arguments; a lot of basic functionality relies on their existence, even when left to default values.

Customizing the configuration system’s defaults

Besides the CLI-oriented content of the previous section, another area of functionality that frequently needs updating when redistributing an Invoke codebase (CLI or no CLI) is configuration. There are typically two concerns here:

  • Configuration filenames and the env var prefix - crucial if you ever expect your users to use the configuration system;

  • Default configuration values - less critical (most defaults aren’t labeled with anything Invoke-specific) but still sometimes desirable.

备注

Both of these involve subclassing Config (and, if using the CLI machinery, informing your Program to use that subclass instead of the default one.)

Changing filenames and/or env var prefix

By default, Invoke’s config system looks for files like /etc/invoke.yaml, ~/.invoke.json, etc. If you’re distributing client code named something else, like the Tester example earlier, you might instead want the config system to load /etc/tester.json or $CWD/tester.py.

Similarly, the environment variable config level looks for env vars like INVOKE_RUN_ECHO; you might prefer TESTER_RUN_ECHO.

There are a few Config attributes controlling these values:

  • prefix: A generic, catchall prefix used directly as the file prefix, and used via all-caps as the env var prefix;

  • file_prefix: For overriding just the filename prefix - otherwise, it defaults to the value of prefix;

  • env_prefix: For overriding just the env var prefix - as you might have guessed, it too defaults to the value of prefix.

Continuing our ‘Tester’ example, you’d do something like this:

from invoke import Config

class TesterConfig(Config):
    prefix = 'tester'

Or, to seek tester.yaml as before, but TEST_RUN_ECHO instead of TESTER_RUN_ECHO:

class TesterConfig(Config):
    prefix = 'tester'
    env_prefix = 'TEST'

Modifying default config values

Default config values are simple - they’re just the return value of the staticmethod Config.global_defaults, so override that and return whatever you like - ideally something based on the superclass’ values, as many defaults are assumed to exist by the rest of the system. (The helper function invoke.config.merge_dicts can be useful here.)

For example, say you want Tester to always echo shell commands by default when your codebase calls Context.run:

from invoke import Program
from invoke.config import Config, merge_dicts

class TesterConfig(Config):
    @staticmethod
    def global_defaults():
        their_defaults = Config.global_defaults()
        my_defaults = {
            'run': {
                'echo': True,
            },
        }
        return merge_dicts(their_defaults, my_defaults)

program = Program(config_class=TesterConfig, version='0.1.0')

For reference, Invoke’s own base defaults (the…default defaults, you could say) are documented at 默认配置值.