← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-librarian-stringio into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-librarian-stringio into launchpad:master.

Commit message:
Port librarian tests away from (c)StringIO.StringIO

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/392178
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-librarian-stringio into launchpad:master.
diff --git a/lib/lp/services/librarian/doc/librarian.txt b/lib/lp/services/librarian/doc/librarian.txt
index 01f7ef9..6bb7a87 100644
--- a/lib/lp/services/librarian/doc/librarian.txt
+++ b/lib/lp/services/librarian/doc/librarian.txt
@@ -38,10 +38,10 @@ setUp
 High Level
 ----------
 
-    >>> from StringIO import StringIO
+    >>> import io
     >>> from lp.services.librarian.interfaces import (
     ...     ILibraryFileAliasSet)
-    >>> data = 'This is some data'
+    >>> data = b'This is some data'
 
 We can create LibraryFileAliases using the ILibraryFileAliasSet utility.
 This name is a mouthful, but is consistent with the rest of our naming.
@@ -49,13 +49,13 @@ This name is a mouthful, but is consistent with the rest of our naming.
     >>> lfas = getUtility(ILibraryFileAliasSet)
     >>> from lp.services.librarian.interfaces import NEVER_EXPIRES
     >>> alias = lfas.create(
-    ...     'text.txt', len(data), StringIO(data), 'text/plain', NEVER_EXPIRES
-    ...     )
-    >>> alias.mimetype
-    u'text/plain'
+    ...     'text.txt', len(data), io.BytesIO(data), 'text/plain',
+    ...     NEVER_EXPIRES)
+    >>> print(alias.mimetype)
+    text/plain
 
-    >>> alias.filename
-    u'text.txt'
+    >>> print(alias.filename)
+    text.txt
 
 We may wish to set an expiry timestamp on the file. The NEVER_EXPIRES
 constant means the file will never be removed from the Librarian, and
@@ -65,7 +65,7 @@ because of this should probably never be used.
     True
 
     >>> alias = lfas.create(
-    ...     'text.txt', len(data), StringIO(data), 'text/plain')
+    ...     'text.txt', len(data), io.BytesIO(data), 'text/plain')
 
 The default expiry of None means the file will expire a few days after
 it is no longer referenced in the database.
@@ -149,7 +149,7 @@ files.
     >>> transaction.commit()
 
     >>> alias.open()
-    >>> alias.read()
+    >>> six.ensure_str(alias.read())
     'This is some data'
 
     >>> alias.close()
@@ -157,13 +157,13 @@ files.
 We can also read it in chunks.
 
     >>> alias.open()
-    >>> alias.read(2)
+    >>> six.ensure_str(alias.read(2))
     'Th'
 
-    >>> alias.read(6)
+    >>> six.ensure_str(alias.read(6))
     'is is '
 
-    >>> alias.read()
+    >>> six.ensure_str(alias.read())
     'some data'
 
     >>> alias.close()
@@ -171,7 +171,7 @@ We can also read it in chunks.
 If you don't want to read the file in chunks you can neglect to call
 open() and close().
 
-    >>> alias.read()
+    >>> six.ensure_str(alias.read())
     'This is some data'
 
 Each alias also has an expiry date associated with it, the default of
@@ -197,11 +197,11 @@ access files in the Librarian.
     >>> from lp.services.librarian.interfaces.client import ILibrarianClient
     >>> client = getUtility(ILibrarianClient)
     >>> aid = client.addFile(
-    ...     'text.txt', len(data), StringIO(data), 'text/plain', NEVER_EXPIRES
-    ...     )
+    ...     'text.txt', len(data), io.BytesIO(data), 'text/plain',
+    ...     NEVER_EXPIRES)
     >>> transaction.commit()
     >>> f = client.getFileByAlias(aid)
-    >>> f.read()
+    >>> six.ensure_str(f.read())
     'This is some data'
 
     >>> url = client.getURLForAlias(aid)
@@ -248,18 +248,18 @@ rolls back. However, the records in the database will not be visible to
 the client until it begins a new transaction.
 
     >>> url = client.remoteAddFile(
-    ...     'text.txt', len(data), StringIO(data), 'text/plain')
-    >>> print url
+    ...     'text.txt', len(data), io.BytesIO(data), 'text/plain')
+    >>> print(url)
     http://.../text.txt
 
     >>> from six.moves.urllib.request import urlopen
-    >>> urlopen(url).read()
+    >>> six.ensure_str(urlopen(url).read())
     'This is some data'
 
 If we abort the transaction, it is still in there
 
     >>> transaction.abort()
-    >>> urlopen(url).read()
+    >>> six.ensure_str(urlopen(url).read())
     'This is some data'
 
 You can also set the expiry date on the file this way too:
@@ -267,7 +267,7 @@ You can also set the expiry date on the file this way too:
     >>> from datetime import date, datetime
     >>> from pytz import utc
     >>> url = client.remoteAddFile(
-    ...     'text.txt', len(data), StringIO(data), 'text/plain',
+    ...     'text.txt', len(data), io.BytesIO(data), 'text/plain',
     ...     expires=datetime(2005,9,1,12,0,0, tzinfo=utc))
     >>> transaction.abort()
 
@@ -280,7 +280,7 @@ because, except for test cases, the URL is the only thing useful
     >>> match = re.search('/(\d+)/', url)
     >>> alias_id = int(match.group(1))
     >>> alias = lfas[alias_id]
-    >>> print alias.expires.isoformat()
+    >>> print(alias.expires.isoformat())
     2005-09-01T12:00:00+00:00
 
 
@@ -305,9 +305,9 @@ librarian.
 File alias uploaded through the restricted librarian have the restricted
 attribute set.
 
-    >>> private_content = 'This is private data.'
+    >>> private_content = b'This is private data.'
     >>> private_file_id = restricted_client.addFile(
-    ...     'private.txt', len(private_content), StringIO(private_content),
+    ...     'private.txt', len(private_content), io.BytesIO(private_content),
     ...     'text/plain')
     >>> file_alias = getUtility(ILibraryFileAliasSet)[private_file_id]
     >>> file_alias.restricted
@@ -315,14 +315,14 @@ attribute set.
 
     >>> transaction.commit()
     >>> file_alias.open()
-    >>> print file_alias.read()
+    >>> print(six.ensure_str(file_alias.read()))
     This is private data.
 
     >>> file_alias.close()
 
 Restricted files are accessible with HTTP on a private domain.
 
-    >>> print file_alias.http_url
+    >>> print(file_alias.http_url)
     http://.../private.txt
 
     >>> file_alias.http_url.startswith(
@@ -336,7 +336,7 @@ provide such a token.
 
     >>> import hashlib
     >>> token_url = file_alias.getURL(include_token=True)
-    >>> print token_url
+    >>> print(token_url)
     https://i...restricted.../private.txt?token=...
 
     >>> token_url.startswith('https://i%d.restricted.' % file_alias.id)
@@ -379,7 +379,7 @@ But using the restricted librarian will work:
     <lp.services.librarian.client._File...>
 
     >>> file_url = restricted_client.getURLForAlias(private_file_id)
-    >>> print file_url
+    >>> print(file_url)
     http://.../private.txt
 
 Trying to access that file directly on the normal librarian will fail
@@ -395,15 +395,15 @@ Trying to access that file directly on the normal librarian will fail
 
 But downloading it from the restricted host, will work.
 
-    >>> print urlopen(file_url).read()
+    >>> print(six.ensure_str(urlopen(file_url).read()))
     This is private data.
 
 Trying to retrieve a non-restricted file from the restricted librarian
 also fails:
 
-    >>> public_content = 'This is public data.'
+    >>> public_content = b'This is public data.'
     >>> public_file_id = getUtility(ILibrarianClient).addFile(
-    ...     'public.txt', len(public_content), StringIO(public_content),
+    ...     'public.txt', len(public_content), io.BytesIO(public_content),
     ...     'text/plain')
     >>> file_alias = getUtility(ILibraryFileAliasSet)[public_file_id]
     >>> file_alias.restricted
@@ -426,8 +426,8 @@ file:
 
     >>> url = restricted_client.remoteAddFile(
     ...     'another-private.txt', len(private_content),
-    ...     StringIO(private_content), 'text/plain')
-    >>> print url
+    ...     io.BytesIO(private_content), 'text/plain')
+    >>> print(url)
     http://.../another-private.txt
 
     >>> url.startswith(config.librarian.restricted_download_url)
@@ -435,7 +435,7 @@ file:
 
 The file can then immediately be retrieved:
 
-    >>> print urlopen(url).read()
+    >>> print(six.ensure_str(urlopen(url).read()))
     This is private data.
 
 Another way to create a restricted file is by using the restricted
@@ -443,7 +443,7 @@ parameter to ILibraryFileAliasSet:
 
     >>> restricted_file = getUtility(ILibraryFileAliasSet).create(
     ...     'yet-another-private.txt', len(private_content),
-    ...     StringIO(private_content), 'text/plain', restricted=True)
+    ...     io.BytesIO(private_content), 'text/plain', restricted=True)
     >>> restricted_file.restricted
     True
 
@@ -454,7 +454,7 @@ So searching for the private content on the public librarian will fail:
 
     >>> transaction.commit()
     >>> search_query = "search?digest=%s" % restricted_file.content.sha1
-    >>> print urlopen(config.librarian.download_url + search_query).read()
+    >>> print(urlopen(config.librarian.download_url + search_query).read())
     0
 
 But on the restricted server, this will work:
@@ -462,7 +462,7 @@ But on the restricted server, this will work:
     >>> result = urlopen(
     ...     config.librarian.restricted_download_url + search_query).read()
     >>> result = result.splitlines()
-    >>> print result[0]
+    >>> print(result[0])
     3
 
     >>> sorted(file_path.split('/')[1] for file_path in result[1:])
@@ -475,7 +475,7 @@ Odds and Sods
 An UploadFailed will be raised if you try to create a file with no
 content
 
-    >>> client.addFile('test.txt', 0, StringIO('hello'), 'text/plain')
+    >>> client.addFile('test.txt', 0, io.BytesIO(b'hello'), 'text/plain')
     Traceback (most recent call last):
         [...]
     UploadFailed: Invalid length: 0
@@ -483,16 +483,16 @@ content
 If you really want a zero length file you can do it:
 
     >>> aid = client.addFile(
-    ...     'test.txt', 0, StringIO(''), 'text/plain', allow_zero_length=True)
+    ...     'test.txt', 0, io.BytesIO(), 'text/plain', allow_zero_length=True)
     >>> transaction.commit()
     >>> f = client.getFileByAlias(aid)
-    >>> f.read()
+    >>> six.ensure_str(f.read())
     ''
 
 An AssertionError will be raised if the number of bytes that could be
 read from the file don't match the declared size.
 
-    >>> client.addFile('test.txt', 42, StringIO(''), 'text/plain')
+    >>> client.addFile('test.txt', 42, io.BytesIO(), 'text/plain')
     Traceback (most recent call last):
         [...]
     AssertionError: size is 42, but 0 were read from the file
@@ -500,10 +500,10 @@ read from the file don't match the declared size.
 Filenames with spaces in them work.
 
     >>> aid = client.addFile(
-    ...     'hot dog', len(data), StringIO(data), 'text/plain')
+    ...     'hot dog', len(data), io.BytesIO(data), 'text/plain')
     >>> transaction.commit()
     >>> f = client.getFileByAlias(aid)
-    >>> f.read()
+    >>> six.ensure_str(f.read())
     'This is some data'
 
     >>> url = client.getURLForAlias(aid)
@@ -514,10 +514,10 @@ Unicode file names work.  Note that the filename in the resulting URL is
 encoded as UTF-8.
 
     >>> aid = client.addFile(
-    ...     u'Yow\N{INTERROBANG}', len(data), StringIO(data), 'text/plain')
+    ...     u'Yow\N{INTERROBANG}', len(data), io.BytesIO(data), 'text/plain')
     >>> transaction.commit()
     >>> f = client.getFileByAlias(aid)
-    >>> f.read()
+    >>> six.ensure_str(f.read())
     'This is some data'
 
     >>> url = client.getURLForAlias(aid)
@@ -554,7 +554,7 @@ URL.
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> req = LaunchpadTestRequest()
     >>> alias = lfas.create(
-    ...     'text2.txt', len(data), StringIO(data), 'text/plain',
+    ...     'text2.txt', len(data), io.BytesIO(data), 'text/plain',
     ...     NEVER_EXPIRES)
     >>> transaction.commit()
     >>> lfa_view = getMultiAdapter((alias, req), name='+index')
@@ -569,15 +569,15 @@ File views setup
 We need some files to test different ways of accessing them.
 
     >>> filename = 'public.txt'
-    >>> content = 'PUBLIC'
+    >>> content = b'PUBLIC'
     >>> public_file = getUtility(ILibraryFileAliasSet).create(
-    ...     filename, len(content), StringIO(content), 'text/plain',
+    ...     filename, len(content), io.BytesIO(content), 'text/plain',
     ...     NEVER_EXPIRES, restricted=False)
 
     >>> filename = 'restricted.txt'
-    >>> content = 'RESTRICTED'
+    >>> content = b'RESTRICTED'
     >>> restricted_file = getUtility(ILibraryFileAliasSet).create(
-    ...     filename, len(content), StringIO(content), 'text/plain',
+    ...     filename, len(content), io.BytesIO(content), 'text/plain',
     ...     NEVER_EXPIRES, restricted=True)
 
     # Create a new LibraryFileAlias not referencing any LibraryFileContent
@@ -595,7 +595,7 @@ Commit the just-created files.
     >>> transaction.commit()
 
     >>> deleted_file = getUtility(ILibraryFileAliasSet)[deleted_file.id]
-    >>> print deleted_file.deleted
+    >>> print(deleted_file.deleted)
     True
 
 Clear out existing tokens.
@@ -610,10 +610,10 @@ The MD5 summary for a file can be downloaded. The text file contains the
 hash and file name.
 
     >>> view = create_view(public_file, '+md5')
-    >>> print view.render()
+    >>> print(view.render())
     cd0c6092d6a6874f379fe4827ed1db8b public.txt
 
-    >>> print view.request.response.getHeader('Content-type')
+    >>> print(view.request.response.getHeader('Content-type'))
     text/plain
 
 
@@ -712,9 +712,9 @@ downloaded.
     >>> public_file.last_downloaded == today - last_downloaded_date
     True
 
-    >>> content = 'something'
+    >>> content = b'something'
     >>> brand_new_file = getUtility(ILibraryFileAliasSet).create(
-    ...     'new.txt', len(content), StringIO(content), 'text/plain',
+    ...     'new.txt', len(content), io.BytesIO(content), 'text/plain',
     ...     NEVER_EXPIRES, restricted=False)
-    >>> print brand_new_file.last_downloaded
+    >>> print(brand_new_file.last_downloaded)
     None
diff --git a/lib/lp/services/librarian/smoketest.py b/lib/lp/services/librarian/smoketest.py
index 6415209..4391e0c 100644
--- a/lib/lp/services/librarian/smoketest.py
+++ b/lib/lp/services/librarian/smoketest.py
@@ -6,8 +6,8 @@
 """Perform simple librarian operations to verify the current configuration.
 """
 
-from cStringIO import StringIO
 import datetime
+import io
 import sys
 
 import pytz
@@ -19,14 +19,14 @@ from lp.services.librarian.interfaces import ILibraryFileAliasSet
 
 
 FILE_SIZE = 1024
-FILE_DATA = 'x' * FILE_SIZE
+FILE_DATA = b'x' * FILE_SIZE
 FILE_LIFETIME = datetime.timedelta(hours=1)
 
 
 def store_file(client):
     expiry_date = datetime.datetime.now(pytz.UTC) + FILE_LIFETIME
     file_id = client.addFile(
-        'smoke-test-file', FILE_SIZE, StringIO(FILE_DATA), 'text/plain',
+        'smoke-test-file', FILE_SIZE, io.BytesIO(FILE_DATA), 'text/plain',
         expires=expiry_date)
     # To be able to retrieve the file, we must commit the current transaction.
     transaction.commit()
diff --git a/lib/lp/services/librarian/tests/test_client.py b/lib/lp/services/librarian/tests/test_client.py
index 9c671cb..d7f3cba 100644
--- a/lib/lp/services/librarian/tests/test_client.py
+++ b/lib/lp/services/librarian/tests/test_client.py
@@ -1,8 +1,8 @@
 # Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from cStringIO import StringIO
 import hashlib
+import io
 import os
 import re
 import socket
@@ -231,7 +231,7 @@ class LibrarianClientTestCase(TestCase):
         # addFile should send the Database-Name header.
         client = InstrumentedLibrarianClient()
         client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         self.assertTrue(client.sentDatabaseName,
             "Database-Name header not sent by addFile")
 
@@ -242,8 +242,8 @@ class LibrarianClientTestCase(TestCase):
         # different process, we need to explicitly tell the DatabaseLayer to
         # fully tear down and set up the database.
         DatabaseLayer.force_dirty_database()
