Nos encontramos con un problema a principios de este año con SpooledTemporaryFile que lanzaba excepciones en las cargas de archivos después de pasar de 0.12.x a 0.14.x. No tuve tiempo de echar un vistazo profundo, no pude encontrar mucho en línea y no quería molestarlos a todos si era culpa nuestra o si se trataba de algunas dependencias incompatibles. Solo fijé a 0.12.2, escribí una pequeña prueba que podría detectar el problema y seguí adelante.
Vi una pregunta de SO para el administrador de matraz en mi bandeja de entrada esta noche que sonaba similar a lo que vimos, así que pensé que al menos valía la pena mencionarla. La pregunta SO no está muy bien formulada, por lo que puede que no sea muy útil (https://stackoverflow.com/questions/51858248/attributeerror-spooledtemporaryfile-object-has-no-attribute-translate).
En caso de que la prueba ayude:
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.")
En el subproceso SO, entinté un conjunto de problemas/PR de github, problemas de Python y otro subproceso SO que pueden estar todos relacionados; Vuelvo a vincular aquí en caso de que desaparezca:
Por lo que vale, no parece ser tan simple como la versión de Werkzeug. Estamos usando 0.14.1 con python 3.6.6 en Alpine3.6 y todo funciona. Cuando usamos Werkzeug 0.14.1 en python 3.7.0 con Alpine 3.8.1, vemos el problema.
Parece que https://github.com/python/cpython/pull/3249 eventualmente puede brindar una solución en el lado de Python.
Hola @abathur - Voy a echar un vistazo...
Notas:
HI @abathur : traté de crear un repositorio, como se indica a continuación, y parece que no pude reproducir el error =
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()
Fragmento para generar un archivo grande
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 @davidismo
No tengo mucho tiempo para mirar el momento, pero también tuve problemas para reproducirlo ahora. Estoy en la carretera en este momento y preparándome para un vuelo por la mañana, pero intentaré recordar para ver si puedo reproducir las condiciones iniciales a finales de esta semana.
@jdalegonzalez ¿Tiene algunas notas sobre su encuentro con este problema que podrían ayudar a completar la imagen mientras tanto?
Disculpas por el difícil caso de reproducción. Confirmé que todavía estamos viendo el problema y dediqué un poco de tiempo esta tarde a reducirlo a una reproducción razonablemente concisa.
En el proceso logré detectar una posible solución. No probé la solución de manera exhaustiva, por lo que no sé si causará problemas propios (especialmente dado el informe de @jdalegonzalez de que la plataforma puede ser un factor aquí).
Dependencias:
pip install flask==1.0.2 pytest pytest-flask
Prueba:
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"
Hola @abathur ,
Eché un vistazo rápido a la ejecución de su repositorio de pytest y, de un vistazo, el resultado a continuación parece ser reproducible del problema. Intentaré echar un vistazo más en los próximos días, pero soy un novato en lo que respecta a matraces, pruebas de pytest y accesorios 😃
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 ...
Reinar
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
Hola,
Parece que la introducción de SpooledTemporaryFile
de los siguientes PR es la fuente de este problema...
PR: #1198: Admite codificación de transferencia fragmentada
PR: #1222 - Soporte agregado para plataformas que carecen de archivos temporales en cola
SpooledTemporaryFile
y el total_content_length > max_size
Al comentar la implementación de SpooledTemporaryFile
, parece pasar el código de prueba 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()
Parece que de los enlaces de @abathur : hay un problema con la clase SpooledTemporaryFile
ya que falta la implementación de los atributos abstractos "asumidos" de IOBase
. Además, esto afectaría los casos de archivos que tienen menos de 512 kilobytes (?)
Al aplicar el siguiente parche de mono a FormDataParser.default_stream_factory , se busca pasar el código de prueba https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862:
seekable
, readable
y writable
): 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()
o tal vez...
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()
¿Cuál sería el mejor curso de acción?
SpooledTemporaryFile
? (Probablemente hay una mejor manera de hacer esto...)SpooledTemporaryFile
, hasta que se publique una solución en una versión posterior de python y agregue una redirección según la versión de python.Solíamos no usar SpooledTemporaryFile
, pero parecía una forma integrada de hacer lo que hacía el código original. Ha causado muchos problemas desde entonces. Probablemente deberíamos volver a no usar SpooledTemporaryFile
.
😞 Observo que bpo-26175 tiene una solicitud de extracción en curso ahora, al menos, consulte python/cpython#3249
El chat con @mjpieters planteó una idea diferente. Podríamos cambiar FileStorage.__getattr__
para probar stream._file
si el atributo no existe en stream
pero tiene un atributo _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
Hola, hoy actualice el matraz a 1.0.2 con necesidad de werkzeug> = 0.14
Tengo este mismo error con la última versión de werkzeug (0.14.1) con python 3.6.7 (que viene con ubuntu 18.04):
AttributeError: el objeto 'SpooledTemporaryFile' no tiene atributo 'legible'
¿Planea portar la corrección a la versión 0.14?
Gracias.
La próxima versión será la 0.15. Siga este repositorio, nuestro blog o https://twitter.com/PalletsTeam para ver los anuncios de lanzamiento.
Comentario más útil
Disculpas por el difícil caso de reproducción. Confirmé que todavía estamos viendo el problema y dediqué un poco de tiempo esta tarde a reducirlo a una reproducción razonablemente concisa.
En el proceso logré detectar una posible solución. No probé la solución de manera exhaustiva, por lo que no sé si causará problemas propios (especialmente dado el informe de @jdalegonzalez de que la plataforma puede ser un factor aquí).
Dependencias:
Prueba: