Skip to main content

定义任务

从Fabric 1.1开始,有两种不同的方法可以用来定义fabfile中哪些对象显示为任务:

  • 从1.1开始的“新”方法考虑 Task 或其子类的实例,并且也下降到导入的模块以允许构建嵌套的命名空间。

  • 1.0和更早版本的“经典”方法考虑所有公共可调用对象(函数,类等),并且只考虑fabfile中的对象,而不会递归到导入的模块中。

注解

这两个方法是 互斥:如果Fabric在您的fabfile或其导入的模块中找到 any 新式任务对象,它将假设您已经提交了此任务声明方法,并且不会考虑任何非 Task 可调用项。如果发现 no 新式任务,它将恢复到经典行为。

本文的其余部分详细探讨了这两种方法。

注解

要查看您的fabfile中可以通过 fab 执行什么任务,请使用 fab --list

新式任务

Fabric 1.1引入了 Task 类,以促进新功能并实现一些编程最佳实践,特别是:

  • 面向对象的任务。继承和它附带的所有它可以使传递更简单的代码重用比传递简单的函数对象。任务声明的经典风格没有完全排除这一点,但它也没有使它非常容易。

  • 命名空间。有一个明确的方法来声明任务使得更容易设置递归命名空间,使用Python的 os 模块(在经典方法下显示为有效的“任务”)的内容来污染任务列表。

随着 Task 的引入,有两种方式来设置新任务:

  • @task 装饰常规模块级函数,该函数将函数透明地包装在 Task 子类中。函数名称将在调用时用作任务名称。

  • 子类 TaskTask 本身是抽象的),定义一个 run 方法,并在模块级实例化你的子类。实例的 name 属性用作任务名称;如果省略,将使用实例的变量名称。

使用新式任务还允许您设置 命名空间

@task 装饰器

使用新式任务功能的最快方法是使用 @task 包装基本任务功能:

from fabric.api import task, run

@task
def mytask():
    run("a command")

当使用这个装饰器时,它通知Fabric,包装在装饰器中的 only 函数将作为有效任务加载。 (当不存在时,经典风格的任务 行为开始。)

参数

@task 也可以用参数调用来定制其行为。下面没有记录的任何参数被传递到正在使用的 task_class 的构造函数中,函数本身作为第一个参数(有关详细信息,请参阅 使用自定义子类与 @task)。

  • task_class:用于包装装饰函数的 Task 子类。默认为 WrappedCallableTask

  • aliases:一个可迭代的字符串名称,将被用作包装函数的别名。有关详细信息,请参阅 别名

  • alias:像 aliases,但是采用单个字符串参数,而不是可迭代。如果指定 aliasaliases,则 aliases 将优先。

  • default:一个布尔值,用于确定装饰任务是否也包含其包含的模块作为任务名称。参见 默认任务

  • name:设置此任务对于命令行界面显示的名称的字符串。有用的任务名称,否则会影响Python内置(这是技术上合法,但皱眉和bug容易)。

别名

这里有一个使用 alias 关键字参数的快速示例,以便于使用更长的人工可读任务名称和更短的名称,更快地键入:

from fabric.api import task

@task(alias='dwm')
def deploy_with_migrations():
    pass

在此fabfile上调用 --list 将显示原始 deploy_with_migrations 及其别名 dwm:

$ fab --list
Available commands:

    deploy_with_migrations
    dwm

当需要同一个函数的多个别名时,只需在 aliases kwarg中进行交换,这需要一个可迭代的字符串而不是单个字符串。

默认任务

以类似于 别名 的方式,将模块中的给定任务指定为“默认”任务有时是有用的,其可以通过引用 just 模块名称来调用。这可以节省打字和/或允许更简单的组织,当有一个“主”任务和一些相关的任务或子程序。

例如,deploy 子模块可能包含用于配置新服务器,推送代码,迁移数据库等任务,但将任务突出显示为默认的“仅部署”操作将非常方便。这样的 deploy.py 模块可能看起来像这样:

from fabric.api import task

@task
def migrate():
    pass

@task
def push():
    pass

@task
def provision():
    pass

@task
def full_deploy():
    if not provisioned:
        provision()
    push()
    migrate()

使用以下任务列表(假设只导入 deploy 的简单顶级 fabfile.py):

$ fab --list
Available commands:

    deploy.full_deploy
    deploy.migrate
    deploy.provision
    deploy.push

在每个部署中调用 deploy.full_deploy 可能会变得老旧,或者团队中的新人可能不确定这是否真的是正确的任务运行。

