构建名称空间

加载单个任务模块的 base case 在一开始工作得很好,但高级用户通常需要更多的组织,比如将任务分割成嵌套命名空间树。

Collection 类提供了 API 来组织任务(和 their configuration)成一个树状结构。当被字符串引用时(例如在 CLI 或 pre/post 钩子中)嵌套命名空间中的任务使用点分隔语法,例如 docs.build

在本节中,我们将展示如何使用这个 API 灵活地构建名称空间,同时还允许使用最少的样板文件遵循 Python 包布局。

开始

未命名的 Collection 总是命名空间根;在隐式的基本情况下,Invoke 从 tasks 模块中的任务中为你创建一个。创建你自己的命名为 namespacens 的命名空间,以建立显式的命名空间(即跳过默认的 “pull in all Task objects” 行为):

from invoke import Collection

ns = Collection()
# or: namespace = Collection()

使用 Collection.add_task 添加任务。add_task 可以接受 Task 对象,例如由 task 装饰器生成的

from invoke import Collection, task

@task
def release(c):
    c.run("python setup.py sdist register upload")

ns = Collection()
ns.add_task(release)

我们的可用任务列表现在看起来像这样

$ invoke --list
Available tasks:

    release

命名你的任务

默认情况下,任务的函数名被用作它的命名空间标识符,但是你可以通过给 @task name 参数来覆盖它(例如,在定义的时候)或者 Collection.add_task (即在 binding/attachment 时)。

例如,假设你在你的任务模块中有一个变量名冲突——也许你想暴露 dir 任务,它遮蔽了 Python 内置。将函数本身命名为 dir 是个坏主意,但你可以将函数命名为 dir_,然后告诉 @task 这个 “real” 名字:

@task(name='dir')
def dir_(c):
    # ...

另一方面,您可能已经获得了任务对象,该对象不符合您希望在名称空间中的名称,可以在附加时重命名它。也许我们需要将 release 任务重命名为 deploy

ns = Collection()
ns.add_task(release, name='deploy')

结果是

$ invoke --list
Available tasks:

    deploy

备注

name kwarg 是 add_task 的第二个参数,所以那些匆忙的人可以把它写成

ns.add_task(release, 'deploy')

别名

任务可能有额外的名称或别名,给出 aliases 关键字参数;它们被附加到任何隐式或显式的 name 值,而不是替换

ns.add_task(release, aliases=('deploy', 'pypi'))

结果,同一个任务有三个名称

$ invoke --list
Available tasks:

    release
    deploy
    pypi

备注

便捷的装饰器 @task 是另一种设置别名的方法(例如 @task(aliases=('foo', 'bar')),这对于确保给定的任务总是有一些别名设置非常有用,无论它是如何添加到命名空间的。

破折号和下划线

在函数即任务的常见情况下,你经常会发现自己编写的任务名称包含下划线:

@task
def my_awesome_task(c):
    print("Awesome!")

类似于任务参数的处理方式,将它们的下划线转换为破折号(因为这是一个常见的命令行约定),所有任务或集合名称中的下划线都将被解释为破折号,默认情况下:

$ inv --list
Available tasks:

  my-awesome-task

$ inv my-awesome-task
Awesome!

如果你希望保留下划线,你可以更新你的配置来设置 tasks.auto_dash_namesFalse 在一个非运行时 config files (系统,用户,或项目)。例如,在 ~/.invoke.yml:

tasks:
    auto_dash_names: false

备注

为了避免混淆,这个设置在本质上是 “exclusive” -任务名称的下划线版本在 CLI 中无效 “除非 auto_dash_names 被禁用。(然而,在 Python 的纯函数级别上,它们必须继续用下划线引用,因为虚线名称不是有效的 Python 语法!)

嵌套集合

命名空间的意义是要有子命名空间;要在Invoke中执行此操作,需要创建额外的 Collection 实例,并通过 Collection.add_collection 将它们添加到它们的父集合中。例如,假设我们有几个文档任务

@task
def build_docs(c):
    c.run("sphinx-build docs docs/_build")

@task
def clean_docs(c):
    c.run("rm -rf docs/_build")

我们可以像这样将它们捆绑到一个新的命名的集合中

docs = Collection('docs')
docs.add_task(build_docs, 'build')
docs.add_task(clean_docs, 'clean')

然后用 add_collection 将这个新集合添加到根命名空间下

ns.add_collection(docs)

结果(假设 ns 目前只包含原始的 release 任务):

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

与任务一样,集合可以显式地用不同的名字绑定到它们的父节点上,而不是通过 name kwarg(也可以像第二个常规参数 add_task 一样):

ns.add_collection(docs, 'sphinx')

结果

$ invoke --list
Available tasks:

    release
    sphinx.build
    sphinx.clean

将模块作为集合导入

在简单的单模块情况下,Invoke 本身使用的简单策略是使用类方法 Collection.from_module,作为另一个 Collection 构造函数,它接受 Python 模块对象作为第一个参数。

给定方法的模块被扫描为 Task 实例,这些实例被添加到新的 Collection。默认情况下,该集合的名称取自模块名称(__name__ 属性),但它也可以显式提供。

备注

和默认的任务模块一样,你可以通过声明 nsnamespace Collection 来覆盖这个默认的加载行为。在被加载模块的顶层。

例如,让我们将之前的单文件示例重组为一个带有几个子模块的 Python 包。首先,tasks/release.py:

from invoke import task

@task
def release(c):
    c.run("python setup.py sdist register upload")

添加 tasks/docs.py:

from invoke import task

@task
def build(c):
    c.run("sphinx-build docs docs/_build")

@task
def clean(c):
    c.run("rm -rf docs/_build")

把它们联系在一起 tasks/__init__.py:

from invoke import Collection

import release, docs

ns = Collection()
ns.add_collection(Collection.from_module(release))
ns.add_collection(Collection.from_module(docs))

这种形式的 API 在实践中有点笨拙。幸运的是,有快捷方式 add_collection 会注意到当传入模块对象作为它的第一个参数时,并在内部为你调用 Collection.from_module

ns = Collection()
ns.add_collection(release)
ns.add_collection(docs)

无论哪种方式,结果都是

$ invoke --list
Available tasks:

    release.release
    docs.build
    docs.clean

默认任务

任务可以被声明为其所属集合的默认任务,例如通过将 default=True 传递给 @task (或传递给 Collection.add_task)。当你在一个命名空间中有一堆相关任务,但其中一个是最常用的,并且与整个命名空间很好地映射时,这非常有用。

例如,一直在实验的文档子模块中,build 任务作为默认任务是合理的,因此可以使用 invoke docs 作为 invoke docs.build 的快捷方式。这很容易实现:

@task(default=True)
def build(c):
    # ...

当导入到根命名空间时(如上所示),这会改变 --list 的输出,突出显示 docs.build 可以根据需要作为 docs 调用的事实:

$ invoke --list
Available tasks:

    release.release
    docs.build (docs)
    docs.clean

默认子集合

从 1.5 版本开始,此功能还扩展到子集合:当将子集合添加到其父集合时,可以将其指定为默认集合,并且该子集合的默认任务(或子子集合!)将作为父集合的默认任务被调用。

举个例子可能会更清楚。这里有一个包含两个子集合的小型内联任务树,每个子集合都有自己的默认任务:

from invoke import Collection, task

@task(default=True)
def build_all(c):
    print("build ALL THE THINGS!")

@task
def build_wheel(c):
    print("Just the wheel")

build = Collection(all=build_all, wheel=build_wheel)

@task(default=True)
def build_docs(c):
    print("Code without docs is no code at all")

docs = Collection(build_docs)

然后将这些子集合整合到一个顶层集合中,将 build 子集合设置为整体默认集合:

ns = Collection()
ns.add_collection(build, default=True)
ns.add_collection(docs)

结果是 build.all 成为绝对默认任务:

$ invoke
build ALL THE THINGS!

混合和匹配

你不仅限于上述具体策略——既然你已经了解了 add_taskadd_collection 的基本工具,请使用最适合你需求的方法。

例如,假设你希望将内容组织到子模块中,但为了方便起见,希望将 release.release ‘提升’回顶层。仅仅因为它存储在一个模块中并不意味着我们必须使用 add_collection——我们可以直接导入任务本身并使用 add_task:

from invoke import Collection

import docs
from release import release

ns = Collection()
ns.add_collection(docs)
ns.add_task(release)

结果

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

更多快捷方式

最后,如果你的需求足够简单,你甚至可以跳过 add_collectionadd_task——Collection 的构造函数会接受未知参数,并根据它们的值适当地构建命名空间:

from invoke import Collection

import docs, release

ns = Collection(release.release, docs)

请注意,既提供了一个任务对象(release.release),也提供了一个包含任务的模块(docs)。结果与上述相同:

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

如果以关键字参数的形式提供,这些关键字的作用类似于 add_* 方法中的 name 参数。当然,两者也可以混合使用:

ns = Collection(docs, deploy=release.release)

结果

$ invoke --list
Available tasks:

    deploy
    docs.build
    docs.clean

备注

如果需要,你仍然可以使用前导字符串参数为这些 Collection 对象命名,这在构建子集合时非常方便。