← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:librarianserver-future-imports into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:librarianserver-future-imports into launchpad:master.

Commit message:
Convert lp.services.librarianserver to preferred __future__ imports

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/394720
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:librarianserver-future-imports into launchpad:master.
diff --git a/lib/lp/services/librarianserver/apachelogparser.py b/lib/lp/services/librarianserver/apachelogparser.py
index 478bdab..8cce64f 100644
--- a/lib/lp/services/librarianserver/apachelogparser.py
+++ b/lib/lp/services/librarianserver/apachelogparser.py
@@ -1,6 +1,8 @@
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 import re
 
 
@@ -8,17 +10,17 @@ DBUSER = 'librarianlogparser'
 
 
 # Regexp used to match paths to LibraryFileAliases.
-lfa_path_re = re.compile('^/[0-9]+/')
-multi_slashes_re = re.compile('/+')
+lfa_path_re = re.compile(br'^/[0-9]+/')
+multi_slashes_re = re.compile(br'/+')
 
 
 def get_library_file_id(path):
-    path = multi_slashes_re.sub('/', path)
+    path = multi_slashes_re.sub(b'/', path)
     if not lfa_path_re.match(path):
         # We only count downloads of LibraryFileAliases, and this is
         # not one of them.
         return None
 
-    file_id = path.split('/')[1]
+    file_id = path.split(b'/')[1]
     assert file_id.isdigit(), ('File ID is not a digit: %s' % path)
