Index: Lib/http/formdata.py =================================================================== --- Lib/http/formdata.py (revision 0) +++ Lib/http/formdata.py (revision 0) @@ -0,0 +1,88 @@ +# Copyright (C) 2010 Python Software Foundation +# Author: Forest Bond + +''' +This module implements FormData, a class for generating a MIME +multipart/form-data message for submission to HTTP servers. You would use this +in an HTTP client to submit forms using Content-Type "multipart/form-data". +This content type is described in RFC2388. + +Example usage with httplib.HTTPConnection: + +message = FormData() +attachment = MIMEText('some text') +message.attach_form_data(attachment, name='foo') +attachment = MIMEApplication(bytes([0, 0, 0]), _subtype='octet-stream') +message.attach_file(attachment, name='files', filename='bar.bin') + +body, headers = message.get_request_data() + +conn = HTTPConnection('www.example.com') +response = conn.send_request('POST', '/url', body, headers) +''' + +from io import StringIO +from email.mime.multipart import MIMEMultipart +from email.generator import Generator + + +class FormData(MIMEMultipart): + '''A simple RFC2388 multipart/form-data implementation.''' + + def __init__(self, boundary=None, _subparts=None, **kwargs): + MIMEMultipart.__init__(self, _subtype='form-data', + boundary=boundary, _subparts=_subparts, **kwargs) + + def attach(self, subpart): + if 'MIME-Version' in subpart: + if subpart['MIME-Version'] != self['MIME-Version']: + raise ValueError('subpart has incompatible MIME-Version') + # Note: This isn't strictly necessary, but there is no point in + # including a MIME-Version header in each subpart. + del subpart['MIME-Version'] + MIMEMultipart.attach(self, subpart) + + def attach_form_data(self, subpart, name): + ''' + Attach a subpart, setting it's Content-Disposition header to + "form-data". + ''' + name = name.replace('"', '\\"') + subpart['Content-Disposition'] = 'form-data; name="%s"' % name + self.attach(subpart) + + def attach_file(self, subpart, name, filename): + ''' + Attach a subpart, setting it's Content-Disposition header to "file". + ''' + name = name.replace('"', '\\"') + filename = filename.replace('"', '\\"') + subpart['Content-Disposition'] = \ + 'file; name="%s"; filename="%s"' % (name, filename) + self.attach(subpart) + + def get_request_data(self, trailing_newline=True): + '''Return the encoded message body.''' + f = StringIO() + generator = Generator(f, mangle_from_=False) + generator._dispatch(self) + # HTTP needs a trailing newline. Since our return value is likely to + # be passed directly to an HTTP connection, we might as well add it + # here. + if trailing_newline: + f.write('\n') + body = f.getvalue() + headers = dict(self) + return body, headers + + def as_string(self, trailing_newline=True): + '''Return the entire formatted message as a string.''' + f = StringIO() + generator = Generator(f, mangle_from_=False) + generator.flatten(self) + # HTTP needs a trailing newline. Since our return value is likely to + # be passed directly to an HTTP connection, we might as well add it + # here. + if trailing_newline: + f.write('\n') + return f.getvalue() Index: Lib/test/test_http_formdata.py =================================================================== --- Lib/test/test_http_formdata.py (revision 0) +++ Lib/test/test_http_formdata.py (revision 0) @@ -0,0 +1,60 @@ +# Copyright (C) 2010 Python Software Foundation +# Author: Forest Bond + +from http.formdata import FormData +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication + +from unittest import TestCase +from test import support + + +class FormDataTests(TestCase): + def test(self): + msg = FormData(boundary='my-boundary') + attachment = MIMEText('some text') + msg.attach_form_data(attachment, name='foo') + attachment = MIMEApplication(bytes([0, 0, 0, 0, 0]), + _subtype='octet-stream') + msg.attach_file(attachment, name='files', filename='bar') + + expected_headers = { + 'Content-Type': 'multipart/form-data; boundary="my-boundary"', + 'MIME-Version': '1.0', + } + expected_headers_string = ( + 'Content-Type: multipart/form-data; boundary="my-boundary"\n' + 'MIME-Version: 1.0\n' + '\n' + ) + expected_body = ( + '--my-boundary\n' + 'Content-Type: text/plain; charset="us-ascii"\n' + 'Content-Transfer-Encoding: 7bit\n' + 'Content-Disposition: form-data; name="foo"\n' + '\n' + 'some text\n' + '--my-boundary\n' + 'Content-Type: application/octet-stream\n' + 'Content-Transfer-Encoding: base64\n' + 'Content-Disposition: file; name="files"; filename="bar"\n' + '\n' + 'AAAAAAA=\n' + '--my-boundary--\n' + ) + + body, headers = msg.get_request_data() + self.assertEqual(body, expected_body) + self.assertEqual(headers, expected_headers) + + msg_str = msg.as_string() + self.assertEqual(msg_str, + ''.join([expected_headers_string, expected_body])) + + +def test_main(verbose=None): + support.run_unittest(FormDataTests) + + +if __name__ == '__main__': + test_main()