Werkzeug: рдлрд╝рд╛рдЗрд▓ рдЕрдкрд▓реЛрдб рдкрд░ SpooledTemporaryрдлрд╝рд╛рдЗрд▓ рдЕрдкрд╡рд╛рдж

рдХреЛ рдирд┐рд░реНрдорд┐рдд 18 рдЕрдЧре░ 2018  ┬╖  13рдЯрд┐рдкреНрдкрдгрд┐рдпрд╛рдБ  ┬╖  рд╕реНрд░реЛрдд: pallets/werkzeug

рд╣рдордиреЗ рдЗрд╕ рд╕рд╛рд▓ рдХреА рд╢реБрд░реБрдЖрдд рдореЗрдВ рдПрдХ рд╕рдорд╕реНрдпрд╛ рдХрд╛ рд╕рд╛рдордирд╛ рдХрд┐рдпрд╛ рдЬрд┐рд╕рдореЗрдВ SpooledTemporaryFile 0.12.x рд╕реЗ 0.14.x рддрдХ рдХреВрджрдиреЗ рдХреЗ рдмрд╛рдж рдлрд╝рд╛рдЗрд▓ рдЕрдкрд▓реЛрдб рдкрд░ рдЕрдкрд╡рд╛рдж рдлреЗрдВрдХ рд░рд╣рд╛ рдерд╛ред рдЧрд╣рд░рд╛рдИ рд╕реЗ рджреЗрдЦрдиреЗ рдХрд╛ рд╕рдордп рдирд╣реАрдВ рдерд╛, рдСрдирд▓рд╛рдЗрди рдмрд╣реБрдд рдХреБрдЫ рдирд╣реАрдВ рдорд┐рд▓ рд╕рдХрд╛, рдФрд░ рдЕрдЧрд░ рдпрд╣ рд╣рдорд╛рд░реА рдЧрд▓рддреА рдереА рдпрд╛ рдХреБрдЫ рдЕрд╕рдВрдЧрдд рдирд┐рд░реНрднрд░рддрд╛рдУрдВ рдХреЗ рдХрд╛рд░рдг рдЖрдк рд╕рднреА рдХреЛ рдкрд╕реАрдирд╛ рдирд╣реАрдВ рдЪрд╛рд╣рддрд╛ рдерд╛ред рдмрд╕ 0.12.2 рдкрд░ рдкрд┐рди рдХрд┐рдпрд╛ рдЧрдпрд╛, рдПрдХ рдЫреЛрдЯрд╛ рдкрд░реАрдХреНрд╖рдг рд▓рд┐рдЦрд╛ рдЬреЛ рд╕рдорд╕реНрдпрд╛ рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛ рд╕рдХреЗ, рдФрд░ рдЖрдЧреЗ рдмрдврд╝ рдЧрдпрд╛ред

рдореИрдВрдиреЗ рдЖрдЬ рд░рд╛рдд рдЕрдкрдиреЗ рдЗрдирдмреЙрдХреНрд╕ рдореЗрдВ рдлреНрд▓рд╛рд╕реНрдХ-рдПрдбрдорд┐рди рдХреЗ рд▓рд┐рдП рдПрдХ 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 рдореБрджреНрджреЛрдВ/PRs, 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 рдХреЗ рд╕рдВрд╕реНрдХрд░рдг рдХреЗ рд░реВрдк рдореЗрдВ рдЙрддрдирд╛ рдЖрд╕рд╛рди рдирд╣реАрдВ рд▓рдЧрддрд╛ рд╣реИред рд╣рдо рдЕрд▓реНрдкрд╛рдЗрди 3.6 рдкрд░ рдЕрдЬрдЧрд░ 3.6.6 рдХреЗ рд╕рд╛рде 0.14.1 рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд░рд╣реЗ рд╣реИрдВ рдФрд░ рд╕рдм рдХреБрдЫ рдХрд╛рдо рдХрд░ рд░рд╣рд╛ рд╣реИред рдЬрдм рд╣рдо рдПрд▓реНрдкрд╛рдЗрди 3.8.1 рдХреЗ рд╕рд╛рде рдЕрдЬрдЧрд░ 3.7.0 рдкрд░ Werkzeug 0.14.1 рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ рддреЛ рд╣рдо рд╕рдорд╕реНрдпрд╛ рджреЗрдЦрддреЗ рд╣реИрдВред

рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ https://github.com/python/cpython/pull/3249 рдЕрдВрддрддрдГ рдкрд╛рдЗрдерди рдкрдХреНрд╖ рдкрд░ рдПрдХ рдлрд┐рдХреНрд╕ рдЙрддреНрдкрдиреНрди рдХрд░ рд╕рдХрддрд╛ рд╣реИред

рд╣рд╛рдп @abathur - рдореИрдВ рджреЗрдЦ рд▓реВрдВрдЧрд╛ ...

рдЯрд┐рдкреНрдкрдгрд┐рдпрд╛рдБ:

