如何包装一个开放的二进制流 – Python 2file
,Python 3io.BufferedReader
, an io.BytesIO
– 在一个io.TextIOWrapper
?
我正在尝试编写不改变即可工作的代码:
- 在 Python 2 上运行。
- 在 Python 3 上运行。
- 使用从标准库生成的二进制流(即我无法控制它们是什么类型)
- 将二进制流制作为测试双打(即没有文件句柄,无法重新打开)。
- 生产一个
io.TextIOWrapper
包装指定的流。
The io.TextIOWrapper
之所以需要,是因为标准库的其他部分需要它的 API。存在其他类似文件的类型,但不提供正确的 API。
Example
将二进制流包装为subprocess.Popen.stdout
属性:
import subprocess
import io
gnupg_subprocess = subprocess.Popen(
["gpg", "--version"], stdout=subprocess.PIPE)
gnupg_stdout = io.TextIOWrapper(gnupg_subprocess.stdout, encoding="utf-8")
在单元测试中,流被替换为io.BytesIO
实例来控制其内容而不触及任何子进程或文件系统。
gnupg_subprocess.stdout = io.BytesIO("Lorem ipsum".encode("utf-8"))
这在 Python 3 标准库创建的流上运行良好。但是,相同的代码在 Python 2 生成的流上失败:
[Python 2]
>>> type(gnupg_subprocess.stdout)
<type 'file'>
>>> gnupg_stdout = io.TextIOWrapper(gnupg_subprocess.stdout, encoding="utf-8")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'file' object has no attribute 'readable'
不是解决方案:特殊处理file
一个明显的反应是在代码中建立一个分支来测试流是否实际上是 Python 2file
对象,并以不同的方式处理该对象io.*
对象。
对于经过良好测试的代码来说,这不是一个选项,因为它创建了一个进行单元测试的分支——为了尽可能快地运行,不得创建任何real文件系统对象 – 无法执行。
单元测试将提供测试替身,而不是真实的file
对象。因此,创建一个不会被这些测试替身执行的分支会破坏测试套件。
不是解决方案:io.open
一些受访者建议重新开放(例如io.open
) 底层文件句柄:
gnupg_stdout = io.open(
gnupg_subprocess.stdout.fileno(), mode='r', encoding="utf-8")
这适用于 Python 3 和 Python 2:
[Python 3]
>>> type(gnupg_subprocess.stdout)
<class '_io.BufferedReader'>
>>> gnupg_stdout = io.open(gnupg_subprocess.stdout.fileno(), mode='r', encoding="utf-8")
>>> type(gnupg_stdout)
<class '_io.TextIOWrapper'>
[Python 2]
>>> type(gnupg_subprocess.stdout)
<type 'file'>
>>> gnupg_stdout = io.open(gnupg_subprocess.stdout.fileno(), mode='r', encoding="utf-8")
>>> type(gnupg_stdout)
<type '_io.TextIOWrapper'>
但当然它依赖于重新打开真实文件从它的文件句柄。因此,当测试替身是一个时,它会在单元测试中失败io.BytesIO
实例:
>>> gnupg_subprocess.stdout = io.BytesIO("Lorem ipsum".encode("utf-8"))
>>> type(gnupg_subprocess.stdout)
<type '_io.BytesIO'>
>>> gnupg_stdout = io.open(gnupg_subprocess.stdout.fileno(), mode='r', encoding="utf-8")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
io.UnsupportedOperation: fileno
不是解决方案:codecs.getreader
标准库还具有codecs
模块,它提供包装器功能:
import codecs
gnupg_stdout = codecs.getreader("utf-8")(gnupg_subprocess.stdout)
这很好,因为它不会尝试重新打开流。但它未能提供io.TextIOWrapper
API。具体来说,它不继承io.IOBase
and 没有encoding
属性:
>>> type(gnupg_subprocess.stdout)
<type 'file'>
>>> gnupg_stdout = codecs.getreader("utf-8")(gnupg_subprocess.stdout)
>>> type(gnupg_stdout)
<type 'instance'>
>>> isinstance(gnupg_stdout, io.IOBase)
False
>>> gnupg_stdout.encoding
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.7/codecs.py", line 643, in __getattr__
return getattr(self.stream, name)
AttributeError: '_io.BytesIO' object has no attribute 'encoding'
So codecs
不提供替代对象io.TextIOWrapper
.
该怎么办?
那么我怎样才能编写适用于 Python 2 和 Python 3 的代码,同时包含测试替身和真实对象,这包裹一个io.TextIOWrapper
围绕已经打开的字节流?