3

Does anyone have an example on how to do a multipart post in Python 3.4 without using a 3rd party library like requests?

I am having issues porting my old Python 2 code to Python 3.4.

Here is the python 2 encoding code:

def _encode_multipart_formdata(self, fields, files):
    boundary = mimetools.choose_boundary()
    buf = StringIO()
    for (key, value) in fields.iteritems():
        buf.write('--%s\r\n' % boundary)
        buf.write('Content-Disposition: form-data; name="%s"' % key)
        buf.write('\r\n\r\n' + self._tostr(value) + '\r\n')
    for (key, filepath, filename) in files:
        if os.path.isfile(filepath):
            buf.write('--%s\r\n' % boundary)
            buf.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename))
            buf.write('Content-Type: %s\r\n' % (self._get_content_type3(filename)))
            file = open(filepath, "rb")
            try:
                buf.write('\r\n' + file.read() + '\r\n')
            finally:
                file.close()
    buf.write('--' + boundary + '--\r\n\r\n')
    buf = buf.getvalue()
    content_type = 'multipart/form-data; boundary=%s' % boundary
    return content_type, buf

The I figured out I can replace the mimetools.choose_boundary() with the following:

import email.generator
print (email.generator._make_boundary())

For the _get_content_type3() method, I am doing the following:

def _get_content_type(self, filename):
        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

When I change the StringIO to BytesIO in using Python3.4, the data never seems to be put into the POST method.

Any suggestions?

code base 5000
  • 3,812
  • 13
  • 44
  • 73
  • Hint: See how requests does it and then do that. – kylieCatt Jul 08 '15 at 15:48
  • @IanAuld are you serious? no help at all? – code base 5000 Jul 13 '15 at 11:23
  • related: [Python standard library to POST multipart/form-data encoded data](http://stackoverflow.com/q/1270518/4279). See also, [`urllib3.filepost`](https://github.com/shazow/urllib3/blob/21a288be4487040c6e21e27cec025b74d2a83152/urllib3/filepost.py#L58-L93). Both provide solutions that work on Python 2 and 3 – jfs Jul 13 '15 at 14:26

1 Answers1

4

Yes, email.generator._make_boundary() would work:

import email.generator
import io
import shutil

def _encode_multipart_formdata(self, fields, files):
    boundary = email.generator._make_boundary()
    buf = io.BytesIO()
    textwriter = io.TextIOWrapper(
        buf, 'utf8', newline='', write_through=True)

    for (key, value) in fields.items():
        textwriter.write(
            '--{boundary}\r\n'
            'Content-Disposition: form-data; name="{key}"\r\n\r\n'
            '{value}\r\n'.format(
                boundary=boundary, key=key, value=value))

    for (key, filepath, filename) in files:
        if os.path.isfile(filepath):
            textwriter.write(
                '--{boundary}\r\n'
                'Content-Disposition: form-data; name="{key}"; '
                'filename="{filename}"\r\n'
                'Content-Type: {content_type}\r\n\r\n'.format(
                    boundary=boundary, key=key, filename=filename,
                    content_type=self._get_content_type3(filename)))
            with open(filepath, "rb") as f:
                shutil.copyfileobj(f, buf)
            textwriter.write('\r\n')

    textwriter.write('--{}--\r\n\r\n'.format(boundary))
    content_type = 'multipart/form-data; boundary={}'.format(boundary)
    return content_type, buf.getvalue()

This uses a io.TextIOWrapper() object to make header formatting and encoding easier (bytes objects don't support formatting operations; you'll have to wait for Python 3.5 which adds % support).

If you insist on using the email package for the whole job, take into account that you'll need twice the memory; once to hold the email.mime objects, and again to hold the written result:

from email.mime import multipart, nonmultipart, text
from email.generator import BytesGenerator
from email import policy
from io import BytesIO

def _encode_multipart_formdata(self, fields, files):
    msg = multipart.MIMEMultipart('form-data')

    for (key, value) in fields.items():
        part = text.MIMEText(value)
        part['Content-Disposition'] = 'form-data; name="{}"'.format(key)
        msg.attach(part)

    for (key, filepath, filename) in files:
        if os.path.isfile(filepath):
            ct = self._get_content_type3(filename)
            part = nonmultipart.MIMENonMultipart(*ct.split('/'))
            part['Content-Disposition'] = (
                'form-data; name="{}"; filename="{}"'.format(
                    key, filename))
            with open(filepath, "rb") as f:
                part.set_payload(f.read())
            msg.attach(part)

    body = BytesIO()
    generator = BytesGenerator(
        body, mangle_from_=False, policy=policy.HTTP)
    generator.flatten(msg)
    return msg['content-type'], body.getvalue().partition(b'\r\n\r\n')[-1]

The result is otherwise basically the same, with the addition of some MIME-Version and Content-Transfer-Encoding headers.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • There are `poster` (pure Python, standalone, no Python 3 support?), `urllib3.filepost` (may depend on other parts), distutils' upload_file` (specialized to upload files to pypi), `MultipartPostHandler` (Python 2), patches for ["multipart/form-data encoding" Python issue](http://bugs.python.org/issue3244). – jfs Jul 13 '15 at 19:20
  • @J.F.Sebastian: but the OP asked for a no-3rd-party library solution, so now we have another one. – Martijn Pieters Jul 13 '15 at 19:21
  • I'm just surprised that there is no `multipart_encode()` equivalent somewhere in `email.mime` package in stdlib. – jfs Jul 13 '15 at 19:26
  • @J.F.Sebastian: there is, but it is [rather buried](https://hg.python.org/cpython/file/1cae77f873af/Lib/email/generator.py#l252); you need to build a tree of `Message` objects, and the resulting generated message contains a complete email structure, not just the encoded multipart body. You then have to remove the extra headers again. – Martijn Pieters Jul 14 '15 at 08:04
  • @MartijnPieters - thank you, I'm going to test this now! – code base 5000 Jul 14 '15 at 17:31
  • @J.F.Sebastian - Could you post a sample using the email library? I'd like to see it done that way out of curiosity. – code base 5000 Jul 16 '15 at 10:22
  • @josh1234: you might have meant to address it to Martijn Pieters♦ – jfs Jul 16 '15 at 11:02
  • @MartijnPieters Can you post a sample using the email modules to do the multipart post as well? I'd be interested in seeing that if you have the time. The other solution worked well too. Thank you for your help! – code base 5000 Jul 16 '15 at 12:09
  • @MartijnPieters - Thank you for all this information/help. +50 rep for you! – code base 5000 Jul 17 '15 at 11:03
  • @MartijnPieters - is there a way to make 2/3 encoder? Thanks – code base 5000 Jan 20 '16 at 15:55
  • @josh1234: I'm sure one could be written based on the code in this answer. I just don't have the time right now to do so. – Martijn Pieters Jan 20 '16 at 15:59
  • @MartijnPieters - no worries, I figured I would ask. Thank you! – code base 5000 Jan 21 '16 at 14:32