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 Sandwich 通用边界:控制节点代码中字节到文本的转换位置
这是一个部分列表,列出了在使用 Unicode Sandwich 字符串策略时必须进行字节与文本之间转换的位置。它并非详尽无遗,但可以帮助你了解需要注意问题的哪些地方。
读写文件
在 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
。
__future__
导入执行以下操作:
用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
。
控制节点代码的字符串格式化
为 Python 2.6 兼容性使用str.format()
从 Python 2.6 开始,字符串获得了一种名为format()
的方法来组合字符串。但是,直到 Python 2.7 才添加了format()
的一个常用功能,因此你需要记住不要在 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.5 中重新加入。这对我们来说不是问题,因为 Python 3.5 是我们的最低版本要求。但是,如果您碰巧使用 Python 3.4 或更早版本测试 Ansible 代码,您会发现这里的字节字符串格式化不起作用。请升级到 Python 3.5 进行测试。
另请参阅
Python 关于百分比格式化 的文档