HI @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

/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 ,

рдореИрдВрдиреЗ рдЖрдкрдХреЗ рдкрд╛рдЗрдЯреЗрд╕реНрдЯ рд░реЗрдкреЛ рдХреЛ рдХреНрд░рд┐рдпрд╛рдиреНрд╡рд┐рдд рдХрд░рдиреЗ рдкрд░ рдПрдХ рддреНрд╡рд░рд┐рдд рдирдЬрд╝рд░ рдбрд╛рд▓реА рдФрд░ рдПрдХ рдирдЬрд╝рд░ рдореЗрдВ рдиреАрдЪреЗ рджрд┐рдпрд╛ рдЧрдпрд╛ рдЖрдЙрдЯрдкреБрдЯ рд╕рдорд╕реНрдпрд╛ рдХрд╛ рдкреБрдирд░реБрддреНрдкрд╛рджрди рдпреЛрдЧреНрдп рдкреНрд░рддреАрдд рд╣реЛрддрд╛ рд╣реИред рдореИрдВ рдЕрдЧрд▓реЗ рдХреБрдЫ рджрд┐рдиреЛрдВ рдореЗрдВ рдФрд░ рджреЗрдЦрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░реВрдВрдЧрд╛, рд▓реЗрдХрд┐рди рдореИрдВ рдлреНрд▓рд╛рд╕реНрдХ, рдкрд╛рдЗрдЯреЗрд╕реНрдЯ рдФрд░ рдлрд┐рдХреНрд╕реНрдЪрд░ рдХреЗ рд╕рдВрдмрдВрдз рдореЗрдВ рдиреМрд╕рд┐рдЦрд┐рдпрд╛ рд╣реВрдВ

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 рдХреА рд╢реБрд░реВрдЖрдд рдЗрд╕ рдореБрджреНрджреЗ рдХрд╛ рд╕реНрд░реЛрдд рд╣реИ...

рдкреАрдЖрд░: #1198: рдЦрдВрдбрд┐рдд рд╕реНрдерд╛рдирд╛рдВрддрд░рдг рдПрдиреНрдХреЛрдбрд┐рдВрдЧ рдХрд╛ рд╕рдорд░реНрдерди рдХрд░реЗрдВ

  • рдкреЗрд╢ рд╣реИ рд╕реНрдкреВрд▓реЗрдб рдЯреЗрдореНрдкреЛрд░рд░реАрдлрд╛рдЗрд▓ - https://github.com/pallets/werkzeug/pull/1198/files#diff-91178c333961d75c285b7784d1b81cecR40)

рдкреАрдЖрд░: #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 рд▓рд┐рдВрдХ рд╕реЗ рджрд┐рдЦрддрд╛ рд╣реИ - SpooledTemporaryFile рд╡рд░реНрдЧ рдХреЗ рд╕рд╛рде рдПрдХ рд╕рдорд╕реНрдпрд╛ рд╣реИ рдХреНрдпреЛрдВрдХрд┐ рдЗрд╕рдореЗрдВ $#$6 IOBase #$ рдХреЗ "рдЕрдиреБрдорд╛рдирд┐рдд" рдЕрдореВрд░реНрдд рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХрд╛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдЧреБрдо рд╣реИред рд╕рд╛рде рд╣реА, рдпрд╣ рдЙрди рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЗ рдорд╛рдорд▓реЛрдВ рдХреЛ рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░реЗрдЧрд╛ рдЬреЛ 512 рдХрд┐рд▓реЛрдмрд╛рдЗрдЯ (?)

рдиреАрдЪреЗ рдмрдВрджрд░ рдкреИрдЪ рдХреЛ 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 рдХреЗ рд▓рд┐рдП рдордВрдХреА рдкреИрдЪ рд▓рд╛рдЧреВ рдХрд░реЗрдВ? (рдРрд╕рд╛ рдХрд░рдиреЗ рдХрд╛ рд╢рд╛рдпрдж рдПрдХ рдмреЗрд╣рддрд░ рддрд░реАрдХрд╛ рд╣реИ ...)
  • SpooledTemporaryFile рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╕реЗ рдмрдЪреЗрдВ, рдЬрдм рддрдХ рдХрд┐ рдкрд╛рдЗрдерди рдХреЗ рдмрд╛рдж рдХреЗ рд╕рдВрд╕реНрдХрд░рдг рдореЗрдВ рдПрдХ рдлрд┐рдХреНрд╕ рдЬрд╛рд░реА рдирд╣реАрдВ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рдФрд░ рдкрд╛рдЗрдерди рд╡рд░реН рдХреЗ рдЖрдзрд╛рд░ рдкрд░ рдкреБрдирд░реНрдирд┐рд░реНрджреЗрд╢рди рдЬреЛрдбрд╝рддрд╛ рд╣реИ?
  • рдореИрдВ рдЕрднреА рднреА рдЗрд╕ рдбреЛрдореЗрди рдХреЗ рд▓рд┐рдП рдиреМрд╕рд┐рдЦрд┐рдпрд╛ рд╣реВрдВ, рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдЕрдиреНрдп рд╡рд┐рдХрд▓реНрдк рднреА рд╣реИрдВ? :)

рд╣рдо рдЙрдкрдпреЛрдЧ рдирд╣реАрдВ рдХрд░рддреЗ рдереЗ SpooledTemporaryFile , рд▓реЗрдХрд┐рди рдпрд╣ рдореВрд▓ рдХреЛрдб рдЬреЛ рдХрд░ рд░рд╣рд╛ рдерд╛ рдЙрд╕реЗ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдЕрдВрддрд░реНрдирд┐рд╣рд┐рдд рддрд░реАрдХреЗ рдХреА рддрд░рд╣ рд▓рдЧ рд░рд╣рд╛ рдерд╛ред рдЗрд╕рдХреЗ рдмрд╛рдж рд╕реЗ рдХрд╛рдлреА рдкрд░реЗрд╢рд╛рдиреА рд╣реЛ рд░рд╣реА рд╣реИред рд╣рдореЗрдВ рд╢рд╛рдпрдж SpooledTemporaryFile рдХрд╛ рдЙрдкрдпреЛрдЧ рди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╡рд╛рдкрд╕ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдПред

рдореИрдВ рдзреНрдпрд╛рди рджреЗрддрд╛ рд╣реВрдВ рдХрд┐ bpo-26175 рдХрд╛ рдкреБрд▓ рдЕрдиреБрд░реЛрдз рдЕрднреА рдкреНрд░рдЧрддрд┐ рдкрд░ рд╣реИ, рдХрдо рд╕реЗ рдХрдо, рджреЗрдЦреЗрдВ python/cpython#3249

@mjpieters рдХреЗ рд╕рд╛рде рдЪреИрдЯ рдХрд░рдиреЗ рд╕реЗ рдПрдХ рдЕрд▓рдЧ рд╡рд┐рдЪрд╛рд░ рдЖрдпрд╛ред рдпрджрд┐ attr stream рдкрд░ рдореМрдЬреВрдж рдирд╣реАрдВ рд╣реИ, рд▓реЗрдХрд┐рди рдЗрд╕рдореЗрдВ _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 рдореЗрдВ рдЕрдкрдЧреНрд░реЗрдб рдХрд░реЗрдВ рдЬрд┐рд╕рдореЗрдВ рдиреАрдб рд╡реЗрд░реНрдХрдЬрд╝реЗрдЧ>= 0.14

рдореИрдВ рдЕрдЬрдЧрд░ 3.6.7 (рдЬреЛ ubuntu 18.04 рдХреЗ рд╕рд╛рде рдЖрддрд╛ рд╣реИ) рдХреЗ рд╕рд╛рде Werkzeug (0.14.1) рдХреЗ рдЕрдВрддрд┐рдо рд╕рдВрд╕реНрдХрд░рдг рдХреЗ рд╕рд╛рде рдпрд╣реА рддреНрд░реБрдЯрд┐ рд╣реИ:

рд╡рд┐рд╢реЗрд╖рддрд╛ рддреНрд░реБрдЯрд┐: 'SpooledTemporaryFile' рдСрдмреНрдЬреЗрдХреНрдЯ рдореЗрдВ рдХреЛрдИ рд╡рд┐рд╢реЗрд╖рддрд╛ рдирд╣реАрдВ рд╣реИ 'рдкрдардиреАрдп'

рдХреНрдпрд╛ рдЖрдк рдлрд┐рдХреНрд╕ рдХреЛ 0.14 рд╕рдВрд╕реНрдХрд░рдг рдореЗрдВ рдкреЛрд░реНрдЯ рдХрд░рдиреЗ рдХреА рдпреЛрдЬрдирд╛ рдмрдирд╛ рд░рд╣реЗ рд╣реИрдВ?

рдзрдиреНрдпрд╡рд╛рджред

рдЕрдЧрд▓рд╛ рд╕рдВрд╕реНрдХрд░рдг 0.15 рд╣реЛрдЧрд╛ред рд░рд┐рд▓реАрдЬ рдХреА рдШреЛрд╖рдгрд╛рдУрдВ рдХреЗ рд▓рд┐рдП рдХреГрдкрдпрд╛ рдЗрд╕ рд░реЗрдкреЛ, рд╣рдорд╛рд░реЗ рдмреНрд▓реЙрдЧ рдпрд╛ https://twitter.com/PalletsTeam рдХрд╛ рдЕрдиреБрд╕рд░рдг рдХрд░реЗрдВред

рдХреНрдпрд╛ рдпрд╣ рдкреГрд╖реНрда рдЙрдкрдпреЛрдЧреА рдерд╛?
0 / 5 - 0 рд░реЗрдЯрд┐рдВрдЧреНрд╕