使用 default kwarg到 @task,我们可以标记例如 full_deploy 作为默认任务:

@task(default=True)
def full_deploy():
    pass

这样做会像这样更新任务列表:

$ fab --list
Available commands:

    deploy
    deploy.full_deploy
    deploy.migrate
    deploy.provision
    deploy.push

请注意,full_deploy 仍然作为其自己的显式任务存在 - 但现在 deploy 显示为 full_deploy 的一种顶级别名。

如果模块中的多个任务设置了 default=True,则将加载最后一个(通常是文件中最低的一个)。

顶级默认任务

当用户调用没有任何任务名称的 fab 时(类似于例子 make),在顶层fabfile中使用 @task(default=True) 将导致表示的任务执行。当使用此快捷方式时,不可能为任务本身指定参数 - 如果必要,定期调用任务。

Task 子类

如果你习惯于 经典风格的任务,一个简单的方法来考虑 Task 子类是他们的 run 方法直接相当于一个经典的任务;它的参数是任务参数(除了 self),它的主体是什么被执行。

例如,这个新式任务:

class MyTask(Task):
    name = "deploy"
    def run(self, environment, domain="whatever.com"):
        run("git clone foo")
        sudo("service apache2 restart")

instance = MyTask()

正好等于这个基于功能的任务:

@task
def deploy(environment, domain="whatever.com"):
    run("git clone foo")
    sudo("service apache2 restart")

注意我们如何实例化我们的类的实例;这只是普通的Python面向对象编程工作。虽然它现在是一个小的样板 - 例如,Fabric不关心你给出的实例化的名称,只有实例的 name 属性 - 它是非常值得的好处的类的功能。

我们计划在未来扩展API,使这种体验更流畅。

使用自定义子类与 @task

可以将定制的 Task 子类与 @task 结婚。这在核心执行逻辑不做任何类/对象特定的,但是你想利用类元编程或类似技术的情况下可能是有用的。

具体来说,被设计为接受可调用作为其第一构造函数参数(如内置 WrappedCallableTask 那样)的任何 Task 子类可以被指定为 @tasktask_class 参数。

Fabric将自动实例化给定类的副本,将包装函数作为第一个参数传递。添加给装饰器的所有其他args/kwargs(除了在 参数 中记录的“特殊”参数)。

这里有一个简单的,有点麻烦的例子,使这个显而易见的:

from fabric.api import task
from fabric.tasks import Task

class CustomTask(Task):
    def __init__(self, func, myarg, *args, **kwargs):
        super(CustomTask, self).__init__(*args, **kwargs)
        self.func = func
        self.myarg = myarg

    def run(self, *args, **kwargs):
        return self.func(*args, **kwargs)

@task(task_class=CustomTask, myarg='value', alias='at')
def actual_task():
    pass

当加载此fabfile时,CustomTask 的副本被实例化,有效地调用:

task_obj = CustomTask(actual_task, myarg='value')

注意 alias kwarg如何被装饰器本身剥离,并且永远不会达到类实例化;这在功能上与 命令行任务参数 的工作方式相同。

命名空间

使用 经典任务,fabfiles被限制为一个单一的,平面的任务名称,没有真正的方式来组织它们。在面料1.1和更新的,如果声明任务的新方法(通过 @task 或您自己的 Task 子类的实例),你可以利用 命名空间 的:

  • 导入到您的fabfile中的任何模块对象将被递归到,寻找其他任务对象。

  • 在子模块中,您可以使用标准的Python __all__ 模块级变量名称(认为它们应该仍然是有效的新式任务对象)来控制哪些对象是“导出的”。

  • 这些任务将根据它们来自的模块给出新的虚线符号名称,类似于Python自己的导入语法。

让我们建立一个从简单到复杂的fabfile包,看看它是如何工作的。

基本

我们从包含几个任务的单个 __init__.py 开始(为了简洁,省略了Fabric API导入):

@task
def deploy():
    ...

@task
def compress():
    ...

fab --list 的输出看起来像这样:

deploy
compress

这里只有一个命名空间:“root”或全局命名空间。看起来很简单,但在一个真实世界的fabfile有几十个任务,它可以很难管理。

导入子模块

如上所述,Fabric将检查任何导入的模块对象的任务,无论该模块在Python导入路径中存在于何处。现在我们只想包含我们自己的“附近”任务,所以我们将在我们的包中创建一个新的子模块来处理,例如负载平衡器 - lb.py:

@task
def add_backend():
    ...

我们将它添加到 __init__.py 的顶部:

import lb

