Ansible 和 Python 3
ansible-core
代码运行 Python 3(有关特定版本,请查看 控制节点要求)ansible-core
和 Ansible 集合的贡献者应了解本文档中的提示,以便他们可以编写在与 Ansible 其他部分相同的 Python 版本上运行的代码。
我们确实有一些考虑因素,具体取决于 Ansible 代码的类型
控制节点上的代码 - 在您调用 /usr/bin/ansible 的机器上运行的代码,只需要支持控制节点的 Python 版本。
模块 - Ansible 传输到托管机器并调用到托管机器上的代码。模块需要支持“托管节点”的 Python 版本,但也有一些例外。
共享
module_utils
代码 - 模块用来执行任务的通用代码,有时也用在控制节点上的代码中。共享module_utils
代码需要支持与模块相同的 Python 范围。
但是,这三种类型的代码不使用相同的字符串策略。如果您正在开发模块或一些 module_utils
代码,请务必仔细阅读有关字符串策略的部分。
Python 3.x 和 Python 2.x 的最低版本
请参阅 控制节点要求 和 托管节点要求 以了解支持的特定版本。
您的自定义模块可以支持您想要的任何版本的 Python(或其他语言),但以上是为 Ansible 项目贡献的代码的要求。
开发支持 Python 2 和 Python 3 的 Ansible 代码
学习编写同时支持 Python 2 和 Python 3 的代码的最佳起点是 Lennart Regebro 的书籍:移植到 Python 3。这本书描述了移植到 Python 3 的几种策略。我们正在使用的一种策略是 从单个代码库支持 Python 2 和 Python 3
了解 Python 2 和 Python 3 中的字符串
Python 2 和 Python 3 处理字符串的方式不同,因此在编写支持 Python 3 的代码时,您必须决定使用哪种字符串模型。字符串可以是字节数组(如 C 中),也可以是文本数组。文本是我们认为的字母、数字、符号、其他可打印符号以及少量不可打印的“符号”(控制代码)。
在 Python 2 中,这两种类型的(str
用于字节,unicode
用于文本)通常可以互换使用。如果只处理 ASCII 字符,则可以自动组合、比较和将字符串从一种类型转换为另一种类型。如果引入了非 ASCII 字符,Python 2 由于不知道非 ASCII 字符应该使用哪种编码,因此会开始抛出异常。
Python 3 通过使字节 (bytes
) 和文本 (str
) 之间的区分更加严格,从而改变了这种行为。当尝试组合和比较这两种类型时,Python 3 会抛出异常。程序员必须显式地从一种类型转换为另一种类型以混合来自每种类型的值。
在 Python 3 中,程序员可以立即清楚地看到代码何时不适当地混合了字节和文本类型,而在 Python 2 中,混合这些类型的代码可能会工作,直到用户通过输入非 ASCII 输入导致异常。Python 3 强制程序员主动为程序中处理字符串定义一种策略,这样他们就不会无意中混合文本和字节字符串。
Ansible 在控制节点上的代码、:ref: 模块 <module_string_strategy> 和 module_utils 代码中使用不同的策略来处理字符串。
控制节点字符串策略:Unicode 三明治
直到最近,ansible-core
还支持 Python 2.x 并遵循这种策略,称为 Unicode 三明治(以 Python 2 的 unicode
文本类型命名)。对于 Unicode 三明治,我们知道在我们的代码和外部世界(例如,文件和网络 I/O、环境变量以及一些库调用)的边界,我们将收到字节。我们需要将这些字节转换为文本,并在代码的内部部分使用它。当我们必须将这些字符串发送回外部世界时,我们首先将文本转换回字节。为了可视化这一点,想象一个“三明治”,它由上面和下面两层字节组成,中间有一层转换层,以及中心的所有文本类型。
为了兼容性,您会看到我们开发的许多自定义函数(to_text
/to_bytes
/to_native
),虽然 Python 2 现在不再是问题,但我们会继续使用它们,因为它们适用于其他使处理 unicode 存在问题的用例。
虽然我们现在不会再使用它了,但下面的文档对于那些正在开发仍然需要同时支持 Python 2 和 3 的模块的人来说仍然有用。
Unicode 三明治的常见边界:在控制节点代码中将字节转换为文本的位置
这是一个部分列表,它列出了在使用 Unicode 三明治字符串策略时我们必须进行字节之间转换的位置。它不是详尽无遗的,但它能让您了解需要注意哪些地方可能会出现问题。
读写文件
在 Python 2 中,从文件读取会产生字节。在 Python 3 中,它可以产生文本。为了使代码可移植到这两个版本,我们不使用 Python 3 的产生文本的能力,而是显式地进行转换。例如
from ansible.module_utils.common.text.converters import to_text
with open('filename-with-utf8-data.txt', 'rb') as my_file:
b_data = my_file.read()
try:
data = to_text(b_data, errors='surrogate_or_strict')
except UnicodeError:
# Handle the exception gracefully -- usually by displaying a good
# user-centric error message that can be traced back to this piece
# of code.
pass
注意
Ansible 的大多数部分假设所有编码文本都是 UTF-8。如果将来有对其他编码的需求,我们可能会更改它,但就目前而言,可以安全地假设字节是 UTF-8。
写入文件是相反的过程
from ansible.module_utils.common.text.converters import to_bytes
with open('filename.txt', 'wb') as my_file:
my_file.write(to_bytes(some_text_string))
请注意,我们不必在这里捕获 UnicodeError
,因为我们正在转换为 UTF-8,并且 Python 中的所有文本字符串都可以转换回 UTF-8。
文件系统交互
处理文件名通常涉及回退到字节,因为在类 UNIX 系统上,文件名是字节。在 Python 2 中,如果我们将文本字符串传递给这些函数,文本字符串将在函数内部转换为字节字符串,如果存在非 ASCII 字符,则会发生回溯。在 Python 3 中,只有在文本字符串无法在当前语言环境中解码时才会发生回溯,但明确地编写在两个版本上都能工作的代码仍然是件好事
import os.path
from ansible.module_utils.common.text.converters import to_bytes
filename = u'/var/tmp/くらとみ.txt'
f = open(to_bytes(filename), 'wb')
mtime = os.path.getmtime(to_bytes(filename))
b_filename = os.path.expandvars(to_bytes(filename))
if os.path.exists(to_bytes(filename)):
pass
当您仅将文件名作为字符串进行操作,而无需与文件系统(或与文件系统进行通信的 C 库)进行交互时,您通常可以不必转换为字节。
import os.path
os.path.join(u'/var/tmp/café', u'くらとみ')
os.path.split(u'/var/tmp/café/くらとみ')
另一方面,如果代码需要操作文件名并与文件系统进行交互,则立即转换为字节并在字节中进行操作会更方便。
警告
确保传递给函数的所有变量类型相同。如果您正在使用类似于os.path.join()
的函数,该函数接收多个字符串并将其组合使用,您需要确保所有类型都相同(全部为字节或全部为文本)。混合字节和文本会导致跟踪错误。
与其他程序交互
与其他程序的交互通过操作系统和 C 库进行,并对 UNIX 内核定义的项目进行操作。这些接口都是面向字节的,因此 Python 接口也是面向字节的。在 Python 2 和 Python 3 中,都应该将字节字符串传递给 Python 的子进程库,并应从子进程库中获得字节字符串。
Ansible 控制节点代码中与其他程序交互的主要地方之一是连接插件的 exec_command
方法。这些方法将它们在命令(以及命令的参数)中接收到的任何文本字符串转换为字节,并将 stdout 和 stderr 返回为字节字符串。更高级别的函数(如操作插件的 _low_level_execute_command
)将输出转换为文本字符串。
模块字符串策略:原生字符串
在模块中,我们使用一种称为原生字符串的策略。这使得维护 Ansible 众多模块的社区成员的工作变得更加容易,因为它们不会通过强制所有模块内部的字符串都是文本并在边界之间进行文本和字节的转换来破坏向后兼容性。
原生字符串指的是您指定裸字符串文字时 Python 使用的类型。
"This is a native string"
在 Python 2 中,这些是字节字符串。在 Python 3 中,这些是文本字符串。模块应该被编码为在 Python 2 上期望字节,而在 Python 3 上期望文本。
Module_utils 字符串策略:混合
在 module_utils
代码中,我们使用混合字符串策略。虽然 Ansible 的 module_utils
代码在很大程度上类似于模块代码,但它的一些部分也被控制节点使用。因此,它需要与模块和控制节点的假设兼容,特别是字符串策略。module_utils 代码尝试将原生字符串作为输入接受到它的函数中,并将其输出作为原生字符串发出。
在 module_utils
代码中
函数**必须**接受字符串参数作为文本字符串或字节字符串。
函数可以返回与给定类型相同的字符串类型,也可以返回它们在运行的 Python 版本的原生字符串类型。
返回字符串的函数**必须**记录它们是否返回与给定类型相同的字符串类型或原生字符串。
因此,module-utils 函数在本质上通常非常防御性。它们将它们的字符串参数转换为文本(使用 ansible.module_utils.common.text.converters.to_text
)在函数的开头,完成它们的工作,然后将返回值转换为原生字符串类型(使用 ansible.module_utils.common.text.converters.to_native
)或返回到它们的函数参数接收到的字符串类型。
Python 2/Python 3 兼容性的提示、技巧和习惯用法
使用向前兼容模板
在所有 python 文件的顶部使用以下模板代码,以确保某些结构在 Python 2 和 Python 3 上的行为相同。
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
__metaclass__ = type
使文件定义的所有类都成为新式类,而无需显式继承自 object
.
The __future__
imports do the following
- absolute_import:
Makes imports look in
sys.path
for the modules being imported, skipping the directory in which the module doing the importing lives. If the code wants to use the directory in which the module doing the importing, there’s a new dot notation to do so.- division:
Makes division of integers always return a float. If you need to find the quotient use
x // y
instead ofx / y
.- print_function:
Changes
print
from a keyword into a function.
在字节字符串前面加上 b_
由于混合文本和字节类型会导致跟踪错误,因此我们希望明确哪些变量保存文本,哪些变量保存字节。我们通过在保存字节的任何变量前面加上 b_
来做到这一点。例如
filename = u'/var/tmp/café.txt'
b_filename = to_bytes(filename)
with open(b_filename) as f:
data = f.read()
我们没有在文本字符串前面加上前缀,因为我们只在边界上对字节字符串进行操作,因此需要字节的变量比文本的变量要少。
导入 Ansible 的捆绑 Python six
库
第三方 Python six 库的存在是为了帮助项目创建可以在 Python 2 和 Python 3 上运行的代码。Ansible 在 module_utils 中包含了该库的版本,以便其他模块可以使用它,而无需要求它安装在远程系统上。要使用它,请像这样导入它
from ansible.module_utils import six
注意
Ansible 也可以使用系统副本的 six
如果系统副本的版本比 Ansible 捆绑的版本晚,Ansible 将使用系统副本的 six。
用 as
处理异常
为了使代码在 Python 2.6+ 和 Python 3 上运行,请使用新的异常捕获语法,该语法使用 as
关键字。
try:
a = 2/0
except ValueError as e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
不要使用以下语法,因为它将在所有版本的 Python 3 上失败。
try:
a = 2/0
except ValueError, e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
更新八进制数
在 Python 2.x 中,八进制字面量可以指定为 0755
。在 Python 3 中,八进制必须指定为 0o755
。
控制节点代码的字符串格式
使用 str.format()
确保 Python 2.6 兼容性
从 Python 2.6 开始,字符串获得了一种名为 format()
的方法来将字符串组合在一起。但是,format()
的一个常用功能直到 Python 2.7 才添加,因此您需要记住不要在 Ansible 代码中使用它。
# Does not work in Python 2.6!
new_string = "Dear {}, Welcome to {}".format(username, location)
# Use this instead
new_string = "Dear {0}, Welcome to {1}".format(username, location)
上面两个格式字符串都将 format()
方法的位置参数映射到字符串中。但是,第一个版本在 Python 2.6 中不起作用。始终记住将数字放入占位符中,以便代码与 Python 2.6 兼容。
使用百分比格式与字节字符串
在 Python 3.x 中,字节字符串没有 format()
方法。但是,它确实支持旧的百分比格式。
b_command_line = b'ansible-playbook --become-user %s -K %s' % (user, playbook_file)
注意
Python 3.5 中添加的百分比格式
字节字符串的百分比格式在 Python 3 的 3.5 版本中重新添加。这对我们来说不是问题,因为 Python 3.5 是我们的最低版本。但是,如果您碰巧正在使用 Python 3.4 或更早版本测试 Ansible 代码,您会发现这里的字节字符串格式不起作用。升级到 Python 3.5 进行测试。
参见
Python 关于百分比格式的文档