-        client.remoteAddFile('sample.txt', 6, StringIO('sample'),
-                                   'text/plain')
+        client.remoteAddFile(
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         self.assertTrue(client.sentDatabaseName,
             "Database-Name header not sent by remoteAddFile")
 
@@ -254,7 +254,8 @@ class LibrarianClientTestCase(TestCase):
         # Force the client to mis-report its database
         client._getDatabaseName = lambda cur: 'wrong_database'
         try:
-            client.addFile('sample.txt', 6, StringIO('sample'), 'text/plain')
+            client.addFile(
+                'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         except UploadFailed as e:
             msg = e.args[0]
             self.assertTrue(
@@ -285,7 +286,8 @@ class LibrarianClientTestCase(TestCase):
         # right after, while uploading the file).
         self.assertRaisesRegex(
             UploadFailed, 'Server said early: STORE 7 sample.txt',
-            client.addFile, 'sample.txt', 7, StringIO('sample'), 'text/plain')
+            client.addFile,
+            'sample.txt', 7, io.BytesIO(b'sample'), 'text/plain')
 
     def test_addFile_uses_master(self):
         # addFile is a write operation, so it should always use the
@@ -296,7 +298,7 @@ class LibrarianClientTestCase(TestCase):
         ISlaveStore(LibraryFileAlias).close()
         with SlaveDatabasePolicy():
             alias_id = client.addFile(
-                'sample.txt', 6, StringIO('sample'), 'text/plain')
+                'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         f = client.getFileByAlias(alias_id)
         self.assertEqual(f.read(), 'sample')
@@ -308,7 +310,7 @@ class LibrarianClientTestCase(TestCase):
         # empty line following the headers.
         client = InstrumentedLibrarianClient()
         client.addFile(
-            'sample.txt', 0, StringIO(''), 'text/plain',
+            'sample.txt', 0, io.BytesIO(b''), 'text/plain',
             allow_zero_length=True)
         # addFile() calls _sendHeader() three times and _sendLine()
         # twice, but it does not check if the server responded
@@ -321,7 +323,7 @@ class LibrarianClientTestCase(TestCase):
         # header line. It does _not_ do this check when it sends the
         # empty line following the headers.
         client = InstrumentedLibrarianClient()
-        client.addFile('sample.txt', 4, StringIO('1234'), 'text/plain')
+        client.addFile('sample.txt', 4, io.BytesIO(b'1234'), 'text/plain')
         # addFile() calls _sendHeader() three times and _sendLine()
         # twice.
         self.assertEqual(5, client.check_error_calls)
@@ -329,14 +331,14 @@ class LibrarianClientTestCase(TestCase):
     def test_addFile_hashes(self):
         # addFile() sets the MD5, SHA-1 and SHA-256 hashes on the
         # LibraryFileContent record.
-        data = 'i am some data'
+        data = b'i am some data'
         md5 = hashlib.md5(data).hexdigest()
         sha1 = hashlib.sha1(data).hexdigest()
         sha256 = hashlib.sha256(data).hexdigest()
 
         client = LibrarianClient()
         lfa = LibraryFileAlias.get(
-            client.addFile('file', len(data), StringIO(data), 'text/plain'))
+            client.addFile('file', len(data), io.BytesIO(data), 'text/plain'))
 
         self.assertEqual(md5, lfa.content.md5)
         self.assertEqual(sha1, lfa.content.sha1)
@@ -351,7 +353,7 @@ class LibrarianClientTestCase(TestCase):
         # (Set up:)
         client = LibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         config.push(
             'test config',
             textwrap.dedent('''\
@@ -387,7 +389,7 @@ class LibrarianClientTestCase(TestCase):
         # (Set up:)
         client = RestrictedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         config.push(
             'test config',
             textwrap.dedent('''\
@@ -421,7 +423,7 @@ class LibrarianClientTestCase(TestCase):
         # (Set up:)
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()  # Make sure the file is in the "remote" database.
         self.assertFalse(client.called_getURLForDownload)
         # (Test:)
@@ -439,7 +441,7 @@ class LibrarianClientTestCase(TestCase):
 
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         self.assertRaises(LookupError, client.getFileByAlias, alias_id)
 
@@ -456,7 +458,7 @@ class LibrarianClientTestCase(TestCase):
             HTTPError('http://fake.url/', 500, 'Forced error', None, None), 2)
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         self.assertRaises(
             LibrarianServerError, client.getFileByAlias, alias_id, 1)
@@ -465,7 +467,7 @@ class LibrarianClientTestCase(TestCase):
             URLError('Connection refused'), 2)
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         self.assertRaises(
             LibrarianServerError, client.getFileByAlias, alias_id, 1)
@@ -482,7 +484,7 @@ class LibrarianClientTestCase(TestCase):
             HTTPError('http://fake.url/', 500, 'Forced error', None, None), 1)
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         self.assertEqual(
             client.getFileByAlias(alias_id), 'This is a fake file object', 3)