现在 fab --list 向我们展示:

deploy
compress
lb.add_backend

再次,在自己的子模块中只有一个任务,它看起来很傻,但好处应该很明显。

更深

命名空间不仅限于一个级别。假设我们有一个更大的设置,并且想要一个用于数据库相关任务的命名空间,在其中有额外的差异。我们制作一个名为 db/ 的子包,里面有一个 migrations.py 模块:

@task
def list():
    ...

@task
def run():
    ...

我们需要确保此模块对任何导入 db 的人都可见,因此我们将其添加到子包的 __init__.py:

import migrations

作为最后一步,我们将子包导入我们的根级 __init__.py,所以现在它的前几行看起来像这样:

import lb
import db

毕竟,我们的文件树看起来像这样:

.
├── __init__.py
├── db
│   ├── __init__.py
│   └── migrations.py
└── lb.py

fab --list 显示:

deploy
compress
lb.add_backend
db.migrations.list
db.migrations.run

我们也可以直接指定(或导入)任务到 db/__init__.py,他们将显示为 db.<whatever>,如你所期望的。

限制与 __all__

您可以通过使用模块级 __all__ 变量(变量名列表)的Python约定来限制Fabric检查导入的模块时使用的Fabric。如果我们不希望 db.migrations.run 任务由于某种原因而显示,我们可以将其添加到 db/migrations.py 的顶部:

__all__ = ['list']

注意缺少 'run' 的地方。如果需要,您可以将 run 直接导入层次结构的其他部分,否则它将保持隐藏。

切换它

我们一直保持我们的fabfile包整齐组织和导入它以一种直接的方式,但文件系统布局在这里没有实际意义。所有Fabric的loader关心的是导入模块时给出的名称。

例如,如果我们改变根 __init__.py 的顶部看起来像这样:

import db as database

我们的任务清单将会改变:

deploy
compress
lb.add_backend
database.migrations.list
database.migrations.run

这适用于任何其他导入 - 您可以将第三方模块导入到您自己的任务层次结构中,或抓取深层嵌套的模块,使其显示在顶层附近。

嵌套列表输出

最后一点,我们在本节中使用了默认的Fabric --list 输出 - 它使得实际任务名称更加明显。但是,您可以通过将 nested 传递到 --list-format 选项来获取更嵌套或树状的视图:

$ fab --list-format=nested --list
Available commands (remember to call as module.[...].task):

    deploy
    compress
    lb:
        add_backend
    database:
        migrations:
            list
            run

虽然它稍微混淆了“真正的”任务名称,但是该视图提供了一种方便的方式来注意大型命名空间中的任务组织。

经典任务

当没有找到新的基于 Task 的任务时,Fabric将考虑您的fabfile中找到的任何可调用对象, 如下:

  • 名称以下划线(_)开头的Callables。换句话说,Python的通常的“私有”约定在这里成立。

  • 在Fabric本身中定义的Callables。 Fabric自己的功能,如 runsudo 不会显示在您的任务列表中。

进口

Python的 import 语句有效地包括在模块的命名空间中导入的对象。由于Fabric的fabfiles只是Python模块,这意味着import也被认为是可能的经典风格的任务,以及fabfile中定义的任何东西。

注解

这仅适用于导入的 可调用对象 - 而不是模块。导入的模块只有在包含 新式任务 时才会起作用,此时此部分不再适用。

因此,我们强烈建议您使用 import module 导入形式,后跟 module.callable(),这将导致比 from module import callable 更干净的fabfile API。

例如,这里有一个样例fabfile,它使用 urllib.urlopen 从web服务获取一些数据:

from urllib import urlopen

from fabric.api import run

def webservice_read():
    objects = urlopen('http://my/web/service/?foo=bar').read().split()
    print(objects)

这看起来很简单,运行没有错误。然而,看看如果我们在这个fabfile上运行 fab --list 会发生什么:

$ fab --list
Available commands:

  webservice_read   List some directories.
  urlopen           urlopen(url [, data]) -> open file-like object

我们只有一个任务的fabfile显示两个“任务”,这是不够的,一个毫不知情的用户可能会意外尝试调用 fab urlopen,这可能不会很好地工作。想象任何真实世界的fabfile,这可能会更复杂,希望你可以看到这可能会变得混乱快。

作为参考,这里是推荐的方法:

import urllib

from fabric.api import run

def webservice_read():
    objects = urllib.urlopen('http://my/web/service/?foo=bar').read().split()
    print(objects)

这是一个简单的改变,但它会使任何人使用你的fabfile有点幸福。