web-dev-qa-db-fra.com

Envelopper un flux ouvert avec io.TextIOWrapper

Comment puis-je envelopper un flux binaire ouvert - a Python 2 file, a Python 3 io.BufferedReader, un io.BytesIO - dans un io.TextIOWrapper?

J'essaie d'écrire du code qui fonctionnera sans changement:

  • Fonctionnant sur Python 2.
  • Fonctionnant sur Python 3.
  • Avec des flux binaires générés à partir de la bibliothèque standard (c'est-à-dire que je ne peux pas contrôler de quel type ils sont)
  • Avec des flux binaires conçus pour être des doubles de test (c'est-à-dire pas de descripteur de fichier, ne peut pas se rouvrir).
  • Produire un io.TextIOWrapper qui enveloppe le flux spécifié.

Le io.TextIOWrapper est nécessaire car son API est attendue par d'autres parties de la bibliothèque standard. Il existe d'autres types de fichiers, mais ne fournissent pas la bonne API.

Exemple

Envelopper le flux binaire présenté comme subprocess.Popen.stdout attribut:

import subprocess
import io

gnupg_subprocess = subprocess.Popen(
        ["gpg", "--version"], stdout=subprocess.PIPE)
gnupg_stdout = io.TextIOWrapper(gnupg_subprocess.stdout, encoding="utf-8")

Dans les tests unitaires, le flux est remplacé par un io.BytesIO instance pour contrôler son contenu sans toucher à aucun sous-processus ou système de fichiers.

gnupg_subprocess.stdout = io.BytesIO("Lorem ipsum".encode("utf-8"))

Cela fonctionne très bien sur les flux créés par Python 3 bibliothèque standard. Le même code, cependant, échoue sur les flux générés par 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'

Pas une solution: traitement spécial pour file

Une réponse évidente consiste à avoir une branche dans le code qui teste si le flux est réellement un objet Python 2 file, et à gérer cela différemment de io.* objets.

Ce n'est pas une option pour un code bien testé, car cela crée une branche qui teste uniquement - qui, pour fonctionner aussi vite que possible, ne doit pas en créer réel objets du système de fichiers - ne peuvent pas s'exercer.

Les tests unitaires fourniront des doubles de test, pas de vrais objets file. Donc, créer une branche qui ne sera pas exercée par ces doubles de test, c'est vaincre la suite de tests.

Pas une solution: io.open

Certains répondants suggèrent une réouverture (par exemple avec io.open) le descripteur de fichier sous-jacent:

gnupg_stdout = io.open(
        gnupg_subprocess.stdout.fileno(), mode='r', encoding="utf-8")

Cela fonctionne sur les deux Python 3 et 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'>

Mais bien sûr, il repose sur la réouverture d'un vrai fichier à partir de son descripteur de fichier. Il échoue donc dans les tests unitaires lorsque le double test est un io.BytesIO exemple:

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

Pas une solution: codecs.getreader

La bibliothèque standard possède également le module codecs, qui fournit des fonctionnalités d'encapsuleur:

import codecs

gnupg_stdout = codecs.getreader("utf-8")(gnupg_subprocess.stdout)

C'est bien car il n'essaie pas de rouvrir le flux. Mais il ne fournit pas le io.TextIOWrapper API. Plus précisément, il n'hérite pas de io.IOBase et n'a pas l'attribut 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'

Donc codecs ne fournit pas d'objets qui se substituent à io.TextIOWrapper.

Que faire?

Alors, comment puis-je écrire du code qui fonctionne à la fois Python 2 et Python 3, avec à la fois le test double et les objets réels, qui encapsule un io.TextIOWrapper autour du flux d'octets déjà ouvert?

30
bignose

Sur la base de plusieurs suggestions dans divers forums et en expérimentant avec la bibliothèque standard pour répondre aux critères, ma conclusion actuelle est cela ne peut pas être fait avec la bibliothèque et les types tels que nous les avons actuellement.

4
bignose

Utilisez codecs.getreader pour produire un objet wrapper:

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

Fonctionne sur Python 2 et Python 3.

14
jbg

Il s'avère que vous avez juste besoin d'envelopper votre io.BytesIO dans io.BufferedReader qui existe sur les deux Python 2 et Python 3.

import io

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

Cette réponse suggérait à l'origine d'utiliser os.pipe, mais le côté lecture du tuyau devrait être enveloppé dans io.BufferedReader sur Python 2 de toute façon pour fonctionner, donc cette solution est plus simple et évite d'allouer un tuyau.

6
jbg

J'en avais également besoin, mais sur la base du fil ici, j'ai déterminé qu'il n'était pas possible d'utiliser simplement le module Python 2 io. Bien que cela casse votre "Traitement spécial pour file ", la technique avec laquelle j'ai opté était de créer un wrapper extrêmement fin pour file (code ci-dessous) qui pourrait ensuite être enveloppé dans un io.BufferedReader, Qui peut à son tour être passé au constructeur io.TextIOWrapper. Ce sera un test unitaire difficile, car évidemment le nouveau chemin de code ne peut pas être testé sur Python 3.

Par ailleurs, la raison pour laquelle les résultats d'un open() peuvent être transmis directement à io.TextIOWrapper Dans Python 3 est parce qu'un mode binaire open() est en fait retourne une instance io.BufferedReader pour commencer (au moins sur Python 3.4, qui est l'endroit où je testais à l'époque).

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)

Au moins, c'est petit, donc j'espère que cela minimise l'exposition pour les pièces qui ne peuvent pas facilement être testées à l'unité.

2
Vek

D'accord, cela semble être une solution complète, pour tous les cas mentionnés dans la question, testés avec Python 2.7 et Python 3.5. La solution générale a fini par être rouvrir le descripteur de fichier, mais au lieu de io.BytesIO, vous devez utiliser un canal pour votre test double afin d'avoir un descripteur de fichier.

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

Voici du code que j'ai testé dans les deux python 2.7 et python 3.6.

La clé ici est que vous devez d'abord utiliser detach () sur votre flux précédent. Cela ne ferme pas le fichier sous-jacent, il arrache simplement l'objet de flux brut afin qu'il puisse être réutilisé. detach () renverra un objet qui peut être encapsulé avec TextIOWrapper.

À titre d'exemple ici, j'ouvre un fichier en mode de lecture binaire, je fais une lecture comme ça, puis je passe à un flux de texte décodé UTF-8 via io.TextIOWrapper.

J'ai enregistré cet exemple sous le nom 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))

Voici ce que j'obtiens lorsque je l'exécute avec python2 et python3.

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

Évidemment, la syntaxe d'impression est différente et, comme prévu, les types de variables diffèrent entre les versions python mais fonctionnent comme dans les deux cas.

0
Grey Christoforo