You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2020/03/10 16:11:54 UTC

[allura] 02/14: [#8354] change StringIO uses that really are BytesIO

This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8354
in repository https://gitbox.apache.org/repos/asf/allura.git

commit dbebcd535eedca0f9279e6da694fac6eff319953
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue Mar 10 12:11:12 2020 -0400

    [#8354] change StringIO uses that really are BytesIO
---
 Allura/allura/app.py                               | 10 ++++----
 Allura/allura/controllers/static.py                |  5 ++--
 Allura/allura/lib/helpers.py                       |  5 ++--
 Allura/allura/lib/plugin.py                        |  4 +--
 Allura/allura/model/filesystem.py                  |  4 +--
 Allura/allura/tests/functional/test_admin.py       | 10 ++++----
 .../allura/tests/functional/test_neighborhood.py   |  6 ++---
 Allura/allura/tests/model/test_discussion.py       | 26 +++++++++----------
 Allura/allura/tests/model/test_filesystem.py       | 21 ++++++++-------
 ForgeBlog/forgeblog/tests/test_app.py              |  4 +--
 .../forgediscussion/tests/functional/test_forum.py |  6 ++---
 .../tests/functional/test_forum_admin.py           |  1 -
 ForgeDiscussion/forgediscussion/tests/test_app.py  |  4 +--
 ForgeImporters/forgeimporters/base.py              | 12 ++++-----
 ForgeImporters/forgeimporters/github/tracker.py    |  8 ++----
 .../forgeimporters/tests/github/test_extractor.py  | 30 ++++++++++------------
 .../forgeimporters/tests/github/test_tracker.py    |  2 +-
 ForgeSVN/forgesvn/model/svn.py                     |  4 +--
 ForgeSVN/forgesvn/tests/model/test_repository.py   |  9 +++----
 ForgeTracker/forgetracker/import_support.py        |  4 +--
 .../forgetracker/tests/functional/test_root.py     |  6 ++---
 ForgeTracker/forgetracker/tests/test_app.py        |  4 +--
 ForgeWiki/forgewiki/tests/functional/test_root.py  |  6 ++---
 ForgeWiki/forgewiki/tests/test_app.py              |  4 +--
 24 files changed, 93 insertions(+), 102 deletions(-)

diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 17e4943..056c776 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -20,7 +20,7 @@ from __future__ import absolute_import
 import os
 import logging
 from urllib import basejoin
-from cStringIO import StringIO
+from io import BytesIO
 from collections import defaultdict
 from xml.etree import ElementTree as ET
 from copy import copy
@@ -50,7 +50,7 @@ from allura.lib.utils import permanent_redirect, ConfigProxy
 from allura import model as M
 from allura.tasks import index_tasks
 import six
-from io import open
+from io import open, BytesIO
 from six.moves import map
 
 log = logging.getLogger(__name__)
@@ -710,7 +710,7 @@ class Application(object):
         if message.get('filename'):
             # Special case - the actual post may not have been created yet
             log.info('Saving attachment %s', message['filename'])
-            fp = StringIO(message['payload'])
+            fp = BytesIO(six.ensure_binary(message['payload']))
             self.AttachmentClass.save_attachment(
                 message['filename'], fp,
                 content_type=message.get(
@@ -728,9 +728,9 @@ class Application(object):
                 message_id)
 
             try:
-                fp = StringIO(message['payload'].encode('utf-8'))
+                fp = BytesIO(message['payload'].encode('utf-8'))
             except UnicodeDecodeError:
-                fp = StringIO(message['payload'])
+                fp = BytesIO(message['payload'])
 
             post.attach(
                 'alternate', fp,
diff --git a/Allura/allura/controllers/static.py b/Allura/allura/controllers/static.py
index fd5404b..720ef77 100644
--- a/Allura/allura/controllers/static.py
+++ b/Allura/allura/controllers/static.py
@@ -17,8 +17,9 @@
 
 from __future__ import unicode_literals
 from __future__ import absolute_import
-from cStringIO import StringIO
+from io import BytesIO
 
+import six
 from tg import expose
 from tg.decorators import without_trailing_slash
 from webob import exc
@@ -55,4 +56,4 @@ class NewForgeController(object):
         """
         css, md5 = g.tool_icon_css
         return utils.serve_file(
-            StringIO(css), 'tool_icon_css', 'text/css', etag=md5)
+            BytesIO(six.ensure_binary(css)), 'tool_icon_css', 'text/css', etag=md5)
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 13c14a8..d2506e7 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -23,6 +23,7 @@ import sys
 import os
 import os.path
 import difflib
+
 import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
 import re
 import unicodedata
@@ -37,7 +38,7 @@ from collections import defaultdict
 import shlex
 import socket
 from functools import partial
-from cStringIO import StringIO
+from io import BytesIO
 import cgi
 
 import emoji
@@ -1211,7 +1212,7 @@ def rate_limit(cfg_opt, artifact_count, start_date, exception=None):
 
 def base64uri(content_or_image, image_format='PNG', mimetype='image/png', windows_line_endings=False):
     if hasattr(content_or_image, 'save'):
-        output = StringIO()
+        output = BytesIO()
         content_or_image.save(output, format=image_format)
         content = output.getvalue()
     else:
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 32b6dae..a1b2629 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -29,7 +29,7 @@ import crypt
 import random
 from six.moves.urllib.request import urlopen
 from six.moves.urllib.parse import urlparse
-from cStringIO import StringIO
+from io import BytesIO
 from random import randint
 from hashlib import sha256
 from base64 import b64encode
@@ -1094,7 +1094,7 @@ class ProjectRegistrationProvider(object):
                     troves.append(
                         M.TroveCategory.query.get(trove_cat_id=trove_id)._id)
         if 'icon' in project_template:
-            icon_file = StringIO(urlopen(project_template['icon']['url']).read())
+            icon_file = BytesIO(urlopen(project_template['icon']['url']).read())
             p.save_icon(project_template['icon']['filename'], icon_file)
 
         if user_project:
diff --git a/Allura/allura/model/filesystem.py b/Allura/allura/model/filesystem.py
index 7bd726c..8392a92 100644
--- a/Allura/allura/model/filesystem.py
+++ b/Allura/allura/model/filesystem.py
@@ -19,7 +19,7 @@ from __future__ import unicode_literals
 from __future__ import absolute_import
 import os
 import re
-from cStringIO import StringIO
+from io import BytesIO
 import logging
 
 import PIL
@@ -103,7 +103,7 @@ class File(MappedClass):
 
     @classmethod
     def from_data(cls, filename, data, **kw):
-        return cls.from_stream(filename, StringIO(data), **kw)
+        return cls.from_stream(filename, BytesIO(data), **kw)
 
     def delete(self):
         self._fs().delete(self.file_id)
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index be3a353..cb3a182 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -21,7 +21,7 @@ from __future__ import absolute_import
 import os
 import allura
 import pkg_resources
-import StringIO
+from io import BytesIO
 import logging
 from io import open
 
@@ -384,12 +384,12 @@ class TestProjectAdmin(TestController):
                 short_description='A Test Project'),
                 upload_files=[upload])
         r = self.app.get('/p/test/icon')
-        image = PIL.Image.open(StringIO.StringIO(r.body))
+        image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (48, 48)
 
         r = self.app.get('/p/test/icon?foo=bar')
         r = self.app.get('/p/test/icon?w=96')
-        image = PIL.Image.open(StringIO.StringIO(r.body))
+        image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (96, 96)
         r = self.app.get('/p/test/icon?w=12345', status=404)
 
@@ -411,10 +411,10 @@ class TestProjectAdmin(TestController):
         filename = project.get_screenshots()[0].filename
         r = self.app.get('/p/test/screenshot/' + filename)
         uploaded = PIL.Image.open(file_path)
-        screenshot = PIL.Image.open(StringIO.StringIO(r.body))
+        screenshot = PIL.Image.open(BytesIO(r.body))
         assert uploaded.size == screenshot.size
         r = self.app.get('/p/test/screenshot/' + filename + '/thumb')
-        thumb = PIL.Image.open(StringIO.StringIO(r.body))
+        thumb = PIL.Image.open(BytesIO(r.body))
         assert thumb.size == (150, 150)
         # FIX: home pages don't currently support screenshots (now that they're a wiki);
         # reinstate this code (or appropriate) when we have a macro for that
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index 7ebf528..1f7034c 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -20,7 +20,7 @@ from __future__ import unicode_literals
 from __future__ import absolute_import
 import json
 import os
-from cStringIO import StringIO
+from io import BytesIO
 import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse
 from io import open
 
@@ -276,7 +276,7 @@ class TestNeighborhood(TestController):
                                       homepage='# MozQ1'),
                           extra_environ=dict(username=str('root')), upload_files=[upload])
         r = self.app.get('/adobe/icon')
-        image = PIL.Image.open(StringIO(r.body))
+        image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (48, 48)
 
         r = self.app.get('/adobe/icon?foo=bar')
@@ -894,7 +894,7 @@ class TestNeighborhood(TestController):
                          foo_id, extra_environ=dict(username=str('root')))
         r = self.app.get('/adobe/_admin/awards/%s/icon' %
                          foo_id, extra_environ=dict(username=str('root')))
-        image = PIL.Image.open(StringIO(r.body))
+        image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (48, 48)
         self.app.post('/adobe/_admin/awards/grant',
                       params=dict(grant='FOO', recipient='adobe-1',
diff --git a/Allura/allura/tests/model/test_discussion.py b/Allura/allura/tests/model/test_discussion.py
index b4e5f22..3f9906f 100644
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -22,7 +22,7 @@ Model tests for artifact
 """
 from __future__ import unicode_literals
 from __future__ import absolute_import
-from cStringIO import StringIO
+from io import BytesIO
 import time
 from datetime import datetime, timedelta
 from cgi import FieldStorage
@@ -178,14 +178,14 @@ def test_attachment_methods():
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     p = t.post('This is a post')
-    p_att = p.attach('foo.text', StringIO('Hello, world!'),
+    p_att = p.attach('foo.text', BytesIO(b'Hello, world!'),
                      discussion_id=d._id,
                      thread_id=t._id,
                      post_id=p._id)
-    t_att = p.attach('foo2.text', StringIO('Hello, thread!'),
+    t_att = p.attach('foo2.text', BytesIO(b'Hello, thread!'),
                      discussion_id=d._id,
                      thread_id=t._id)
-    d_att = p.attach('foo3.text', StringIO('Hello, discussion!'),
+    d_att = p.attach('foo3.text', BytesIO(b'Hello, discussion!'),
                      discussion_id=d._id)
 
     ThreadLocalORMSession.flush_all()
@@ -202,7 +202,7 @@ def test_attachment_methods():
     fs.name = 'file_info'
     fs.filename = 'fake.txt'
     fs.type = 'text/plain'
-    fs.file = StringIO('this is the content of the fake file\n')
+    fs.file = BytesIO(b'this is the content of the fake file\n')
     p = t.post(text='test message', forum=None, subject='', file_info=fs)
     ThreadLocalORMSession.flush_all()
     n = M.Notification.query.get(
@@ -220,12 +220,12 @@ def test_multiple_attachments():
     test_file1.name = 'file_info'
     test_file1.filename = 'test1.txt'
     test_file1.type = 'text/plain'
-    test_file1.file = StringIO('test file1\n')
+    test_file1.file = BytesIO(b'test file1\n')
     test_file2 = FieldStorage()
     test_file2.name = 'file_info'
     test_file2.filename = 'test2.txt'
     test_file2.type = 'text/plain'
-    test_file2.file = StringIO('test file2\n')
+    test_file2.file = BytesIO(b'test file2\n')
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     test_post = t.post('test post')
@@ -243,7 +243,7 @@ def test_add_attachment():
     test_file.name = 'file_info'
     test_file.filename = 'test.txt'
     test_file.type = 'text/plain'
-    test_file.file = StringIO('test file\n')
+    test_file.file = BytesIO(b'test file\n')
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     test_post = t.post('test post')
@@ -262,12 +262,12 @@ def test_notification_two_attaches():
     fs1.name = 'file_info'
     fs1.filename = 'fake.txt'
     fs1.type = 'text/plain'
-    fs1.file = StringIO('this is the content of the fake file\n')
+    fs1.file = BytesIO(b'this is the content of the fake file\n')
     fs2 = FieldStorage()
     fs2.name = 'file_info'
     fs2.filename = 'fake2.txt'
     fs2.type = 'text/plain'
-    fs2.file = StringIO('this is the content of the fake file\n')
+    fs2.file = BytesIO(b'this is the content of the fake file\n')
     p = t.post(text='test message', forum=None, subject='', file_info=[fs1, fs2])
     ThreadLocalORMSession.flush_all()
     n = M.Notification.query.get(
@@ -285,7 +285,7 @@ def test_discussion_delete():
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     p = t.post('This is a post')
-    p.attach('foo.text', StringIO(''),
+    p.attach('foo.text', BytesIO(b''),
              discussion_id=d._id,
              thread_id=t._id,
              post_id=p._id)
@@ -302,7 +302,7 @@ def test_thread_delete():
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     p = t.post('This is a post')
-    p.attach('foo.text', StringIO(''),
+    p.attach('foo.text', BytesIO(b''),
              discussion_id=d._id,
              thread_id=t._id,
              post_id=p._id)
@@ -315,7 +315,7 @@ def test_post_delete():
     d = M.Discussion(shortname='test', name='test')
     t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
     p = t.post('This is a post')
-    p.attach('foo.text', StringIO(''),
+    p.attach('foo.text', BytesIO(b''),
              discussion_id=d._id,
              thread_id=t._id,
              post_id=p._id)
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index d757a4f..3ff5727 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -21,7 +21,6 @@ from __future__ import unicode_literals
 from __future__ import absolute_import
 import os
 from unittest import TestCase
-from cStringIO import StringIO
 from io import BytesIO
 
 from tg import tmpl_context as c
@@ -55,7 +54,7 @@ class TestFile(TestCase):
         self.db.fs.chunks.remove()
 
     def test_from_stream(self):
-        f = File.from_stream('test1.txt', StringIO('test1'))
+        f = File.from_stream('test1.txt', BytesIO(b'test1'))
         self.session.flush()
         assert self.db.fs.count() == 1
         assert self.db.fs.files.count() == 1
@@ -65,7 +64,7 @@ class TestFile(TestCase):
         self._assert_content(f, 'test1')
 
     def test_from_data(self):
-        f = File.from_data('test2.txt', 'test2')
+        f = File.from_data('test2.txt', b'test2')
         self.session.flush(f)
         assert self.db.fs.count() == 1
         assert self.db.fs.files.count() == 1
@@ -86,7 +85,7 @@ class TestFile(TestCase):
         assert text.startswith('# -*-')
 
     def test_delete(self):
-        f = File.from_data('test1.txt', 'test1')
+        f = File.from_data('test1.txt', b'test1')
         self.session.flush()
         assert self.db.fs.count() == 1
         assert self.db.fs.files.count() == 1
@@ -98,8 +97,8 @@ class TestFile(TestCase):
         assert self.db.fs.chunks.count() == 0
 
     def test_remove(self):
-        File.from_data('test1.txt', 'test1')
-        File.from_data('test2.txt', 'test2')
+        File.from_data('test1.txt', b'test1')
+        File.from_data('test2.txt', b'test2')
         self.session.flush()
         assert self.db.fs.count() == 2
         assert self.db.fs.files.count() == 2
@@ -111,7 +110,7 @@ class TestFile(TestCase):
         assert self.db.fs.chunks.count() == 1
 
     def test_overwrite(self):
-        f = File.from_data('test1.txt', 'test1')
+        f = File.from_data('test1.txt', b'test1')
         self.session.flush()
         assert self.db.fs.count() == 1
         assert self.db.fs.files.count() == 1
@@ -126,7 +125,7 @@ class TestFile(TestCase):
         self._assert_content(f, 'test2')
 
     def test_serve_embed(self):
-        f = File.from_data('te s\u0b6e1.txt', 'test1')
+        f = File.from_data('te s\u0b6e1.txt', b'test1')
         self.session.flush()
         with patch('allura.lib.utils.tg.request', Request.blank('/')), \
                 patch('allura.lib.utils.tg.response', Response()) as response, \
@@ -139,7 +138,7 @@ class TestFile(TestCase):
             assert 'Content-Disposition' not in response.headers
 
     def test_serve_embed_false(self):
-        f = File.from_data('te s\u0b6e1.txt', 'test1')
+        f = File.from_data('te s\u0b6e1.txt', b'test1')
         self.session.flush()
         with patch('allura.lib.utils.tg.request', Request.blank('/')), \
                 patch('allura.lib.utils.tg.response', Response()) as response, \
@@ -175,7 +174,7 @@ class TestFile(TestCase):
     def test_not_image(self):
         f, t = File.save_image(
             'file.txt',
-            StringIO('blah'),
+            BytesIO(b'blah'),
             thumbnail_size=(16, 16),
             square=True,
             save_original=True)
@@ -185,7 +184,7 @@ class TestFile(TestCase):
     def test_invalid_image(self):
         f, t = File.save_image(
             'bogus.png',
-            StringIO('bogus data here!'),
+            BytesIO(b'bogus data here!'),
             thumbnail_size=(16, 16),
             square=True,
             save_original=True)
diff --git a/ForgeBlog/forgeblog/tests/test_app.py b/ForgeBlog/forgeblog/tests/test_app.py
index 9f32613..26089b9 100644
--- a/ForgeBlog/forgeblog/tests/test_app.py
+++ b/ForgeBlog/forgeblog/tests/test_app.py
@@ -23,10 +23,10 @@ import tempfile
 import json
 import os
 from cgi import FieldStorage
+from io import BytesIO
 
 from nose.tools import assert_equal
 from tg import tmpl_context as c
-from cStringIO import StringIO
 from ming.orm import ThreadLocalORMSession
 
 from allura import model as M
@@ -112,7 +112,7 @@ class TestBulkExport(object):
             test_file1 = FieldStorage()
             test_file1.name = 'file_info'
             test_file1.filename = 'test_file'
-            test_file1.file = StringIO('test file1\n')
+            test_file1.file = BytesIO(b'test file1\n')
             p = post.discussion_thread.add_post(text='test comment')
             p.add_multiple_attachments(test_file1)
             ThreadLocalORMSession.flush_all()
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
index 8ad92f6..f4b59d6 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
@@ -212,13 +212,13 @@ class TestForumMessageHandling(TestController):
         assert_equal(FM.ForumPost.query.find().count(), 3)
 
     def test_attach(self):
-        self._post('testforum', 'Attachment Thread', 'This is a text file',
+        self._post('testforum', 'Attachment Thread', 'This is text attachment',
                    message_id='test.attach.100@domain.net',
                    filename='test.txt',
                    content_type='text/plain')
-        self._post('testforum', 'Test Thread', 'Nothing here',
+        self._post('testforum', 'Test Thread', b'Nothing here',
                    message_id='test.attach.100@domain.net')
-        self._post('testforum', 'Attachment Thread', 'This is a text file',
+        self._post('testforum', 'Attachment Thread', 'This is binary ¶¬¡™£¢¢•º™™¶'.encode('utf-8'),
                    message_id='test.attach.100@domain.net',
                    content_type='text/plain')
 
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
index 41682ec..127e2d2 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
@@ -19,7 +19,6 @@ from __future__ import unicode_literals
 from __future__ import absolute_import
 import os
 import allura
-from StringIO import StringIO
 import logging
 
 import PIL
diff --git a/ForgeDiscussion/forgediscussion/tests/test_app.py b/ForgeDiscussion/forgediscussion/tests/test_app.py
index 4e842c3..30f9d90 100644
--- a/ForgeDiscussion/forgediscussion/tests/test_app.py
+++ b/ForgeDiscussion/forgediscussion/tests/test_app.py
@@ -26,10 +26,10 @@ import json
 import os
 from operator import attrgetter
 from cgi import FieldStorage
+from io import BytesIO
 
 from nose.tools import assert_equal
 from tg import tmpl_context as c
-from cStringIO import StringIO
 
 from forgediscussion.site_stats import posts_24hr
 from ming.orm import ThreadLocalORMSession
@@ -96,7 +96,7 @@ class TestBulkExport(TestDiscussionApiBase):
         test_file1 = FieldStorage()
         test_file1.name = 'file_info'
         test_file1.filename = 'test_file'
-        test_file1.file = StringIO('test file1\n')
+        test_file1.file = BytesIO(b'test file1\n')
         post.add_attachment(test_file1)
         ThreadLocalORMSession.flush_all()
 
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index f4e78c9..9897910 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -20,6 +20,8 @@ from __future__ import absolute_import
 import os
 import errno
 import logging
+from io import BytesIO
+
 import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
 import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse
 from collections import defaultdict
@@ -29,10 +31,6 @@ from datetime import datetime
 import codecs
 from six.moves import filter
 import six
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
 
 from bs4 import BeautifulSoup
 from tg import expose, validate, flash, redirect, config
@@ -616,17 +614,17 @@ class ImportAdminExtension(AdminExtension):
         sidebar_links.append(link)
 
 
-def stringio_parser(page):
+def bytesio_parser(page):
     return {
         'content-type': page.info()['content-type'],
-        'data': StringIO(page.read()),
+        'data': BytesIO(page.read()),
     }
 
 
 class File(object):
 
     def __init__(self, url, filename=None):
-        extractor = ProjectExtractor(None, url, parser=stringio_parser)
+        extractor = ProjectExtractor(None, url, parser=bytesio_parser)
         self.url = url
         self.filename = filename or os.path.basename(urlparse(url).path)
         # try to get the mime-type from the filename first, because
diff --git a/ForgeImporters/forgeimporters/github/tracker.py b/ForgeImporters/forgeimporters/github/tracker.py
index 7db14e5..b796d3c 100644
--- a/ForgeImporters/forgeimporters/github/tracker.py
+++ b/ForgeImporters/forgeimporters/github/tracker.py
@@ -22,11 +22,7 @@ import logging
 from datetime import datetime
 from six.moves.urllib.error import HTTPError
 import six
-
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+from io import BytesIO
 
 from formencode import validators as fev
 from tg import (
@@ -289,7 +285,7 @@ class Attachment(object):
     def get_file(self, extractor):
         try:
             fp_ish = extractor.urlopen(self.url)
-            fp = StringIO(fp_ish.read())
+            fp = BytesIO(fp_ish.read())
             return fp
         except HTTPError as e:
             if e.code == 404:
diff --git a/ForgeImporters/forgeimporters/tests/github/test_extractor.py b/ForgeImporters/forgeimporters/tests/github/test_extractor.py
index 617e56b..2f31577 100644
--- a/ForgeImporters/forgeimporters/tests/github/test_extractor.py
+++ b/ForgeImporters/forgeimporters/tests/github/test_extractor.py
@@ -19,15 +19,13 @@ from __future__ import unicode_literals
 from __future__ import absolute_import
 import json
 from unittest import TestCase
+from io import BytesIO
 import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse
 
 from mock import patch, Mock
 
 from ... import github
 
-# Can't use cStringIO here, because we cannot set attributes or subclass it,
-# and this is needed in mocked_urlopen below
-from StringIO import StringIO
 from six.moves import zip
 
 
@@ -65,24 +63,24 @@ class TestGitHubProjectExtractor(TestCase):
     def mocked_urlopen(self, url):
         headers = {}
         if url.endswith('/test_project'):
-            response = StringIO(json.dumps(self.PROJECT_INFO))
+            response = BytesIO(json.dumps(self.PROJECT_INFO))
         elif url.endswith('/issues?state=closed'):
-            response = StringIO(json.dumps(self.CLOSED_ISSUES_LIST))
+            response = BytesIO(json.dumps(self.CLOSED_ISSUES_LIST))
         elif url.endswith('/issues?state=open'):
-            response = StringIO(json.dumps(self.OPENED_ISSUES_LIST))
+            response = BytesIO(json.dumps(self.OPENED_ISSUES_LIST))
             headers = {'Link': '</issues?state=open&page=2>; rel="next"'}
         elif url.endswith('/issues?state=open&page=2'):
-            response = StringIO(json.dumps(self.OPENED_ISSUES_LIST_PAGE2))
+            response = BytesIO(json.dumps(self.OPENED_ISSUES_LIST_PAGE2))
         elif url.endswith('/comments'):
-            response = StringIO(json.dumps(self.ISSUE_COMMENTS))
+            response = BytesIO(json.dumps(self.ISSUE_COMMENTS))
             headers = {'Link': '</comments?page=2>; rel="next"'}
         elif url.endswith('/comments?page=2'):
-            response = StringIO(json.dumps(self.ISSUE_COMMENTS_PAGE2))
+            response = BytesIO(json.dumps(self.ISSUE_COMMENTS_PAGE2))
         elif url.endswith('/events'):
-            response = StringIO(json.dumps(self.ISSUE_EVENTS))
+            response = BytesIO(json.dumps(self.ISSUE_EVENTS))
             headers = {'Link': '</events?page=2>; rel="next"'}
         elif url.endswith('/events?page=2'):
-            response = StringIO(json.dumps(self.ISSUE_EVENTS_PAGE2))
+            response = BytesIO(json.dumps(self.ISSUE_EVENTS_PAGE2))
 
         response.info = lambda: headers
         return response
@@ -166,9 +164,9 @@ class TestGitHubProjectExtractor(TestCase):
             'X-RateLimit-Remaining': '0',
             'X-RateLimit-Reset': '1382693522',
         }
-        response_limit_exceeded = StringIO('{}')
+        response_limit_exceeded = BytesIO(b'{}')
         response_limit_exceeded.info = lambda: limit_exceeded_headers
-        response_ok = StringIO('{}')
+        response_ok = BytesIO(b'{}')
         response_ok.info = lambda: {}
         urlopen.side_effect = [response_limit_exceeded, response_ok]
         e = github.GitHubProjectExtractor('test_project')
@@ -182,7 +180,7 @@ class TestGitHubProjectExtractor(TestCase):
         sleep.reset_mock()
         urlopen.reset_mock()
         log.warn.reset_mock()
-        response_ok = StringIO('{}')
+        response_ok = BytesIO(b'{}')
         response_ok.info = lambda: {}
         urlopen.side_effect = [response_ok]
         e.get_page('fake 2')
@@ -202,11 +200,11 @@ class TestGitHubProjectExtractor(TestCase):
         }
 
         def urlopen_side_effect(*a, **kw):
-            mock_resp = StringIO('{}')
+            mock_resp = BytesIO(b'{}')
             mock_resp.info = lambda: {}
             urlopen.side_effect = [mock_resp]
             raise six.moves.urllib.error.HTTPError(
-                'url', 403, 'msg', limit_exceeded_headers, StringIO('{}'))
+                'url', 403, 'msg', limit_exceeded_headers, BytesIO(b'{}'))
         urlopen.side_effect = urlopen_side_effect
         e = github.GitHubProjectExtractor('test_project')
         e.get_page('fake')
diff --git a/ForgeImporters/forgeimporters/tests/github/test_tracker.py b/ForgeImporters/forgeimporters/tests/github/test_tracker.py
index 83da436..3c975a4 100644
--- a/ForgeImporters/forgeimporters/tests/github/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/github/test_tracker.py
@@ -136,7 +136,7 @@ class TestTrackerImporter(TestCase):
     def test_get_attachments(self):
         importer = tracker.GitHubTrackerImporter()
         extractor = mock.Mock()
-        extractor.urlopen().read.return_value = 'data'
+        extractor.urlopen().read.return_value = b'data'
         body = 'hello\n' \
             '![cdbpzjc5ex4](https://f.cloud.github.com/assets/979771/1027411/a393ab5e-0e70-11e3-8a38-b93a3df904cf.jpg)\r\n' \
             '![screensh0t](http://f.cl.ly/items/13453x43053r2G0d3x0v/Screen%20Shot%202012-04-28%20at%2010.48.17%20AM.png)'
diff --git a/ForgeSVN/forgesvn/model/svn.py b/ForgeSVN/forgesvn/model/svn.py
index 7370104..b66e1fc 100644
--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -27,7 +27,7 @@ import time
 import operator as op
 from subprocess import Popen, PIPE
 from hashlib import sha1
-from cStringIO import StringIO
+from io import BytesIO
 from datetime import datetime
 import tempfile
 from shutil import rmtree
@@ -578,7 +578,7 @@ class SVNImplementation(M.RepositoryImplementation):
         data = self._svn.cat(
             self._url + blob.path(),
             revision=self._revision(blob.commit._id))
-        return StringIO(data)
+        return BytesIO(data)
 
     def blob_size(self, blob):
         try:
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 07b14a5..7ef9de7 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -26,8 +26,9 @@ import pkg_resources
 from itertools import count, product
 from datetime import datetime
 from zipfile import ZipFile
-
+from io import BytesIO
 from collections import defaultdict
+
 from tg import tmpl_context as c, app_globals as g
 import mock
 from nose.tools import assert_equal, assert_in
@@ -921,8 +922,7 @@ class TestCommit(_TestWithRepo):
             return counter.i
         counter.i = 0
         blobs = defaultdict(counter)
-        from cStringIO import StringIO
-        return lambda blob: StringIO(str(blobs[blob.path()]))
+        return lambda blob: BytesIO(str(blobs[blob.path()]))
 
     def test_diffs_file_renames(self):
         def open_blob(blob):
@@ -935,8 +935,7 @@ class TestCommit(_TestWithRepo):
                 # moved from /b/b and modified
                 '/b/a/z': 'Death Star will destroy you\nALL',
             }
-            from cStringIO import StringIO
-            return StringIO(blobs.get(blob.path(), ''))
+            return BytesIO(blobs.get(blob.path(), ''))
         self.repo._impl.open_blob = open_blob
 
         self.repo._impl.commit = mock.Mock(return_value=self.ci)
diff --git a/ForgeTracker/forgetracker/import_support.py b/ForgeTracker/forgetracker/import_support.py
index 0fb4103..efe011f 100644
--- a/ForgeTracker/forgetracker/import_support.py
+++ b/ForgeTracker/forgetracker/import_support.py
@@ -21,7 +21,7 @@ from __future__ import absolute_import
 import logging
 import json
 from datetime import datetime
-from cStringIO import StringIO
+from io import BytesIO
 
 # Non-stdlib imports
 from tg import tmpl_context as c
@@ -67,7 +67,7 @@ class ResettableStream(object):
     def _read_header(self):
         if self.buf is None:
             data = self.fp.read(self.buf_size)
-            self.buf = StringIO(data)
+            self.buf = BytesIO(data)
             self.buf_len = len(data)
             self.stream_pos = self.buf_len
 
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index 3ff8512..a5d9fe8 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -23,7 +23,7 @@ import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
 import os
 import time
 import json
-import StringIO
+from io import BytesIO
 import allura
 import mock
 from io import open
@@ -979,11 +979,11 @@ class TestFunctionalController(TrackerTestController):
 
         uploaded = PIL.Image.open(file_path)
         r = self.app.get('/bugs/1/attachment/' + filename)
-        downloaded = PIL.Image.open(StringIO.StringIO(r.body))
+        downloaded = PIL.Image.open(BytesIO(r.body))
         assert uploaded.size == downloaded.size
         r = self.app.get('/bugs/1/attachment/' + filename + '/thumb')
 
-        thumbnail = PIL.Image.open(StringIO.StringIO(r.body))
+        thumbnail = PIL.Image.open(BytesIO(r.body))
         assert thumbnail.size == (100, 100)
 
     def test_sidebar_static_page(self):
diff --git a/ForgeTracker/forgetracker/tests/test_app.py b/ForgeTracker/forgetracker/tests/test_app.py
index f1cfd2f..5053990 100644
--- a/ForgeTracker/forgetracker/tests/test_app.py
+++ b/ForgeTracker/forgetracker/tests/test_app.py
@@ -21,11 +21,11 @@ import tempfile
 import json
 import operator
 import os
+from io import BytesIO
 
 from nose.tools import assert_equal, assert_true
 from tg import tmpl_context as c
 from cgi import FieldStorage
-from cStringIO import StringIO
 
 from alluratest.controller import setup_basic_test
 from ming.orm import ThreadLocalORMSession
@@ -102,7 +102,7 @@ class TestBulkExport(TrackerTestController):
         test_file1 = FieldStorage()
         test_file1.name = 'file_info'
         test_file1.filename = 'test_file'
-        test_file1.file = StringIO('test file1\n')
+        test_file1.file = BytesIO(b'test file1\n')
         self.post.add_attachment(test_file1)
         ThreadLocalORMSession.flush_all()
 
diff --git a/ForgeWiki/forgewiki/tests/functional/test_root.py b/ForgeWiki/forgewiki/tests/functional/test_root.py
index b2a6022..77e87ef 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_root.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_root.py
@@ -21,7 +21,7 @@ from __future__ import unicode_literals
 from __future__ import print_function
 from __future__ import absolute_import
 import os
-import StringIO
+from io import BytesIO
 import allura
 import json
 from io import open
@@ -550,11 +550,11 @@ class TestRootController(TestController):
 
         uploaded = PIL.Image.open(file_path)
         r = self.app.get('/wiki/TEST/attachment/' + filename)
-        downloaded = PIL.Image.open(StringIO.StringIO(r.body))
+        downloaded = PIL.Image.open(BytesIO(r.body))
         assert uploaded.size == downloaded.size
         r = self.app.get('/wiki/TEST/attachment/' + filename + '/thumb')
 
-        thumbnail = PIL.Image.open(StringIO.StringIO(r.body))
+        thumbnail = PIL.Image.open(BytesIO(r.body))
         assert thumbnail.size == (100, 100)
 
         # Make sure thumbnail is absent
diff --git a/ForgeWiki/forgewiki/tests/test_app.py b/ForgeWiki/forgewiki/tests/test_app.py
index bd58982..d7f80e8 100644
--- a/ForgeWiki/forgewiki/tests/test_app.py
+++ b/ForgeWiki/forgewiki/tests/test_app.py
@@ -22,8 +22,8 @@ import tempfile
 import json
 import operator
 import os
+from io import BytesIO
 
-from cStringIO import StringIO
 from nose.tools import assert_equal
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
@@ -96,7 +96,7 @@ class TestBulkExport(object):
         self.page.text = 'test_text'
         self.page.mod_date = datetime.datetime(2013, 7, 5)
         self.page.labels = ['test_label1', 'test_label2']
-        self.page.attach('some/path/test_file', StringIO('test string'))
+        self.page.attach('some/path/test_file', BytesIO(b'test string'))
         ThreadLocalORMSession.flush_all()
 
     def test_bulk_export_with_attachmetns(self):