واجهتنا مشكلة في وقت سابق من هذا العام مع SpooledTporaryFile بإلقاء استثناءات على عمليات تحميل الملفات بعد القفز من 0.12.x إلى 0.14.x. لم يكن لديك الوقت لإلقاء نظرة متعمقة ، ولم أستطع العثور على الكثير عبر الإنترنت ، ولم أرغب في إجهادك جميعًا إذا كان خطأنا أو نزلنا إلى بعض التبعيات غير المتوافقة. تم التثبيت للتو على 0.12.2 ، وكتب اختبارًا صغيرًا يمكنه اكتشاف المشكلة ، والمضي قدمًا.
لقد رأيت سؤالًا خاصًا بـ flask-admin في صندوق الوارد الخاص بي الليلة بدا مشابهًا لما رأيناه ، لذلك اعتقدت أنه على الأقل يستحق التحدث. سؤال SO ليس مصاغًا جيدًا ، لذلك قد لا يكون الأكثر استخدامًا (https://stackoverflow.com/questions/51858248/attributeerror-spooledtertainfile-object-has-no-attribute-translate).
في حال كان الاختبار يساعد:
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.")
في خيط SO ، قمت بتوقيع مجموعة من مشكلات github / العلاقات العامة ، ومشكلات Python ، وخيط SO آخر قد يكون جميعها ذات صلة ؛ إعادة الربط هنا في حالة اختفاء ذلك:
لما يستحق الأمر ، لا يبدو أنه بسيط مثل إصدار Werkzeug. نحن نستخدم 0.14.1 مع python 3.6.6 على Alpine3.6 وكل شيء يعمل. عندما نستخدم Werkzeug 0.14.1 على python 3.7.0 مع Alpine 3.8.1 فإننا نرى المشكلة.
يبدو أن https://github.com/python/cpython/pull/3249 قد يؤدي في النهاية إلى إصلاح جانب Python.
مرحبًا abathur - سألقي نظرة ...
ملاحظات:
مرحبًا abathur - لقد حاولت إنشاء الريبو ، وفقًا لما هو مذكور أدناه ، ولم أستطع إعادة إنتاج الخطأ =
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()
مقتطف لإنشاء ملف كبير
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
/ ccdavidism
ليس لدي الكثير من الوقت للنظر في هذه اللحظة ، لكنني واجهت مشكلة في إعادة إنتاجها الآن أيضًا. أنا على الطريق في الوقت الحالي وأستعد لرحلة في الصباح ، لكنني أحاول أن أتذكر معرفة ما إذا كان بإمكاني إعادة إنتاج الظروف الأولية في وقت لاحق من هذا الأسبوع.
jdalegonzalez هل لديك بعض الملاحظات حول مشاركتك مع هذه المشكلة والتي قد تساعد في ملء الصورة في هذه الأثناء؟
الاعتذار عن حالة التكاثر الصعبة. لقد أكدت أننا ما زلنا نرى المشكلة وأمضينا بعض الوقت بعد ظهر هذا اليوم في تقليصها إلى إعادة إنتاج مقتضبة بشكل معقول.
في هذه العملية ، تمكنت من اكتشاف حل بديل محتمل. لم أختبر الحل البديل على نطاق واسع ، لذلك لا أعرف ما إذا كان سيتسبب في مشاكل من تلقاء نفسه (خاصة بالنظر إلى تقرير jdalegonzalez أن النظام الأساسي قد يكون عاملاً هنا).
التبعيات:
pip install flask==1.0.2 pytest pytest-flask
اختبار:
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"
مرحبا abathur ،
لقد ألقيت نظرة سريعة على تنفيذ الريبو الخاص بك وفي لمحة ، يبدو أن الإخراج أدناه قابل لإعادة إنتاج المشكلة. سأحاول إلقاء نظرة أخرى في الأيام القليلة المقبلة ، لكنني مبتدئ فيما يتعلق بالقارورة والأفات والتركيبات 😃
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 ...
بيئة
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
أهلا،
يبدو أن تقديم SpooledTemporaryFile
من العلاقات العامة أدناه هو مصدر هذه المشكلة ...
PR: # 1198: دعم ترميز النقل المقسم
PR: # 1222 - دعم إضافي للأنظمة الأساسية التي تفتقر إلى ملفات temp المخزنة
SpooledTemporaryFile
و total_content_length > max_size
التعليق على تنفيذ SpooledTemporaryFile
- يبدو أنه اجتياز رمز الاختبار 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()
يبدو أنه من ارتباطات abathur - توجد مشكلة في فئة SpooledTemporaryFile
لأنها تفتقد إلى تنفيذ السمات المجردة "المفترضة" لـ IOBase
. أيضًا ، قد يؤثر هذا على حالات الملفات التي يقل حجمها عن 512 كيلو بايت (؟)
يتطلع تطبيق التصحيح القرد أدناه على FormDataParser.default_stream_factory إلى اجتياز رمز الاختبار https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862:
seekable
و readable
و 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()
أو ربما...
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()
ما هو أفضل مسار للعمل؟
SpooledTemporaryFile
؟ (ربما توجد طريقة أفضل للقيام بذلك ...)SpooledTemporaryFile
، حتى يتم إصدار إصلاح في إصدار لاحق من python وإضافة إعادة توجيه اعتمادًا على الإصدار python؟اعتدنا على عدم استخدام SpooledTemporaryFile
، ولكن بدا الأمر وكأنه طريقة مضمنة للقيام بما كان يفعله الكود الأصلي. لقد تسبب في الكثير من المشاكل منذ ذلك الحين. ربما علينا العودة إلى عدم استخدام SpooledTemporaryFile
.
😞 ألاحظ أن bpo-26175 لديه طلب سحب قيد التقدم الآن ، على الأقل ، راجع python / cpython # 3249
تحدثت الدردشة مع mjpieters عن فكرة مختلفة. يمكننا تغيير FileStorage.__getattr__
لتجربة stream._file
إذا كانت سمة غير موجودة في stream
لكنها تحتوي على سمة _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
مرحبًا ، قم اليوم بترقية flask إلى 1.0.2 مع need werkzeug> = 0.14
لدي نفس الخطأ مع الإصدار الأخير من werkzeug (0.14.1) مع python 3.6.7 (الذي يأتي مع ubuntu 18.04):
AttributeError: الكائن "SpooledTarilyFile" ليس له سمة "قابل للقراءة"
هل تخطط لتحويل الإصلاح إلى الإصدار 0.14؟
شكرا لك.
الإصدار القادم سيكون 0.15. يرجى اتباع هذا الريبو أو مدونتنا أو https://twitter.com/PalletsTeam للحصول على إعلانات الإصدار.
التعليق الأكثر فائدة
الاعتذار عن حالة التكاثر الصعبة. لقد أكدت أننا ما زلنا نرى المشكلة وأمضينا بعض الوقت بعد ظهر هذا اليوم في تقليصها إلى إعادة إنتاج مقتضبة بشكل معقول.
في هذه العملية ، تمكنت من اكتشاف حل بديل محتمل. لم أختبر الحل البديل على نطاق واسع ، لذلك لا أعرف ما إذا كان سيتسبب في مشاكل من تلقاء نفسه (خاصة بالنظر إلى تقرير jdalegonzalez أن النظام الأساسي قد يكون عاملاً هنا).
التبعيات:
اختبار: