构建名称空间¶
加载单个任务模块的 base case 在一开始工作得很好,但高级用户通常需要更多的组织,比如将任务分割成嵌套命名空间树。
Collection
类提供了 API 来组织任务(和 their configuration)成一个树状结构。当被字符串引用时(例如在 CLI 或 pre/post 钩子中)嵌套命名空间中的任务使用点分隔语法,例如 docs.build
。
在本节中,我们将展示如何使用这个 API 灵活地构建名称空间,同时还允许使用最少的样板文件遵循 Python 包布局。
开始¶
未命名的 Collection
总是命名空间根;在隐式的基本情况下,Invoke 从 tasks 模块中的任务中为你创建一个。创建你自己的命名为 namespace
或 ns
的命名空间,以建立显式的命名空间(即跳过默认的 “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
别名¶
任务可能有额外的名称或别名,给出 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_names
到 False
在一个非运行时 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__
属性),但它也可以显式提供。
备注
和默认的任务模块一样,你可以通过声明 ns
或 namespace
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_task
和 add_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_collection
和 add_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
对象命名,这在构建子集合时非常方便。