构建名称空间¶
加载单个任务模块的 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
备注
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_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
默认任务¶
Tasks may be declared as the default task to invoke for the collection they
belong to, e.g. by giving default=True
to @task
(or to
Collection.add_task
.) This is useful when you have a bunch of related tasks
in a namespace but one of them is the most commonly used, and maps well to the
namespace as a whole.
For example, in the documentation submodule we’ve been experimenting with so
far, the build
task makes sense as a default, so we can say things like
invoke docs
as a shortcut to invoke docs.build
. This is easy to do:
@task(default=True)
def build(c):
# ...
When imported into the root namespace (as shown above) this alters the output
of --list
, highlighting the fact that docs.build
can be invoked as
docs
if desired:
$ invoke --list
Available tasks:
release.release
docs.build (docs)
docs.clean
Default subcollections¶
As of version 1.5, this functionality is also extended to subcollections: a subcollection can be specified as the default when being added to its parent collection, and that subcollection’s own default task (or sub-subcollection!) will be invoked as the default for the parent.
An example probably makes that clearer. Here’s a tiny inline task tree with two subcollections, each with their own default task:
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)
Then we tie those into one top level collection, setting the build
subcollection as the overall default:
ns = Collection()
ns.add_collection(build, default=True)
ns.add_collection(docs)
The result is that build.all
becomes the absolute default task:
$ invoke
build ALL THE THINGS!
Mix and match¶
You’re not limited to the specific tactics shown above – now that you know
the basic tools of add_task
and add_collection
, use whatever approach
best fits your needs.
For example, let’s say you wanted to keep things organized into submodules, but
wanted to “promote” release.release
back to the top level for convenience’s
sake. Just because it’s stored in a module doesn’t mean we must use
add_collection
– we could instead import the task itself and use
add_task
directly:
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
More shortcuts¶
Finally, you can even skip add_collection
and add_task
if your needs
are simple enough – Collection
’s constructor will take
unknown arguments and build the namespace from their values as
appropriate:
from invoke import Collection
import docs, release
ns = Collection(release.release, docs)
Notice how we gave both a task object (release.release
) and a module
containing tasks (docs
). The result is identical to the above:
$ invoke --list
Available tasks:
release
docs.build
docs.clean
If given as keyword arguments, the keywords act like the name
arguments do
in the add_*
methods. Naturally, both can be mixed together as well:
ns = Collection(docs, deploy=release.release)
结果
$ invoke --list
Available tasks:
deploy
docs.build
docs.clean
备注
You can still name these Collection
objects with a leading string
argument if desired, which can be handy when building sub-collections.