New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying the MIME type of an uploaded file #87

Merged
merged 1 commit into from Sep 17, 2013
Jump to file or symbol
Failed to load files and symbols.
+33 −9
Diff settings

Always

Just for now

View
@@ -13,6 +13,9 @@ News
- Remove old (and broken?) casperjs/selenium backward compat imports. Fix
https://github.com/gawel/webtest-selenium/issues/9
- Allow optionally specifying the MIME type of an uploaded form file. Fixes #86
[Marius Gedminas]
2.0.7 (2013-08-07)
------------------
View
@@ -172,6 +172,7 @@ You can deal with file upload by using the Upload class:
>>> from webtest import Upload
>>> form['file'] = Upload('README.rst')
>>> form['file'] = Upload('README.rst', b'data')
>>> form['file'] = Upload('README.rst', b'data', 'text/x-rst')
Submit a form
--------------
View
@@ -44,6 +44,16 @@ def test_encode_multipart(self):
[(six.b('key'), six.b('value'))], [])
self.assertIn(to_bytes('name="key"'), data[-1])
def test_encode_multipart_content_type(self):
data = self.app.encode_multipart(
[], [('file', 'data.txt', six.b('data'), 'text/x-custom-mime-type')])
self.assertIn(to_bytes('Content-Type: text/x-custom-mime-type'), data[-1])
data = self.app.encode_multipart(
[('file', webtest.Upload('data.txt', six.b('data'),
'text/x-custom-mime-type'))], [])
self.assertIn(to_bytes('Content-Type: text/x-custom-mime-type'), data[-1])
def test_get_params(self):
resp = self.app.get('/', 'a=b')
resp.mustcontain('a=b')
View
@@ -343,7 +343,7 @@ def head(self, url, headers=None, extra_environ=None,
def encode_multipart(self, params, files):
"""
Encodes a set of parameters (typically a name/value list) and
a set of files (a list of (name, filename, file_body)) into a
a set of files (a list of (name, filename, file_body, mimetype)) into a
typical POST body, returning the (content_type, body).
"""
@@ -352,20 +352,19 @@ def encode_multipart(self, params, files):
lines = []
def _append_file(file_info):
key, filename, value = self._get_file_info(file_info)
key, filename, value, fcontent = self._get_file_info(file_info)
if isinstance(key, text_type):
try:
key = key.encode('ascii')
except: # pragma: no cover
raise # file name must be ascii
if isinstance(filename, text_type):
fcontent = mimetypes.guess_type(filename)[0]
try:
filename = filename.encode('utf8')
except: # pragma: no cover
raise # file name must be ascii or utf8
else:
fcontent = mimetypes.guess_type(filename.decode('ascii'))[0]
if not fcontent:
fcontent = mimetypes.guess_type(filename.decode('utf8'))[0]
fcontent = to_bytes(fcontent)
fcontent = fcontent or b'application/octet-stream'
lines.extend([
@@ -387,6 +386,8 @@ def _append_file(file_info):
file_info = [key, value.filename]
if value.content is not None:
file_info.append(value.content)
if value.content_type is not None:
file_info.append(value.content_type)
_append_file(file_info)
else:
if isinstance(value, text_type):
@@ -627,16 +628,20 @@ def _get_file_info(self, file_info):
f = open(filename, 'rb')
content = f.read()
f.close()
return (file_info[0], filename, content)
elif len(file_info) == 3:
return (file_info[0], filename, content, None)
elif 3 <= len(file_info) <= 4:
content = file_info[2]
if not isinstance(content, binary_type):
raise ValueError('File content must be %s not %s'
% (binary_type, type(content)))
return file_info
if len(file_info) == 3:
return tuple(file_info) + (None,)
else:
return file_info
else:
raise ValueError(
"upload_files need to be a list of tuples of (fieldname, "
"filename, filecontent, mimetype) or (fieldname, "
"filename, filecontent) or (fieldname, filename); "
"you gave: %r"
% repr(file_info)[:100])
View
@@ -16,24 +16,29 @@ class Upload(object):
"""
A file to upload::
>>> Upload('filename.txt', 'data', 'application/octet-stream')
<Upload "filename.txt">
>>> Upload('filename.txt', 'data')
<Upload "filename.txt">
>>> Upload("README.txt")
<Upload "README.txt">
:param filename: Name of the file to upload.
:param content: Contents of the file.
:param content_type: MIME type of the file.
"""
def __init__(self, filename, content=None):
def __init__(self, filename, content=None, content_type=None):
self.filename = filename
self.content = content
self.content_type = content_type
def __iter__(self):
yield self.filename
if self.content:
yield self.content
# XXX: do we need to yield self.content_type here?

This comment has been minimized.

@mgedmin

mgedmin Sep 18, 2013

Contributor

Unfortunately the answer is "Yes".

TestApp.encode_multipart has this bit:

        if isinstance(value, forms.File):
            if value.value:
                _append_file([key] + list(value.value))

Turns out the value attribute of forms.File is an instance of forms.Upload, so the content type gets lost here without that extra yield. :-(

I wish I'd discovered this before asking for the 2.0.8 release to PyPI.

@mgedmin

mgedmin Sep 18, 2013

Contributor

Unfortunately the answer is "Yes".

TestApp.encode_multipart has this bit:

        if isinstance(value, forms.File):
            if value.value:
                _append_file([key] + list(value.value))

Turns out the value attribute of forms.File is an instance of forms.Upload, so the content type gets lost here without that extra yield. :-(

I wish I'd discovered this before asking for the 2.0.8 release to PyPI.

# TODO: do we handle the case when we need to get
# contents ourselves?
ProTip! Use n and p to navigate between commits in a pull request.