Оберните открытый поток с io.TextIOWrapper
Как я могу обернуть открытый двоичный поток - Python 2 file
Питон 3 io.BufferedReader
, io.BytesIO
- в io.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'>
Но, конечно, он основан на повторном открытии реального файла из его дескриптора файла. Так что это не сработает в модульных тестах, когда двойной тест 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 ответов
Основываясь на многочисленных предложениях на различных форумах и экспериментируя со стандартной библиотекой, чтобы соответствовать критериям, мой текущий вывод состоит в том, что этого нельзя сделать с библиотекой и типами, какими они у нас сейчас являются.
Используйте codecs.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
В этом ответе изначально предлагалось использовать os.pipe, но сторона канала для чтения в любом случае должна была бы быть обернута в 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() в вашем предыдущем потоке. Это не закрывает основной файл, оно просто удаляет объект необработанного потока, чтобы его можно было повторно использовать. detach() вернет объект, который можно обернуть с помощью TextIOWrapper.
В качестве примера здесь я открываю файл в двоичном режиме чтения, выполняю такое чтение, а затем переключаюсь на поток декодированного текста UTF-8 через io.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))
Вот что я получаю, когда запускаю его как с python 2, так и с python 3.
$ python2.7 this-file.py
(<type 'str'>, 10)
(<type 'unicode'>, 406)
$ python3.6 this-file.py
<class 'bytes'> 10
<class 'str'> 406
Очевидно, что синтаксис печати отличается, и, как и ожидалось, типы переменных различаются в разных версиях Python, но в обоих случаях работает так, как должно.