使用io.TextIOWrapper

时间:2015-12-24 05:17:53

标签: python unit-testing python-3.x character-encoding python-2.x

如何在file中包装开放二进制流 - Python 2 io.BufferedReader,Python 3 io.BytesIOio.TextIOWrapper

我正在尝试编写不会改变的代码:

  • 在Python 2上运行。
  • 在Python 3上运行。
  • 使用标准库生成的二进制流(即我无法控制它们的类型)
  • 使二进制流成为测试双精度(即没有文件句柄,无法重新打开)。
  • 生成包装指定流的io.TextIOWrapper

需要io.TextIOWrapper,因为标准库的其他部分需要其API。存在其他类似文件的类型,但不提供正确的API。

实施例

包装呈现为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 2 file对象,并且处理与io.*个对象不同。

对于经过良好测试的代码,这不是一个选项,因为它创建了一个单元测试的分支 - 为了尽可能快地运行,必须不能创建任何真正的文件系统对象 - 可以'运动。

单元测试将提供测试双精度,而不是真正的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'>

但当然依赖于从文件句柄重新打开真实文件。因此,当测试double为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 没有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'

因此codecs不提供替代io.TextIOWrapper的对象。

怎么办?

那么如何编写适用于Python 2和Python 3的代码,包括测试双精度和真实对象,围绕已经打开的字节流包装io.TextIOWrapper

6 个答案:

答案 0 :(得分:13)

使用codecs.getreader生成包装器对象:

text_stream = codecs.getreader("utf-8")(bytes_stream)

适用于Python 2和Python 3。

答案 1 :(得分:4)

基于各种论坛中的多个建议,并尝试使用标准库来满足标准,我目前的结论是这是不可能的与我们目前拥有的库和类型。

答案 2 :(得分:4)

事实证明,您只需要将io.BytesIO包装在Python 2和Python 3上都存在的io.BufferedReader中。

import io

reader = io.BufferedReader(io.BytesIO("Lorem ipsum".encode("utf-8")))
wrapper = io.TextIOWrapper(reader)
wrapper.read()  # returns Lorem ipsum

这个答案最初建议使用os.pipe,但管道的读取端必须包装在Python 2上的io.BufferedReader中才能工作,所以这个解决方案更简单,避免分配管道。

答案 3 :(得分:2)

好的,这似乎是一个完整的解决方案,对于问题中提到的所有情况,使用Python 2.7和Python 3.5进行测试。一般的解决方案最终是重新打开文件描述符,但是你需要使用一个管道来测试你的文件描述符,而不是io.BytesIO。

import io
import subprocess
import os

# Example function, re-opens a file descriptor for UTF-8 decoding,
# reads until EOF and prints what is read.
def read_as_utf8(fileno):
    fp = io.open(fileno, mode="r", encoding="utf-8", closefd=False)
    print(fp.read())
    fp.close()

# Subprocess
gpg = subprocess.Popen(["gpg", "--version"], stdout=subprocess.PIPE)
read_as_utf8(gpg.stdout.fileno())

# Normal file (contains "Lorem ipsum." as UTF-8 bytes)
normal_file = open("loremipsum.txt", "rb")
read_as_utf8(normal_file.fileno())  # prints "Lorem ipsum."

# Pipe (for test harness - write whatever you want into the pipe)
pipe_r, pipe_w = os.pipe()
os.write(pipe_w, "Lorem ipsum.".encode("utf-8"))
os.close(pipe_w)
read_as_utf8(pipe_r)  # prints "Lorem ipsum."
os.close(pipe_r)

答案 4 :(得分:2)

我也需要这个,但是基于这里的线程,我确定只使用Python 2的io模块是不可能的。虽然这打破了file&#34;的特殊待遇。规则,我使用的技术是为file(下面的代码)创建一个非常薄的包装器,然后可以将其包装在io.BufferedReader中,然后可以将其传递给io.TextIOWrapper构造函数。单元测试会很麻烦,因为很明显新的代码路径无法在Python 3上进行测试。

顺便说一句,open()的结果可以直接传递给Python 3中的io.TextIOWrapper的原因是因为二进制模式open()实际上返回了一个io.BufferedReader实例开头(至少在Python 3.4上,这是我当时正在测试的地方)。

import io
import six  # for six.PY2

if six.PY2:
    class _ReadableWrapper(object):
        def __init__(self, raw):
            self._raw = raw

        def readable(self):
            return True

        def writable(self):
            return False

        def seekable(self):
            return True

        def __getattr__(self, name):
            return getattr(self._raw, name)

def wrap_text(stream, *args, **kwargs):
    # Note: order important here, as 'file' doesn't exist in Python 3
    if six.PY2 and isinstance(stream, file):
        stream = io.BufferedReader(_ReadableWrapper(stream))

    return io.TextIOWrapper(stream)

至少这个很小,所以希望它可以最大限度地减少无法轻松进行单元测试的部件的曝光。

答案 5 :(得分:0)

这是我在python 2.7和python 3.6中测试过的一些代码。

这里的关键是你需要先在前一个流上使用detach()。这不会关闭底层文件,它只是删除原始流对象,以便可以重用它。 detach()将返回一个可与TextIOWrapper一起包装的对象。

作为一个例子,我在二进制读取模式下打开一个文件,对它进行读取,然后通过io.TextIOWrapper切换到UTF-8解码文本流。

我将此示例保存为this-file.py

import io

fileName = 'this-file.py'
fp = io.open(fileName,'rb')
fp.seek(20)
someBytes = fp.read(10)
print(type(someBytes) + len(someBytes))

# now let's do some wrapping to get a new text (non-binary) stream
pos = fp.tell() # we're about to lose our position, so let's save it
newStream = io.TextIOWrapper(fp.detach(),'utf-8') # FYI -- fp is now unusable
newStream.seek(pos)
theRest = newStream.read()
print(type(theRest), len(theRest))

这是我用python2和python3运行时得到的结果。

$ python2.7 this-file.py 
(<type 'str'>, 10)
(<type 'unicode'>, 406)
$ python3.6 this-file.py 
<class 'bytes'> 10
<class 'str'> 406

显然,打印语法是不同的,正如预期的那样,变量类型在python版本之间有所不同,但在两种情况下都应该像它一样。