Nous avons rencontré un problème plus tôt cette année avec SpooledTemporaryFile lançant des exceptions sur les téléchargements de fichiers après être passé de 0.12.x à 0.14.x. Je n'ai pas eu le temps de regarder en profondeur, je n'ai pas trouvé grand-chose en ligne et je ne voulais pas vous faire suer si c'était de notre faute ou si cela se résumait à des dépendances incompatibles. Je viens d'épingler à 0.12.2, j'ai écrit un petit test qui pourrait détecter le problème, et je suis passé à autre chose.
J'ai vu une question SO pour flask-admin dans ma boîte de réception ce soir qui ressemblait à ce que nous avons vu, alors j'ai pensé que cela valait au moins la peine d'être soulevé. La question SO n'est pas très bien formulée, elle n'est donc peut-être pas la plus utile (https://stackoverflow.com/questions/51858248/attributeerror-spooledtemporaryfile-object-has-no-attribute-translate).
Si le test aide :
def test_werkzeug_spooled_temp_file(self):
"""
When we jumped from Werkzeug 0.12.x to 0.14.x, we started running into an error on any attempt to upload a CSV:
File "/home/vagrant/site/videohub/admin.py", line 728, in import_content
for line in csv.DictReader(io.TextIOWrapper(new_file), restval=None):
File "/usr/local/lib/python3.6/dist-packages/werkzeug/datastructures.py", line 2745, in __getattr__
return getattr(self.stream, name)
AttributeError: 'SpooledTemporaryFile' object has no attribute 'readable'
Until/unless we debug this, we need to stay on Werkzeug versions where it doesn't break.
"""
with self.client:
from videohub import admin
admin.auth_admin = lambda: True
try:
what = self.client.post(
"/admin/importcontent/",
data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
content_type="multipart/form-data",
)
except AttributeError as e:
if (
str(e)
== "'SpooledTemporaryFile' object has no attribute 'readable'"
):
self.fail("Werkzeug/SpooledTemporaryFile still exists.")
Dans le fil SO, j'ai encré un ensemble de problèmes/PR github, des problèmes Python et un autre fil SO qui peuvent tous être liés ; re-lien ici au cas où cela disparaît:
Pour ce que ça vaut, ça ne semble pas aussi simple que la version de Werkzeug. Nous utilisons 0.14.1 avec python 3.6.6 sur Alpine3.6 et tout fonctionne. Lorsque nous utilisons Werkzeug 0.14.1 sur python 3.7.0 avec Alpine 3.8.1, nous voyons le problème.
Il semble que https://github.com/python/cpython/pull/3249 puisse éventuellement produire un correctif du côté Python.
Salut @abathur - Je vais jeter un oeil...
Remarques:
HI @abathur - J'ai essayé de créer un repo, comme ci-dessous, et je n'arrive pas à reproduire l'erreur =
app.py
import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = './uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('uploaded_file', filename=filename))
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
from flask import send_from_directory
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
if __name__ == '__main__':
app.run()
Extrait pour générer un gros fichier
dd if=/dev/zero of=filename bs=1024 count=2M
Ubuntu - 18.10
Python - 3.6.7rc1
> pip freeze
Click==7.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
Werkzeug==0.14.1
/cc @davidisme
Je n'ai pas beaucoup de temps pour regarder pour le moment, mais j'ai eu du mal à le reproduire tout à l'heure aussi. Je suis sur la route en ce moment et je prépare un vol dans la matinée, mais j'essaierai de me souvenir pour voir si je peux reproduire les conditions initiales plus tard cette semaine.
@jdalegonzalez Avez-vous des notes sur votre rencontre avec ce problème qui pourraient aider à compléter le tableau entre-temps ?
Toutes mes excuses pour le cas difficile de reproduction. J'ai confirmé que nous voyons toujours le problème et avons passé un peu de temps cet après-midi à le réduire à une reproduction raisonnablement concise.
Dans le processus, j'ai réussi à repérer une solution de contournement potentielle. Je n'ai pas testé la solution de contournement de manière approfondie, donc je ne sais pas si cela causera ses propres problèmes (en particulier compte tenu du rapport de @jdalegonzalez selon lequel la plate-forme peut être un facteur ici).
Dépendances :
pip install flask==1.0.2 pytest pytest-flask
Test:
import pytest, io, csv
from flask import Flask, request
def fields(ob):
return csv.DictReader(ob).fieldnames[0]
@pytest.fixture(scope="session")
def app():
x = Flask(__name__)
x.testing = True
@x.route("/textio", methods=["POST"])
def upload_file_textio():
"""succeeds with 0.12.2; fails with werkzeug==0.14.1;"""
return fields(io.TextIOWrapper(request.files["csv"]))
@x.route("/stringio", methods=["POST"])
def upload_file_stringio():
"""potential workaround; succeeds for both versions"""
return fields(io.StringIO(request.files["csv"].read().decode()))
with x.app_context():
yield x
@pytest.mark.parametrize("uri", ["/textio", "/stringio"])
def test_werkzeug_spooled_temp_file(client, uri):
what = client.post(
uri,
data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
content_type="multipart/form-data",
)
assert what.data == b"my file contents"
Salut @abathur ,
J'ai jeté un coup d'œil sur l'exécution de votre référentiel pytest et en un coup d'œil, la sortie ci-dessous semble être reproductible du problème. J'essaierai d'y jeter un coup d'œil dans les prochains jours, mais je suis novice en matière de flask, pytests et fixtures 😃
Testing started at 20:35 ...
/Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/venv/bin/python "/Users/jayCee/Library/Application Support/JetBrains/Toolbox/apps/PyCharm-P/ch-1/182.4505.26/PyCharm.app/Contents/helpers/pycharm/_jb_pytest_runner.py" --target test_werkzeug_spooled_temp.py::test_werkzeug_spooled_temp_file
Launching pytest with arguments test_werkzeug_spooled_temp.py::test_werkzeug_spooled_temp_file in /Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/test
============================= test session starts ==============================
platform darwin -- Python 3.6.6, pytest-4.0.0, py-1.7.0, pluggy-0.8.0
rootdir: /Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/test, inifile:
plugins: flask-0.14.0collected 2 items
test_werkzeug_spooled_temp.py F
test_werkzeug_spooled_temp.py:27 (test_werkzeug_spooled_temp_file[/textio])
client = <FlaskClient <Flask 'test_werkzeug_spooled_temp'>>, uri = '/textio'
@pytest.mark.parametrize("uri", ["/textio", "/stringio"])
def test_werkzeug_spooled_temp_file(client, uri):
what = client.post(
uri,
data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
> content_type="multipart/form-data",
)
test_werkzeug_spooled_temp.py:34:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../venv/lib/python3.6/site-packages/werkzeug/test.py:840: in post
return self.open(*args, **kw)
../venv/lib/python3.6/site-packages/flask/testing.py:200: in open
follow_redirects=follow_redirects
../venv/lib/python3.6/site-packages/werkzeug/test.py:803: in open
response = self.run_wsgi_app(environ, buffered=buffered)
../venv/lib/python3.6/site-packages/werkzeug/test.py:716: in run_wsgi_app
rv = run_wsgi_app(self.application, environ, buffered=buffered)
../venv/lib/python3.6/site-packages/werkzeug/test.py:923: in run_wsgi_app
app_rv = app(environ, start_response)
../venv/lib/python3.6/site-packages/flask/app.py:2309: in __call__
return self.wsgi_app(environ, start_response)
../venv/lib/python3.6/site-packages/flask/app.py:2295: in wsgi_app
response = self.handle_exception(e)
../venv/lib/python3.6/site-packages/flask/app.py:1741: in handle_exception
reraise(exc_type, exc_value, tb)
../venv/lib/python3.6/site-packages/flask/_compat.py:35: in reraise
raise value
../venv/lib/python3.6/site-packages/flask/app.py:2292: in wsgi_app
response = self.full_dispatch_request()
../venv/lib/python3.6/site-packages/flask/app.py:1815: in full_dispatch_request
rv = self.handle_user_exception(e)
../venv/lib/python3.6/site-packages/flask/app.py:1718: in handle_user_exception
reraise(exc_type, exc_value, tb)
../venv/lib/python3.6/site-packages/flask/_compat.py:35: in reraise
raise value
../venv/lib/python3.6/site-packages/flask/app.py:1813: in full_dispatch_request
rv = self.dispatch_request()
../venv/lib/python3.6/site-packages/flask/app.py:1799: in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
test_werkzeug_spooled_temp.py:17: in upload_file_textio
return fields(io.TextIOWrapper(request.files["csv"]))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <FileStorage: 'test.csv' ('text/csv')>, name = 'readable'
def __getattr__(self, name):
> return getattr(self.stream, name)
E AttributeError: 'SpooledTemporaryFile' object has no attribute 'readable'
../venv/lib/python3.6/site-packages/werkzeug/datastructures.py:2745: AttributeError
. [100%]
=================================== FAILURES ===================================
___________________ test_werkzeug_spooled_temp_file[/textio] ___________________
...
# truncated duplicate above output ...
Environ
macOS Mojave 10.14
Python 3.6.6
pip freeze
atomicwrites==1.2.1
attrs==18.2.0
Click==7.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
more-itertools==4.3.0
pluggy==0.8.0
py==1.7.0
pytest==4.0.0
pytest-flask==0.14.0
six==1.11.0
Werkzeug==0.14.1
Salut,
Il semble que l'introduction de SpooledTemporaryFile
dans les relations publiques ci-dessous soit la source de ce problème...
PR : #1198 : Prise en charge de l'encodage de transfert fragmenté
PR : #1222 - Ajout de la prise en charge des plates-formes dépourvues de fichiers temporaires spoolés
SpooledTemporaryFile
et le total_content_length > max_size
Commentant l'implémentation de SpooledTemporaryFile
- semble réussir le code de test https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862
def default_stream_factory(total_content_length, filename, content_type,
content_length=None):
"""The stream factory that is used per default."""
max_size = 1024 * 500
# if SpooledTemporaryFile is not None:
# return SpooledTemporaryFile(max_size=max_size, mode='wb+')
if total_content_length is None or total_content_length > max_size:
return TemporaryFile('wb+')
return BytesIO()
Cela ressemble à des liens @abathur - il y a un problème avec la classe SpooledTemporaryFile
car il manque l'implémentation des attributs abstraits "supposés" de IOBase
. En outre, cela aurait un impact sur les cas de fichiers inférieurs à 512 kilo-octets (?)
L'application du correctif de singe ci-dessous à FormDataParser.default_stream_factory semble réussir le code de test https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862 :
seekable
, readable
et writable
sont manquants) - https://bugs.python.org/issue26175def default_stream_factory(total_content_length, filename, content_type,
content_length=None):
"""The stream factory that is used per default."""
max_size = 1024 * 500
if SpooledTemporaryFile is not None:
monkeypatch_SpooledTemporaryFile = SpooledTemporaryFile(max_size=max_size, mode='wb+')
monkeypatch_SpooledTemporaryFile.readable = monkeypatch_SpooledTemporaryFile._file.readable
monkeypatch_SpooledTemporaryFile.writable = monkeypatch_SpooledTemporaryFile._file.writable
monkeypatch_SpooledTemporaryFile.seekable = monkeypatch_SpooledTemporaryFile._file.seekable
return monkeypatch_SpooledTemporaryFile
if total_content_length is None or total_content_length > max_size:
return TemporaryFile('wb+')
return BytesIO()
ou peut-être...
class SpooledTemporaryFile_Patched(SpooledTemporaryFile):
"""Patch for `SpooledTemporaryFile exceptions on file upload #1344`
- SpooledTemporaryFile does not fully satisfy the abstract for IOBase - https://bugs.python.org/issue26175
- bpo-26175: Fix SpooledTemporaryFile IOBase abstract - https://github.com/python/cpython/pull/3249
TODO: Remove patch once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved..."""
def readable(self):
return self._file.readable
def writable(self):
return self._file.writable
def seekable(self):
return self._file.seekable
...
def default_stream_factory(total_content_length, filename, content_type, content_length=None):
"""The stream factory that is used per default.
Patch: `SpooledTemporaryFile exceptions on file upload #1344`, Remove once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved...
- SpooledTemporaryFile does not fully satisfy the abstract for IOBase - https://bugs.python.org/issue26175
- bpo-26175: Fix SpooledTemporaryFile IOBase abstract - https://github.com/python/cpython/pull/3249"""
max_size = 1024 * 500
if SpooledTemporaryFile is not None:
# TODO: Remove patch once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved...
SpooledTemporaryFile = SpooledTemporaryFile_Patched
return SpooledTemporaryFile_Patched(max_size=max_size, mode='wb+')
if total_content_length is None or total_content_length > max_size:
return TemporaryFile('wb+')
return BytesIO()
Quelle serait la meilleure marche à suivre ?
SpooledTemporaryFile
? (Il y a probablement une meilleure façon de faire cela...)SpooledTemporaryFile
, jusqu'à ce qu'un correctif soit publié dans une version ultérieure de python et ajoutez une redirection en fonction de la version python ?Auparavant, nous n'utilisions pas SpooledTemporaryFile
, mais cela semblait être un moyen intégré de faire ce que faisait le code d'origine. Cela a causé beaucoup de problèmes depuis. Nous devrions probablement recommencer à ne plus utiliser SpooledTemporaryFile
.
😞 Je note que bpo-26175 a une pull request en cours maintenant, au moins, voir python/cpython#3249
Discuter avec @mjpieters a soulevé une idée différente. Nous pourrions changer FileStorage.__getattr__
pour essayer stream._file
si l'attribut n'existe pas sur stream
mais il a un attribut _file
.
class FileStorage:
def __getattr__(self, name):
try:
return getattr(self.stream, name)
except AttributeError:
if hasattr(self.stream, "_file"):
return getattr(self.stream._file, name)
raise
Bonjour, aujourd'hui, mettez à niveau Flask vers la version 1.0.2 avec besoin de werkzeug >= 0,14
J'ai la même erreur avec la dernière version de werkzeug (0.14.1) avec python 3.6.7 (fournie avec ubuntu 18.04):
AttributeError : l'objet 'SpooledTemporaryFile' n'a pas d'attribut 'lisible'
Prévoyez-vous de porter le correctif vers la version 0.14 ?
Merci.
La prochaine version sera la 0.15. Veuillez suivre ce dépôt, notre blog ou https://twitter.com/PalletsTeam pour les annonces de sortie.
Commentaire le plus utile
Toutes mes excuses pour le cas difficile de reproduction. J'ai confirmé que nous voyons toujours le problème et avons passé un peu de temps cet après-midi à le réduire à une reproduction raisonnablement concise.
Dans le processus, j'ai réussi à repérer une solution de contournement potentielle. Je n'ai pas testé la solution de contournement de manière approfondie, donc je ne sais pas si cela causera ses propres problèmes (en particulier compte tenu du rapport de @jdalegonzalez selon lequel la plate-forme peut être un facteur ici).
Dépendances :
Test: