Оберните открытый поток с помощью io.TextIOWrapper

как я могу обернуть открытый двоичный поток-Python 2 file питон 3 io.BufferedReader, an io.BytesIO – в io.TextIOWrapper?

я пытаюсь написать код, который будет работать без изменений:

  • работает на Python 2.
  • работает на Python 3.
  • с двоичными потоками, генерируемыми из стандартной библиотеки (т. е. я не могу контролировать, какой тип они)
  • с двоичными потоками, сделанными тестовыми двойниками (т. е. без дескриптора файла, не может re-open).
  • производства io.TextIOWrapper который обертывает указанный поток.

на io.TextIOWrapper необходим, потому что его API ожидается другими частями стандартной библиотеки. Существуют и другие типы файлов, но не предоставляют правильный API.

пример

обертывание двоичного потока, представленного как :

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'>

но, конечно, это полагается на повторное открытие реального файла из его дескриптора файла. Таким образом, он терпит неудачу в модульных тестах, когда тестовый двойник 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 ответов


использовать кодеки.getreader для создания объекта-оболочки:

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

работает на Python 2 и Python 3.


основываясь на нескольких предложениях на различных форумах и экспериментируя со стандартной библиотекой, чтобы соответствовать критериям, мой текущий вывод это невозможно сделать с библиотекой и типами, как мы в настоящее время их имеем.


оказывается, вам просто нужно обернуть свой io.BytesIO на io.BufferedReader который существует как на Python 2, так и на Python 3.

import io

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

этот ответ первоначально предложил использовать ОС.труба, но считывающая сторона трубы должна быть обернута в io.BufferedReader на Python 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)

мне это тоже было нужно, но, основываясь на потоке здесь, я определил, что невозможно использовать только Python 2 io модуль. В то время как это нарушает ваше "особое отношение к file" правило, техника, с которой я пошел, заключалась в создании чрезвычайно тонкой обертки для file (код ниже), который затем может быть обернут в io.BufferedReader, который в свою очередь может быть передан io.TextIOWrapper конструктор. Это будет боль для модульного теста, поскольку, очевидно, новый путь кода не может быть протестирован на Python 3.

кстати, причина в результатахopen() может быть передан непосредственно к io.TextIOWrapper в Python 3-это потому, что двоичный режим 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)

по крайней мере, это мало, поэтому, надеюсь, он минимизирует воздействие на детали, которые не могут быть легко проверены.


вот некоторый код, который я тестировал как в python 2.7, так и в python 3.6.

ключ здесь в том, что вам нужно сначала использовать detach() в предыдущем потоке. Это не закрывает базовый файл, он просто вырывает объект raw stream, чтобы его можно было использовать повторно. detach () вернет объект, который можно обернуть с помощью TextIOWrapper.

в качестве примера здесь я открываю файл в двоичном режиме чтения, делаю чтение на нем, а затем переключаюсь на декодированный текстовый поток UTF-8 через Ио.TextIOWrapper.

Я сохранил этот пример как 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, но работает так, как должно в обоих случаях.