-    return file_id
+    return file_id.decode('UTF-8')
diff --git a/lib/lp/services/librarianserver/db.py b/lib/lp/services/librarianserver/db.py
index dc59db5..41119df 100644
--- a/lib/lp/services/librarianserver/db.py
+++ b/lib/lp/services/librarianserver/db.py
@@ -3,6 +3,8 @@
 
 """Database access layer for the Librarian."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'Library',
diff --git a/lib/lp/services/librarianserver/doc/librarian-report.txt b/lib/lp/services/librarianserver/doc/librarian-report.txt
index 3270ea6..1c11b0f 100644
--- a/lib/lp/services/librarianserver/doc/librarian-report.txt
+++ b/lib/lp/services/librarianserver/doc/librarian-report.txt
@@ -5,10 +5,10 @@ storage.
     >>> script = 'scripts/librarian-report.py'
 
     >>> rv, out, err = run_script(script)
-    >>> print rv
+    >>> print(rv)
     0
-    >>> print err
-    >>> print '\n' + out
+    >>> print(err)
+    >>> print('\n' + out)
     <BLANKLINE>
     ...
     4866 bytes    hwsubmission in 2 files
@@ -19,13 +19,11 @@ We can filter on date to produce deltas.
 
     >>> rv, out, err = run_script(
     ...     script, ['--from=2005/01/01', '--until=2005/12/31'])
-    >>> print rv
+    >>> print(rv)
     0
-    >>> print err
-    >>> print '\n' + out
+    >>> print(err)
+    >>> print('\n' + out)
     <BLANKLINE>
     ...
     0 bytes      hwsubmission in 0 files
     ...
-
-
diff --git a/lib/lp/services/librarianserver/doc/upload.txt b/lib/lp/services/librarianserver/doc/upload.txt
index 562ea5f..ca8c26a 100644
--- a/lib/lp/services/librarianserver/doc/upload.txt
+++ b/lib/lp/services/librarianserver/doc/upload.txt
@@ -11,7 +11,7 @@ Database check
 The Database-Name header is now mandatory.  If it isn't present, an otherwise
 well-formed request will be rejected:
 
-    >>> upload_request("""STORE 14 hello.txt
+    >>> upload_request(b"""STORE 14 hello.txt
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -23,7 +23,7 @@ well-formed request will be rejected:
 If Database-Name is specified by the client, and doesn't match the database name
 of the server, the upload is rejected.
 
-    >>> upload_request("""STORE 14 hello.txt
+    >>> upload_request(b"""STORE 14 hello.txt
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -35,7 +35,7 @@ of the server, the upload is rejected.
 
 If the database name matches, it's accepted as usual.
 
-    >>> upload_request("""STORE 14 hello.txt
+    >>> upload_request(b"""STORE 14 hello.txt
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -43,7 +43,7 @@ If the database name matches, it's accepted as usual.
     ...
     ... Cats and dogs.""")
     reply: '200'
-    file u'hello.txt' stored as text/plain, contents: 'Cats and dogs.'
+    file 'hello.txt' stored as text/plain, contents: 'Cats and dogs.'
 
 
 Error conditions
@@ -52,39 +52,39 @@ Error conditions
 Errors receive a 400 status code in the reply, and the connection will be
 closed.
 
+Invalid UTF-8 lines are rejected.
+
+    >>> upload_request(b"STORE 10000 \xff\n")
+    reply: '400 Non-data lines must be in UTF-8'
+    connection closed
+
 Unknown commands are rejected.
 
-    >>> upload_request("FROB the chicken\n")
+    >>> upload_request(b"FROB the chicken\n")
     reply: '400 Unknown command: FROB the chicken'
     connection closed
 
 Incomplete STORE commands are rejected.
 
-    >>> upload_request("STORE bad-arg!\n")
+    >>> upload_request(b"STORE bad-arg!\n")
     reply: '400 STORE command expects a size and file name'
     connection closed
 
 Invalid headers are rejected.
 
-    >>> upload_request("""STORE 10000 foo.txt
+    >>> upload_request(b"""STORE 10000 foo.txt
     ... Some garbage.
     ... """)
     reply: '400 Invalid header: Some garbage.'
     connection closed
 
-Invalid UTF-8 filenames are rejected.
-
-    >>> upload_request("STORE 10000 \xff\n")
-    reply: '400 STORE command expects the filename to be in UTF-8'
-    connection closed
-
 
 Uploading corner cases
 ----------------------
 
 Empty files work, rather than hang the connection.
 
-    >>> upload_request("""STORE 0 foo.txt
+    >>> upload_request(b"""STORE 0 foo.txt
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -92,11 +92,11 @@ Empty files work, rather than hang the connection.
     ...
     ... """)
     reply: '200'
-    file u'foo.txt' stored as text/plain, contents: ''
+    file 'foo.txt' stored as text/plain, contents: ''
 
 Filename with spaces work.
 
-    >>> upload_request("""STORE 14 cats and dogs.txt
+    >>> upload_request(b"""STORE 14 cats and dogs.txt
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -104,12 +104,12 @@ Filename with spaces work.
     ...
     ... Cats and dogs.""")
     reply: '200'
-    file u'cats and dogs.txt' stored as text/plain, contents: 'Cats and dogs.'
+    file 'cats and dogs.txt' stored as text/plain, contents: 'Cats and dogs.'
 
 Unicode filenames work, but must be encoded as UTF-8 on the socket.
 
-    >>> filename = u'Yow\N{INTERROBANG}'.encode('utf-8')
-    >>> upload_request("""STORE 14 %s
+    >>> filename = 'Yow\N{INTERROBANG}'.encode('utf-8')
+    >>> upload_request(b"""STORE 14 %s
     ... Content-Type: text/plain
     ... File-Content-ID: 123
     ... File-Alias-ID: 456
@@ -117,6 +117,4 @@ Unicode filenames work, but must be encoded as UTF-8 on the socket.
     ...
     ... Cats and dogs.""" % filename)
     reply: '200'
-    file u'Yow\u203d' stored as text/plain, contents: 'Cats and dogs.'
-
-
+    file 'Yow‽' stored as text/plain, contents: 'Cats and dogs.'
diff --git a/lib/lp/services/librarianserver/librariangc.py b/lib/lp/services/librarianserver/librariangc.py
index 3072629..af866ff 100644
--- a/lib/lp/services/librarianserver/librariangc.py
+++ b/lib/lp/services/librarianserver/librariangc.py
@@ -3,6 +3,8 @@
 
 """Librarian garbage collection routines"""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from datetime import (
diff --git a/lib/lp/services/librarianserver/libraryprotocol.py b/lib/lp/services/librarianserver/libraryprotocol.py
index 385d15e..6a8d61d 100644
--- a/lib/lp/services/librarianserver/libraryprotocol.py
+++ b/lib/lp/services/librarianserver/libraryprotocol.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from datetime import datetime
@@ -67,6 +69,10 @@ class FileUploadProtocol(basic.LineReceiver):
 
     def lineReceived(self, line):
         try:
+            try:
+                line = line.decode('UTF-8')
+            except UnicodeDecodeError:
+                raise ProtocolViolation('Non-data lines must be in UTF-8')
             getattr(self, 'line_' + self.state, self.badLine)(line)
         except ProtocolViolation as e:
             self.sendError(e.msg)
@@ -156,11 +162,6 @@ class FileUploadProtocol(basic.LineReceiver):
     def command_STORE(self, args):
         try:
             size, name = args.split(None, 1)
-            try:
-                name = name.decode('utf-8')
-            except:
-                raise ProtocolViolation(
-                    "STORE command expects the filename to be in UTF-8")
             size = int(size)
         except ValueError:
             raise ProtocolViolation(
diff --git a/lib/lp/services/librarianserver/storage.py b/lib/lp/services/librarianserver/storage.py
index 253bc41..89af9d6 100644
--- a/lib/lp/services/librarianserver/storage.py
+++ b/lib/lp/services/librarianserver/storage.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import errno
diff --git a/lib/lp/services/librarianserver/swift.py b/lib/lp/services/librarianserver/swift.py
index 4407ffd..b45293f 100644
--- a/lib/lp/services/librarianserver/swift.py
+++ b/lib/lp/services/librarianserver/swift.py
@@ -3,6 +3,8 @@
 
 """Move files from Librarian disk storage into Swift."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'SWIFT_CONTAINER_PREFIX',
diff --git a/lib/lp/services/librarianserver/testing/fake.py b/lib/lp/services/librarianserver/testing/fake.py
index c482abe..46ca6f1 100644
--- a/lib/lp/services/librarianserver/testing/fake.py
+++ b/lib/lp/services/librarianserver/testing/fake.py
@@ -10,6 +10,8 @@ provides a simple and fast alternative to the full Librarian in unit
 tests.
 """
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'FakeLibrarian',
diff --git a/lib/lp/services/librarianserver/testing/server.py b/lib/lp/services/librarianserver/testing/server.py
index ee11175..0ac1a68 100644
--- a/lib/lp/services/librarianserver/testing/server.py
+++ b/lib/lp/services/librarianserver/testing/server.py
@@ -3,6 +3,8 @@
 
 """Fixture for the librarians."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'fillLibrarianFile',
diff --git a/lib/lp/services/librarianserver/testing/tests/test_fakelibrarian.py b/lib/lp/services/librarianserver/testing/tests/test_fakelibrarian.py
index ac849c5..d4cc436 100644
--- a/lib/lp/services/librarianserver/testing/tests/test_fakelibrarian.py
+++ b/lib/lp/services/librarianserver/testing/tests/test_fakelibrarian.py
@@ -3,6 +3,8 @@
 
 """Test the fake librarian."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import io
diff --git a/lib/lp/services/librarianserver/tests/test_apachelogparser.py b/lib/lp/services/librarianserver/tests/test_apachelogparser.py
index 1be4c14..c81cd3f 100644
--- a/lib/lp/services/librarianserver/tests/test_apachelogparser.py
+++ b/lib/lp/services/librarianserver/tests/test_apachelogparser.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 from datetime import datetime
 import io
 import os
@@ -35,52 +37,52 @@ class TestRequestParsing(TestCase):
     def assertMethodAndFileIDAreCorrect(self, request):
         method, path = get_method_and_path(request)
         file_id = get_library_file_id(path)
-        self.assertEqual(method, 'GET')
+        self.assertEqual(method, b'GET')
         self.assertEqual(file_id, '8196569')
 
     def test_return_value(self):
-        request = 'GET /8196569/mediumubuntulogo.png HTTP/1.1'
+        request = b'GET /8196569/mediumubuntulogo.png HTTP/1.1'
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_return_value_for_http_path(self):
-        request = ('GET http://launchpadlibrarian.net/8196569/'
-                   'mediumubuntulogo.png HTTP/1.1')
+        request = (b'GET http://launchpadlibrarian.net/8196569/'
+                   b'mediumubuntulogo.png HTTP/1.1')
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_extra_slashes_are_ignored(self):
-        request = 'GET http://launchpadlibrarian.net//8196569//foo HTTP/1.1'
+        request = b'GET http://launchpadlibrarian.net//8196569//foo HTTP/1.1'
         self.assertMethodAndFileIDAreCorrect(request)
 
-        request = 'GET //8196569//foo HTTP/1.1'
+        request = b'GET //8196569//foo HTTP/1.1'
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_multiple_consecutive_white_spaces(self):
         # Some request strings might have multiple consecutive white spaces,
         # but they're parsed just like if they didn't have the extra spaces.
-        request = 'GET /8196569/mediumubuntulogo.png  HTTP/1.1'
+        request = b'GET /8196569/mediumubuntulogo.png  HTTP/1.1'
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_return_value_for_https_path(self):
-        request = ('GET https://launchpadlibrarian.net/8196569/'
-                   'mediumubuntulogo.png HTTP/1.1')
+        request = (b'GET https://launchpadlibrarian.net/8196569/'
+                   b'mediumubuntulogo.png HTTP/1.1')
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_return_value_for_request_missing_http_version(self):
         # HTTP 1.0 requests might omit the HTTP version so we must cope with
         # them.
-        request = 'GET https://launchpadlibrarian.net/8196569/foo.png'
+        request = b'GET https://launchpadlibrarian.net/8196569/foo.png'
         self.assertMethodAndFileIDAreCorrect(request)
 
     def test_requests_for_paths_that_are_not_of_an_lfa_return_none(self):
-        request = 'GET https://launchpadlibrarian.net/ HTTP/1.1'
+        request = b'GET https://launchpadlibrarian.net/ HTTP/1.1'
         self.assertEqual(
             get_library_file_id(get_method_and_path(request)[1]), None)
 
-        request = 'GET /robots.txt HTTP/1.1'
+        request = b'GET /robots.txt HTTP/1.1'
         self.assertEqual(
             get_library_file_id(get_method_and_path(request)[1]), None)
 
-        request = 'GET /@@person HTTP/1.1'
+        request = b'GET /@@person HTTP/1.1'
         self.assertEqual(
             get_library_file_id(get_method_and_path(request)[1]), None)
 
diff --git a/lib/lp/services/librarianserver/tests/test_db.py b/lib/lp/services/librarianserver/tests/test_db.py
index 3dd2a5b..be7327e 100644
--- a/lib/lp/services/librarianserver/tests/test_db.py
+++ b/lib/lp/services/librarianserver/tests/test_db.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from fixtures import MockPatchObject
@@ -208,8 +210,8 @@ class TestLibrarianStuff(TestCase):
         library = db.Library(restricted=False)
         aliases = library.getAliases(1)
         expected_aliases = [
-            (1, u'netapplet-1.0.0.tar.gz', u'application/x-gtar'),
-            (2, u'netapplet_1.0.0.orig.tar.gz', u'application/x-gtar'),
+            (1, 'netapplet-1.0.0.tar.gz', 'application/x-gtar'),
+            (2, 'netapplet_1.0.0.orig.tar.gz', 'application/x-gtar'),
             ]
         self.assertEqual(expected_aliases, aliases)
 
@@ -221,7 +223,7 @@ class TestLibrarianStuff(TestCase):
         alias.content = None
         aliases = library.getAliases(1)
         expected_aliases = [
-            (2, u'netapplet_1.0.0.orig.tar.gz', u'application/x-gtar'),
+            (2, 'netapplet_1.0.0.orig.tar.gz', 'application/x-gtar'),
             ]
         self.assertEqual(expected_aliases, aliases)
 
@@ -235,13 +237,13 @@ class TestLibrarianStuff(TestCase):
 
         aliases = unrestricted_library.getAliases(1)
         expected_aliases = [
-            (2, u'netapplet_1.0.0.orig.tar.gz', u'application/x-gtar'),
+            (2, 'netapplet_1.0.0.orig.tar.gz', 'application/x-gtar'),
             ]
         self.assertEqual(expected_aliases, aliases)
 
         restricted_library = db.Library(restricted=True)
         aliases = restricted_library.getAliases(1)
         expected_aliases = [
-            (1, u'netapplet-1.0.0.tar.gz', u'application/x-gtar'),
+            (1, 'netapplet-1.0.0.tar.gz', 'application/x-gtar'),
             ]
         self.assertEqual(expected_aliases, aliases)
diff --git a/lib/lp/services/librarianserver/tests/test_db_outage.py b/lib/lp/services/librarianserver/tests/test_db_outage.py
index d6fa401..8642ace 100644
--- a/lib/lp/services/librarianserver/tests/test_db_outage.py
+++ b/lib/lp/services/librarianserver/tests/test_db_outage.py
@@ -5,6 +5,8 @@
 
 Database outages happen by accident and during fastdowntime deployments."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import io
diff --git a/lib/lp/services/librarianserver/tests/test_doc.py b/lib/lp/services/librarianserver/tests/test_doc.py
index ff7ea00..6a9ed9c 100644
--- a/lib/lp/services/librarianserver/tests/test_doc.py
+++ b/lib/lp/services/librarianserver/tests/test_doc.py
@@ -5,7 +5,7 @@
 Run the doctests and pagetests.
 """
 
-from __future__ import absolute_import, print_function
+from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 
@@ -25,7 +25,7 @@ from lp.testing.systemdocs import (
 class MockTransport:
     disconnecting = False
 
-    bytesWritten = ''
+    bytesWritten = b''
     connectionLost = False
 
     def write(self, bytes):
@@ -45,7 +45,7 @@ class MockLibrary:
 
 
 class MockFile:
-    bytes = ''
+    bytes = b''
     stored = False
     databaseName = None
     debugID = None
@@ -74,7 +74,7 @@ def upload_request(request):
     closed, e.g.::
 
         reply: '200'
-        file u'foo.txt' stored as text/plain, contents: 'Foo!'
+        file 'foo.txt' stored as text/plain, contents: 'Foo!'
 
     or::
 
@@ -115,18 +115,18 @@ def upload_request(request):
     server.fileLibrary = MockLibrary()
 
     # Feed in the request
-    server.dataReceived(request.replace('\n', '\r\n'))
+    server.dataReceived(request.replace(b'\n', b'\r\n'))
 
     # Report on what happened
-    print("reply: %r" % server.transport.bytesWritten.rstrip('\r\n'))
+    print("reply: %r" % server.transport.bytesWritten.rstrip(b'\r\n'))
 
     if server.transport.connectionLost:
         print('connection closed')
 
     mockFile = server.fileLibrary.file
     if mockFile is not None and mockFile.stored:
-        print("file %r stored as %s, contents: %r" %
-              (mockFile.name, mockFile.mimetype, mockFile.bytes))
+        print("file '%s' stored as %s, contents: %r" % (
+                mockFile.name, mockFile.mimetype, mockFile.bytes))
 
     # Cleanup: remove the observer.
     log.removeObserver(log_observer)
@@ -137,12 +137,12 @@ here = os.path.dirname(os.path.realpath(__file__))
 special = {
     'librarian-report.txt': LayeredDocFileSuite(
             '../doc/librarian-report.txt',
-            setUp=setUp, tearDown=tearDown,
+            setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
             layer=LaunchpadZopelessLayer
             ),
     'upload.txt': LayeredDocFileSuite(
             '../doc/upload.txt',
-            setUp=setUp, tearDown=tearDown,
+            setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
             layer=LaunchpadZopelessLayer,
             globs={'upload_request': upload_request},
             ),
diff --git a/lib/lp/services/librarianserver/tests/test_gc.py b/lib/lp/services/librarianserver/tests/test_gc.py
index 71fb0c4..0c2465d 100644
--- a/lib/lp/services/librarianserver/tests/test_gc.py
+++ b/lib/lp/services/librarianserver/tests/test_gc.py
@@ -3,6 +3,8 @@
 
 """Librarian garbage collection tests"""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import calendar
diff --git a/lib/lp/services/librarianserver/tests/test_sigdumpmem.py b/lib/lp/services/librarianserver/tests/test_sigdumpmem.py
index 65801be..8bb7443 100644
--- a/lib/lp/services/librarianserver/tests/test_sigdumpmem.py
+++ b/lib/lp/services/librarianserver/tests/test_sigdumpmem.py
@@ -3,6 +3,8 @@
 
 """Test the SIGDUMPMEM signal handler."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import os
diff --git a/lib/lp/services/librarianserver/tests/test_storage.py b/lib/lp/services/librarianserver/tests/test_storage.py
index c71a056..59b7cda 100644
--- a/lib/lp/services/librarianserver/tests/test_storage.py
+++ b/lib/lp/services/librarianserver/tests/test_storage.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 import hashlib
 import shutil
 import tempfile
diff --git a/lib/lp/services/librarianserver/tests/test_swift.py b/lib/lp/services/librarianserver/tests/test_swift.py
index 6fe7ffd..98ebc86 100644
--- a/lib/lp/services/librarianserver/tests/test_swift.py
+++ b/lib/lp/services/librarianserver/tests/test_swift.py
@@ -3,6 +3,8 @@
 
 """Librarian disk to Swift storage tests."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 import hashlib
diff --git a/lib/lp/services/librarianserver/tests/test_web.py b/lib/lp/services/librarianserver/tests/test_web.py
index c7ceeeb..5133aa5 100644
--- a/lib/lp/services/librarianserver/tests/test_web.py
+++ b/lib/lp/services/librarianserver/tests/test_web.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from datetime import datetime
@@ -159,7 +161,7 @@ class LibrarianWebTestCase(LibrarianWebTestMixin, TestCaseWithFactory):
         # displaying Ubuntu build logs in the browser.  The mimetype should be
         # "text/plain" for these files.
         client = LibrarianClient()
-        contents = u'Build log \N{SNOWMAN}...'.encode('UTF-8')
+        contents = 'Build log \N{SNOWMAN}...'.encode('UTF-8')
         build_log = BytesIO()
         with GzipFile(mode='wb', fileobj=build_log) as f:
             f.write(contents)
diff --git a/lib/lp/services/librarianserver/web.py b/lib/lp/services/librarianserver/web.py
index bb0612a..0d6278b 100644
--- a/lib/lp/services/librarianserver/web.py
+++ b/lib/lp/services/librarianserver/web.py
@@ -1,6 +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 __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from datetime import datetime
@@ -50,7 +52,7 @@ defaultResource = static.Data(b"""
         <p><small>Copyright 2004-2020 Canonical Ltd.</small></p>
         <!-- kthxbye. -->
         </body></html>
-        """, type='text/html')
+        """, type=six.ensure_str('text/html'))
 fourOhFour = resource.NoResource('No such resource')
 
 
@@ -316,7 +318,8 @@ class DigestSearchResource(resource.Resource):
         try:
             digest = request.args['digest'][0]
         except LookupError:
-            return static.Data(b'Bad search', 'text/plain').render(request)
+            return static.Data(
+                b'Bad search', six.ensure_str('text/plain')).render(request)
 
         deferred = deferToThread(self._matchingAliases, digest)
         deferred.addCallback(self._cb_matchingAliases, request)
@@ -333,8 +336,9 @@ class DigestSearchResource(resource.Resource):
 
     def _cb_matchingAliases(self, matches, request):
         text = '\n'.join([str(len(matches))] + matches)
-        response = static.Data(text.encode('utf-8'),
-                               'text/plain; charset=utf-8').render(request)
+        response = static.Data(
+            text.encode('utf-8'),
+            six.ensure_str('text/plain; charset=utf-8')).render(request)
         request.write(response)
         request.finish()
 
@@ -343,7 +347,7 @@ class DigestSearchResource(resource.Resource):
 robotsTxt = static.Data(b"""
 User-agent: *
 Disallow: /
-""", type='text/plain')
+""", type=six.ensure_str('text/plain'))
 
 
 def _eb(failure, request):