Werkzeug: 파일 μ—…λ‘œλ“œ μ‹œ SpooledTemporaryFile μ˜ˆμ™Έ

에 λ§Œλ“  2018λ…„ 08μ›” 18일  Β·  13μ½”λ©˜νŠΈ  Β·  좜처: pallets/werkzeug

μ˜¬ν•΄ 초 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 μŠ€λ ˆλ“œ μ„ΈνŠΈλ₯Ό μž…λ ₯ν–ˆμŠ΅λ‹ˆλ‹€. μ‚¬λΌμ§€λŠ” 경우 여기에 λ‹€μ‹œ μ—°κ²°:

κ°€μž₯ μœ μš©ν•œ λŒ“κΈ€

μ–΄λ €μš΄ μž¬μƒμ‚° 사둀에 λŒ€ν•΄ μ‚¬κ³Όλ“œλ¦½λ‹ˆλ‹€. λ‚˜λŠ” μš°λ¦¬κ°€ μ—¬μ „νžˆ 문제λ₯Ό 보고 μžˆλ‹€λŠ” 것을 ν™•μΈν–ˆμœΌλ©° 였늘 μ˜€ν›„μ— 그것을 ν•©λ¦¬μ μœΌλ‘œ κ°„κ²°ν•œ μž¬μƒμ‚°μœΌλ‘œ μ€„μ΄λŠ” 데 μ•½κ°„μ˜ μ‹œκ°„μ„ ν• μ• ν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έ κ³Όμ •μ—μ„œ 잠재적인 ν•΄κ²° 방법을 λ°œκ²¬ν–ˆμŠ΅λ‹ˆλ‹€. ν•΄κ²° 방법을 κ΄‘λ²”μœ„ν•˜κ²Œ ν…ŒμŠ€νŠΈν•˜μ§€ μ•Šμ•˜μœΌλ―€λ‘œ 자체 λ¬Έμ œκ°€ λ°œμƒν• μ§€ λͺ¨λ₯΄κ² μŠ΅λ‹ˆλ‹€(특히 @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"

λͺ¨λ“  13 λŒ“κΈ€

κ°€μΉ˜κ°€ 있기 λ•Œλ¬Έμ— 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: 청크 전솑 인코딩 지원

  • SpooledTemporaryFile λ„μž… - https://github.com/pallets/werkzeug/pull/1198/files#diff-91178c333961d75c285b7784d1b81cecR40)

PR: #1222 - μŠ€ν’€λ§λœ μž„μ‹œ 파일이 μ—†λŠ” ν”Œλž«νΌμ— λŒ€ν•œ 지원 μΆ”κ°€

  • FormDataParser.default_stream_factory 에 λŒ€ν•œ λ™μž‘ κ΅¬ν˜„ - 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λ₯Ό ν†΅κ³Όν•˜λŠ” κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€.

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:
        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 에 λŒ€ν•œ μ›μˆ­μ΄ 패치λ₯Ό κ΅¬ν˜„ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? (μ•„λ§ˆλ„ 더 쒋은 방법이 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€...)
  • 이후 λ²„μ „μ˜ Pythonμ—μ„œ μˆ˜μ • 사항이 릴리슀될 λ•ŒκΉŒμ§€ 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 을 νŒ”λ‘œμš°ν•˜μ„Έμš”.

이 νŽ˜μ΄μ§€κ°€ 도움이 λ˜μ—ˆλ‚˜μš”?
0 / 5 - 0 λ“±κΈ‰