@@ -491,7 +493,7 @@ class LibrarianClientTestCase(TestCase):
             URLError('Connection refused'), 1)
         client = InstrumentedLibrarianClient()
         alias_id = client.addFile(
-            'sample.txt', 6, StringIO('sample'), 'text/plain')
+            'sample.txt', 6, io.BytesIO(b'sample'), 'text/plain')
         transaction.commit()
         self.assertEqual(
             client.getFileByAlias(alias_id), 'This is a fake file object', 3)
@@ -502,7 +504,7 @@ class LibrarianClientTestCase(TestCase):
         client = InstrumentedLibrarianClient()
         th = PropagatingThread(
             target=client.addFile,
-            args=('sample.txt', 6, StringIO('sample'), 'text/plain'))
+            args=('sample.txt', 6, io.BytesIO(b'sample'), 'text/plain'))
         th.start()
         th.join()
         self.assertEqual(5, client.check_error_calls)
diff --git a/lib/lp/services/librarian/tests/test_libraryfilealias.py b/lib/lp/services/librarian/tests/test_libraryfilealias.py
index bacd931..0189feb 100644
--- a/lib/lp/services/librarian/tests/test_libraryfilealias.py
+++ b/lib/lp/services/librarian/tests/test_libraryfilealias.py
@@ -5,7 +5,7 @@
 
 __metaclass__ = type
 
-from cStringIO import StringIO
+import io
 import unittest
 
 import transaction
@@ -25,10 +25,10 @@ class TestLibraryFileAlias(unittest.TestCase):
 
     def setUp(self):
         login(ANONYMOUS)
-        self.text_content = "This is content\non two lines."
+        self.text_content = b"This is content\non two lines."
         self.file_alias = getUtility(ILibraryFileAliasSet).create(
             'content.txt', len(self.text_content),
-            StringIO(self.text_content), 'text/plain')
+            io.BytesIO(self.text_content), 'text/plain')
         # Make it posssible to retrieve the content from the Librarian.
         transaction.commit()
 
diff --git a/lib/lp/services/librarian/tests/test_smoketest.py b/lib/lp/services/librarian/tests/test_smoketest.py
index 2134967..992cf38 100644
--- a/lib/lp/services/librarian/tests/test_smoketest.py
+++ b/lib/lp/services/librarian/tests/test_smoketest.py
@@ -7,10 +7,11 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 
-from cStringIO import StringIO
 from functools import partial
+import io
 
 from fixtures import MockPatch
+import six
 
 from lp.services.librarian.smoketest import (
     do_smoketest,
@@ -24,12 +25,12 @@ from lp.testing.layers import ZopelessDatabaseLayer
 
 def good_urlopen(url):
     """A urllib replacement for testing that returns good results."""
-    return StringIO(FILE_DATA)
+    return io.BytesIO(FILE_DATA)
 
 
 def bad_urlopen(url):
     """A urllib replacement for testing that returns bad results."""
-    return StringIO('bad data')
+    return io.BytesIO(b'bad data')
 
 
 def error_urlopen(url):
@@ -66,7 +67,7 @@ class SmokeTestTestCase(TestCaseWithFactory):
                 "lp.services.librarian.smoketest.urlopen", good_urlopen):
             self.assertEqual(
                 do_smoketest(self.fake_librarian, self.fake_librarian,
-                             output=StringIO()),
+                             output=six.StringIO()),
                 0)
 
     def test_bad_data(self):
@@ -75,7 +76,7 @@ class SmokeTestTestCase(TestCaseWithFactory):
         with MockPatch("lp.services.librarian.smoketest.urlopen", bad_urlopen):
             self.assertEqual(
                 do_smoketest(self.fake_librarian, self.fake_librarian,
-                             output=StringIO()),
+                             output=six.StringIO()),
                 1)
 
     def test_exception(self):
@@ -86,7 +87,7 @@ class SmokeTestTestCase(TestCaseWithFactory):
                 "lp.services.librarian.smoketest.urlopen", error_urlopen):
             self.assertEqual(
                 do_smoketest(self.fake_librarian, self.fake_librarian,
-                             output=StringIO()),
+                             output=six.StringIO()),
                 1)
 
     def test_explosive_errors(self):
@@ -99,4 +100,4 @@ class SmokeTestTestCase(TestCaseWithFactory):
                 self.assertRaises(
                     exception,
                     do_smoketest, self.fake_librarian, self.fake_librarian,
-                    output=StringIO())
+                    output=six.StringIO())