web-dev-qa-db-ja.com

io.TextIOWrapperでオープンストリームをラップする

オープンバイナリストリームをラップする方法-a Python 2 file、a Python 3 io.BufferedReaderio.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.*オブジェクトとは異なる方法で処理することです。

これは、十分にテストされたコードのオプションではありません。ユニットテストのブランチを作成するためです。可能な限り高速に実行するために、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 AP​​Iの提供に失敗します。具体的には、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、テストダブルと実際のオブジェクトの両方で動作するコード、つまりwraps既に開いているバイトストリームの周りのio.TextIOWrapper

30
bignose

さまざまなフォーラムでの複数の提案に基づいて、基準を満たすために標準ライブラリを試してみると、私の現在の結論はこれはできません現在持っているライブラリとタイプです。

4
bignose

codecs.getreader を使用して、ラッパーオブジェクトを生成します。

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

Python 2およびPython 3。

14
jbg

ラップするだけで_io.BytesIO in 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を使用することを示唆していましたが、パイプの読み取り側はPython 2で動作するためにio.BufferedReaderでラップする必要があるため、このソリューションはよりシンプルで割り当てを回避しますパイプ。

6
jbg

私もこれが必要でしたが、ここのスレッドに基づいて、Python 2のioモジュールを使用することは不可能であると判断しました。これにより、fileの特別な扱い]私が行ったのは、file(以下のコード)の非常に薄いラッパーを作成し、それを_io.BufferedReader_でラップし、それを_io.TextIOWrapper_コンストラクターに渡すことでした。明らかに、新しいコードパスはPython 3。

ちなみに、open()の結果を_io.TextIOWrapper_ in 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)
_

少なくともこれは小さいので、単体テストが簡単にできない部品の露出を最小限に抑えることが望まれます。

2
Vek

さて、これは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)
2
jbg

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バージョン間で異なりますが、両方の場合に必要なように機能します。

0