μ¬ν΄ μ΄ SpooledTemporaryFileμ΄ 0.12.xμμ 0.14.xλ‘ μ νν ν νμΌ μ λ‘λμμ μμΈλ₯Ό λ°μμν€λ λ¬Έμ μ λΆλͺμ³€μ΅λλ€. κΉμ΄ μ΄ν΄λ³Ό μκ°μ΄ μμκ³ , μ¨λΌμΈμμ λ§μ κ²μ μ°Ύμ μ μμκ³ , κ·Έκ²μ΄ μ°λ¦¬μ μλͺ»μ΄κ±°λ μΌλΆ νΈνλμ§ μλ μ’ μμ±μΌλ‘ μΈν΄ λΉμ μ νλ€κ² νκ³ μΆμ§ μμμ΅λλ€. 0.12.2μ κ³ μ νκ³ λ¬Έμ λ₯Ό κ°μ§ν μ μλ μ½κ°μ ν μ€νΈλ₯Ό μμ±ν λ€μ κ³μ μ§ννμ΅λλ€.
μ€λλ°€ λ΄ λ°μ νΈμ§ν¨μμ μ°λ¦¬κ° λ³Έ κ²κ³Ό λΉμ·ν μλ¦¬κ° λλ flask-adminμ λν SO μ§λ¬Έμ 보μμΌλ―λ‘ μ μ΄λ μΈκΈν κ°μΉκ° μλ€κ³ μκ°νμ΅λλ€. SO μ§λ¬Έμ μ ννλμ§ μμμΌλ―λ‘ κ°μ₯ λ§μ΄ μ¬μ©λμ§ μμ μ μμ΅λλ€(https://stackoverflow.com/questions/51858248/attributeerror-spooledtemporaryfile-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 λ¬Έμ /PR, Python λ¬Έμ λ° λͺ¨λ κ΄λ ¨λ μ μλ λ€λ₯Έ SO μ€λ λ μΈνΈλ₯Ό μ λ ₯νμ΅λλ€. μ¬λΌμ§λ κ²½μ° μ¬κΈ°μ λ€μ μ°κ²°:
κ°μΉκ° μκΈ° λλ¬Έμ Werkzeug λ²μ λ§νΌ κ°λ¨νμ§ μμ κ² κ°μ΅λλ€. μ°λ¦¬λ Alpine3.6μμ python 3.6.6κ³Ό ν¨κ» 0.14.1μ μ¬μ©νκ³ μμΌλ©° λͺ¨λ κ²μ΄ μλν©λλ€. Alpine 3.8.1κ³Ό ν¨κ» Python 3.7.0μμ Werkzeug 0.14.1μ μ¬μ©ν λ λ¬Έμ κ° λνλ©λλ€.
https://github.com/python/cpython/pull/3249 κ° κ²°κ΅ Python μΈ‘μμ μμ μ¬νμ μ°μΆν μ μμ κ² κ°μ΅λλ€.
μλ νμΈμ @abathur - μ΄ν΄λ³΄κ² μ΅λλ€...
λ©λͺ¨:
HI @abathur - μλμ κ°μ΄ 리ν¬μ§ν 리λ₯Ό λ§λ€λ €κ³ νλλ° μ€λ₯λ₯Ό μ¬νν μ μλ κ² κ°μ΅λλ€ =
μ±.νμ΄
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
/cc @davidism
κ·Έ μκ°μ λ³Ό μκ°μ΄ λ§μ§λ μμ§λ§, μ λ μ§κΈ μ¬ννλ λ° μ΄λ €μμ κ²ͺμμ΅λλ€. μ§κΈμ μ΄λ μ€μ΄κ³ μμΉ¨μ λΉνμ μ€λΉνκ³ μμ§λ§, μ΄λ² μ£Ό νλ°μ μ΄κΈ° μνλ₯Ό μ¬νν μ μλμ§ κΈ°μ΅ν΄ λ³΄λ €κ³ λ Έλ ₯νκ² μ΅λλ€.
@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 ,
κ·νμ pytest repo μ€νμ κ°λ΅ν μ΄ν΄λ³΄μκ³ , ν λμ μλ μΆλ ₯μ΄ λ¬Έμ λ₯Ό μ¬νν μ μλ κ²μΌλ‘ 보μ λλ€. μμΌλ‘ λ©°μΉ λμ λ μ΄ν΄λ³΄λ €κ³ νμ§λ§ νλΌμ€ν¬, pytests λ° λΉνμ κ΄ν΄μλ μ΄λ³΄μμ λλ€ π
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
μλ ,
μλ PRμμ SpooledTemporaryFile
μ λμ
μ΄ μ΄ λ¬Έμ μ μμΈμΈ κ² κ°μ΅λλ€...
PR: #1198: μ²ν¬ μ μ‘ μΈμ½λ© μ§μ
PR: #1222 - μ€νλ§λ μμ νμΌμ΄ μλ νλ«νΌμ λν μ§μ μΆκ°
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 λ§ν¬μμμ²λΌ 보μ
λλ€. $# IOBase
μ "κ°μ λ" μΆμ μμ± κ΅¬νμ΄ λλ½λμ΄ SpooledTemporaryFile
ν΄λμ€μ λ¬Έμ κ° μμ΅λλ€. λν μ΄λ 512KB(?)λ³΄λ€ μμ νμΌμ κ²½μ°μλ μν₯μ λ―ΈμΉ©λλ€.
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 λ²μ μ λ°λΌ 리λλ μ
μ μΆκ°νμκ² μ΅λκΉ?μ°λ¦¬λ SpooledTemporaryFile
λ₯Ό μ¬μ©νμ§ μμμ§λ§ μλ μ½λκ° νλ μΌμ νλ λ΄μ₯λ λ°©λ²μ²λΌ 보μμ΅λλ€. μ΄ν λ§μ λ¬Έμ λ₯Ό μΌμΌμΌ°λ€. SpooledTemporaryFile
λ₯Ό μ¬μ©νμ§ μλ κ²μΌλ‘ λμκ°μΌ ν©λλ€.
π bpo-26175μλ νμ¬ μ§ν μ€μΈ pull μμ²μ΄ μμ΅λλ€. μ΅μν python/cpython#3249λ₯Ό μ°Έμ‘°νμΈμ.
@mjpietersμμ μ±ν
μμ λ€λ₯Έ μμ΄λμ΄κ° λ μ¬λμ΅λλ€. $# stream
μ attrμ΄ μμ§λ§ _file
μμ±μ΄ μλ κ²½μ° FileStorage.__getattr__
μ λ³κ²½νμ¬ 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
μλ νμΈμ, μ€λ νλΌμ€ν¬λ₯Ό 1.0.2λ‘ μ κ·Έλ μ΄λνκ³ werkzeug >= 0.14κ° νμν©λλ€.
python 3.6.7(μ°λΆν¬ 18.04μ ν¨κ» μ 곡λ¨)μ΄ μλ λ§μ§λ§ λ²μ μ werkzeug(0.14.1)μμλ μ΄μ λμΌν μ€λ₯κ° λ°μνμ΅λλ€.
AttributeError: 'SpooledTemporaryFile' κ°μ²΄μ 'μ½μ μ μλ' μμ±μ΄ μμ΅λλ€.
μμ μ¬νμ λ²μ 0.14λ‘ μ΄μν κ³νμ λκΉ?
κ°μ¬ν©λλ€.
λ€μ λ²μ μ 0.15μ λλ€. λ¦΄λ¦¬μ€ λ°νλ₯Ό λ³΄λ €λ©΄ μ΄ λ¦¬ν¬μ§ν 리, λΈλ‘κ·Έ λλ https://twitter.com/PalletsTeam μ νλ‘μ°νμΈμ.
κ°μ₯ μ μ©ν λκΈ
μ΄λ €μ΄ μ¬μμ° μ¬λ‘μ λν΄ μ¬κ³Όλ립λλ€. λλ μ°λ¦¬κ° μ¬μ ν λ¬Έμ λ₯Ό λ³΄κ³ μλ€λ κ²μ νμΈνμΌλ©° μ€λ μ€νμ κ·Έκ²μ ν©λ¦¬μ μΌλ‘ κ°κ²°ν μ¬μμ°μΌλ‘ μ€μ΄λ λ° μ½κ°μ μκ°μ ν μ νμ΅λλ€.
κ·Έ κ³Όμ μμ μ μ¬μ μΈ ν΄κ²° λ°©λ²μ λ°κ²¬νμ΅λλ€. ν΄κ²° λ°©λ²μ κ΄λ²μνκ² ν μ€νΈνμ§ μμμΌλ―λ‘ μ체 λ¬Έμ κ° λ°μν μ§ λͺ¨λ₯΄κ² μ΅λλ€(νΉν @jdalegonzalez μ λ³΄κ³ μμμ νλ«νΌμ΄ μ¬κΈ° μμΈμΌ μ μμ).
μ’ μμ±:
ν μ€νΈ: