← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/twisted-17.9.0 into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/twisted-17.9.0 into lp:launchpad.

Commit message:
Upgrade to Twisted 17.9.0.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/twisted-17.9.0/+merge/342777

The main complication was a bit of librarianserver that was incompatible with backpressure changes introduced in Twisted 17.1.0 and caused tests to hang, but I eventually worked out what was going on there.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/twisted-17.9.0 into lp:launchpad.
=== modified file 'constraints.txt'
--- constraints.txt	2018-03-29 18:21:36 +0000
+++ constraints.txt	2018-04-06 08:39:18 +0000
@@ -226,6 +226,7 @@
 auditor==0.0.3
 auditorclient==0.0.4
 auditorfixture==0.0.7
+Automat==0.6.0
 Babel==2.5.1
 backports.lzma==0.0.3
 BeautifulSoup==3.2.1
@@ -253,11 +254,11 @@
 feedvalidator==0.0.0DEV-r1049
 fixtures==3.0.0
 FormEncode==1.2.4
-gmpy==1.17
 grokcore.component==1.6
 html5browser==0.0.9
 httmock==1.2.3
 httplib2==0.8
+hyperlink==18.0.0
 idna==2.6
 imagesize==0.7.1
 importlib==1.0.2
@@ -268,7 +269,7 @@
 jsautobuild==0.2
 keyring==0.6.2
 kombu==3.0.30
-launchpad-buildd==157
+launchpad-buildd==159
 launchpadlib==1.10.5
 lazr.authentication==0.1.1
 lazr.batchnavigator==1.2.11
@@ -285,10 +286,12 @@
 lazr.uri==1.0.3
 libnacl==1.3.6
 lpjsmin==0.5
+m2r==0.1.13
 manuel==1.7.2
 Markdown==2.3.1
 martian==0.11
 meliae==0.2.0.final.0
+mistune==0.8.3
 mock==1.0.1
 mocker==1.1.1
 netaddr==0.7.19
@@ -333,6 +336,7 @@
 service-identity==17.0.0
 setproctitle==1.1.7
 setuptools-git==1.2
+setuptools-scm==1.15.7
 simplejson==3.8.2
 SimpleTAL==4.3
 six==1.11.0
@@ -346,7 +350,7 @@
 testresources==0.2.7
 testscenarios==0.4
 timeline==0.0.3
-Twisted[conch,tls]==16.5.0
+Twisted[conch,tls]==17.9.0
 txAMQP==0.6.2
 txfixtures==0.4.2
 txlongpoll==0.2.12

=== modified file 'daemons/distributionmirror_http_server.tac'
--- daemons/distributionmirror_http_server.tac	2011-12-24 19:06:36 +0000
+++ daemons/distributionmirror_http_server.tac	2018-04-06 08:39:18 +0000
@@ -25,4 +25,4 @@
 site.displayTracebacks = False
 # XXX: Guilherme Salgado 2007-01-30: The port 11375 is what we use in the
 # URLs of our mirrors in sampledata, so we need to use the same here.
-strports.service("11375", site).setServiceParent(httpserverService)
+strports.service("tcp:11375", site).setServiceParent(httpserverService)

=== modified file 'lib/lp/bugs/externalbugtracker/tests/test_xmlrpc.py'
--- lib/lp/bugs/externalbugtracker/tests/test_xmlrpc.py	2015-07-10 07:48:06 +0000
+++ lib/lp/bugs/externalbugtracker/tests/test_xmlrpc.py	2018-04-06 08:39:18 +0000
@@ -5,9 +5,12 @@
 
 __metaclass__ = type
 
+import socket
 from urllib2 import URLError
 from xml.parsers.expat import ExpatError
 
+from fixtures import MockPatch
+
 from lp.bugs.externalbugtracker.xmlrpc import UrlLib2Transport
 from lp.bugs.tests.externalbugtracker import (
     ensure_response_parser_is_expat,
@@ -39,6 +42,10 @@
         # Python's httplib doesn't like Unicode URLs much. Ensure that
         # they don't cause it to crash, and we get a post-serialisation
         # connection error instead.
+        self.useFixture(MockPatch(
+            "socket.getaddrinfo",
+            side_effect=socket.gaierror(
+                socket.EAI_NONAME, "Name or service not known")))
         transport = UrlLib2Transport(u"http://test.invalid/";)
         self.assertRaisesWithContent(
             URLError, '<urlopen error [Errno -2] Name or service not known>',

=== modified file 'lib/lp/services/librarianserver/storage.py'
--- lib/lp/services/librarianserver/storage.py	2018-01-03 17:17:12 +0000
+++ lib/lp/services/librarianserver/storage.py	2018-04-06 08:39:18 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -45,8 +45,8 @@
         os.close(fd)
 
 
-def makedirs_fsync(name, mode=0777):
-    """makedirs_fsync(path [, mode=0777])
+def makedirs_fsync(name, mode=0o777):
+    """makedirs_fsync(path [, mode=0o777])
 
     os.makedirs, but fsyncing on the way up to ensure durability.
     """
@@ -168,27 +168,21 @@
         if size == 0:
             defer.returnValue('')
 
-        return_chunks = []
-        return_size = 0
-
-        while return_size < size:
+        if not self._chunk:
+            self._chunk = yield deferToThread(self._next_chunk)
             if not self._chunk:
-                self._chunk = yield deferToThread(self._next_chunk)
-                if not self._chunk:
-                    # If we have drained the data successfully,
-                    # the connection can be reused saving on auth
-                    # handshakes.
-                    swift.connection_pool.put(self._swift_connection)
-                    self._swift_connection = None
-                    self._chunks = None
-                    break
-            split = size - return_size
-            return_chunks.append(self._chunk[:split])
-            self._chunk = self._chunk[split:]
-            return_size += len(return_chunks[-1])
+                # If we have drained the data successfully,
+                # the connection can be reused saving on auth
+                # handshakes.
+                swift.connection_pool.put(self._swift_connection)
+                self._swift_connection = None
+                self._chunks = None
+                defer.returnValue('')
+        return_chunk = self._chunk[:size]
+        self._chunk = self._chunk[size:]
 
-        self._offset += return_size
-        defer.returnValue(''.join(return_chunks))
+        self._offset += len(return_chunk)
+        defer.returnValue(return_chunk)
 
 
 class LibraryFileUpload(object):

=== modified file 'lib/lp/services/librarianserver/tests/test_swift.py'
--- lib/lp/services/librarianserver/tests/test_swift.py	2018-02-13 17:46:48 +0000
+++ lib/lp/services/librarianserver/tests/test_swift.py	2018-04-06 08:39:18 +0000
@@ -55,7 +55,7 @@
         self.contents = [str(i) * i for i in range(1, 5)]
         self.lfa_ids = [
             self.add_file('file_{0}'.format(i), content, when=the_past)
-            for content in self.contents]
+            for i, content in enumerate(self.contents)]
         self.lfas = [
             IStore(LibraryFileAlias).get(LibraryFileAlias, lfa_id)
                 for lfa_id in self.lfa_ids]
@@ -64,6 +64,7 @@
     def tearDown(self):
         super(TestFeedSwift, self).tearDown()
         # Restart the Librarian so it picks up the feature flag change.
+        self.attachLibrarianLog(LibrarianLayer.librarian_fixture)
         LibrarianLayer.librarian_fixture.killTac()
         LibrarianLayer.librarian_fixture.setUp()
 

=== modified file 'lib/lp/services/librarianserver/web.py'
--- lib/lp/services/librarianserver/web.py	2018-01-01 17:40:03 +0000
+++ lib/lp/services/librarianserver/web.py	2018-04-06 08:39:18 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -8,9 +8,18 @@
 from urlparse import urlparse
 
 from storm.exceptions import DisconnectionError
-from twisted.internet import defer
+from twisted.internet import (
+    abstract,
+    defer,
+    reactor,
+    )
+from twisted.internet.interfaces import IPushProducer
 from twisted.internet.threads import deferToThread
 from twisted.python import log
+from twisted.python.compat import (
+    intToBytes,
+    networkString,
+    )
 from twisted.web import (
     http,
     proxy,
@@ -19,6 +28,7 @@
     static,
     util,
     )
+from zope.interface import implementer
 
 from lp.services.config import config
 from lp.services.database import (
@@ -147,12 +157,9 @@
             return fourOhFour
 
     @defer.inlineCallbacks
-    def _cb_getFileAlias(
-            self,
-            (dbcontentID, dbfilename, mimetype, date_created, size,
-                restricted),
-            filename, request
-            ):
+    def _cb_getFileAlias(self, results, filename, request):
+        (dbcontentID, dbfilename, mimetype, date_created, size,
+         restricted) = results
         # Return a 404 if the filename in the URL is incorrect. This offers
         # a crude form of access control (stuff we care about can have
         # unguessable names effectively using the filename as a secret).
@@ -168,7 +175,6 @@
             # stored as part of a file's metadata this logic will be replaced.
             encoding, mimetype = guess_librarian_encoding(filename, mimetype)
             file = File(mimetype, encoding, date_created, stream, size)
-            assert file.exists
             # Set our caching headers. Public Librarian files can be
             # cached forever, while private ones mustn't be at all.
             request.setHeader(
@@ -188,76 +194,109 @@
         return defaultResource.render(request)
 
 
-class File(static.File):
+class File(resource.Resource):
     isLeaf = True
 
-    def __init__(
-        self, contentType, encoding, modification_time, stream, size):
+    def __init__(self, contentType, encoding, modification_time, stream, size):
+        resource.Resource.__init__(self)
         # Have to convert the UTC datetime to POSIX timestamp (localtime)
         offset = datetime.utcnow() - datetime.now()
         local_modification_time = modification_time - offset
         self._modification_time = time.mktime(
             local_modification_time.timetuple())
-        static.File.__init__(self, '.')
         self.type = contentType
         self.encoding = encoding
         self.stream = stream
         self.size = size
 
-    def getModificationTime(self):
-        """Override the time on disk with the time from the database.
-
-        This is used by twisted to set the Last-Modified: header.
-        """
-        return self._modification_time
-
-    def restat(self, reraise=True):
-        return  # Noop
-
-    def getsize(self):
-        return self.size
-
-    def exists(self):
-        return self.stream is not None
-
-    def isdir(self):
-        return False
-
-    def openForReading(self):
-        return self.stream
-
-    def makeProducer(self, request, fileForReading):
-        # Unfortunately, by overriding the static.File's more
-        # complex makeProducer method we lose HTTP range support.
-        # However, this seems the only sane way of coping with the fact
-        # that sucking data in from Swift requires a Deferred and the
-        # static.*Producer implementations don't cope. This shouldn't be
-        # a problem as the Librarian sits behind Squid. If it is, I
-        # think we will need to cargo-cult three Procucer
-        # implementations in static, making the small modification to
-        # cope with self.fileObject.read maybe returning a Deferred, and
+    def _setContentHeaders(self, request):
+        request.setHeader(b'content-length', intToBytes(self.size))
+        if self.type:
+            request.setHeader(b'content-type', networkString(self.type))
+        if self.encoding:
+            request.setHeader(
+                b'content-encoding', networkString(self.encoding))
+
+    def render_GET(self, request):
+        """See `Resource`."""
+        request.setHeader(b'accept-ranges', b'none')
+
+        if request.setLastModified(self._modification_time) is http.CACHED:
+            # `setLastModified` also sets the response code for us, so if
+            # the request is cached, we close the file now that we've made
+            # sure that the request would otherwise succeed and return an
+            # empty body.
+            self.stream.close()
+            return b''
+
+        if request.method == b'HEAD':
+            # Set the content headers here, rather than making a producer.
+            self._setContentHeaders(request)
+            self.stream.close()
+            return b''
+
+        # static.File has HTTP range support, which would be nice to have.
+        # Unfortunately, static.File isn't a good match for producing data
+        # dynamically by fetching it from Swift. This shouldn't be a problem
+        # as the Librarian sits behind Squid. If it is, I think we will need
+        # to cargo-cult the byte-range support and three Producer
+        # implementations from static.File, making the small modifications
+        # to cope with self.fileObject.read maybe returning a Deferred, and
         # the static.File.makeProducer method to return the correct
         # producer.
         self._setContentHeaders(request)
         request.setResponseCode(http.OK)
-        return FileProducer(request, fileForReading)
-
-
-class FileProducer(static.NoRangeStaticProducer):
+        producer = FileProducer(request, self.stream)
+        producer.start()
+
+        return server.NOT_DONE_YET
+
+
+@implementer(IPushProducer)
+class FileProducer(object):
+
+    buffer_size = abstract.FileDescriptor.bufferSize
+
+    def __init__(self, request, stream):
+        self.request = request
+        self.stream = stream
+        self.producing = True
+
+    def start(self):
+        self.request.registerProducer(self, True)
+        self.resumeProducing()
+
+    def pauseProducing(self):
+        """See `IPushProducer`."""
+        self.producing = False
+
     @defer.inlineCallbacks
+    def _produceFromStream(self):
+        """Read data from our stream and write it to our consumer."""
+        while self.request and self.producing:
+            data = yield self.stream.read(self.buffer_size)
+            # pauseProducing or stopProducing may have been called while we
+            # were waiting.
+            if not self.producing:
+                return
+            if data:
+                self.request.write(data)
+            else:
+                self.request.unregisterProducer()
+                self.request.finish()
+                self.stopProducing()
+
     def resumeProducing(self):
-        if not self.request:
-            return
-        data = yield self.fileObject.read(self.bufferSize)
-        # stopProducing may have been called while we were waiting.
-        if not self.request:
-            return
-        if data:
-            self.request.write(data)
-        else:
-            self.request.unregisterProducer()
-            self.request.finish()
-            self.stopProducing()
+        """See `IPushProducer`."""
+        self.producing = True
+        if self.request:
+            reactor.callLater(0, self._produceFromStream)
+
+    def stopProducing(self):
+        """See `IProducer`."""
+        self.producing = False
+        self.stream.close()
+        self.request = None
 
 
 class DigestSearchResource(resource.Resource):

=== modified file 'lib/lp/services/twistedsupport/tests/test_xmlrpc.py'
--- lib/lp/services/twistedsupport/tests/test_xmlrpc.py	2012-06-29 08:40:05 +0000
+++ lib/lp/services/twistedsupport/tests/test_xmlrpc.py	2018-04-06 08:39:18 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for Twisted XML-RPC support."""
@@ -49,28 +49,30 @@
         except:
             return Failure()
 
-    def assertRaisesFailure(self, failure, function, *args, **kwargs):
+    def assertRaisesExactException(self, exception, function, *args, **kwargs):
         try:
             function(*args, **kwargs)
-        except Failure as raised_failure:
-            self.assertEqual(failure, raised_failure)
+        except Exception as raised_exception:
+            self.assertEqual(raised_exception, exception)
 
     def test_raises_non_faults(self):
-        # trap_fault re-raises any failures it gets that aren't faults.
+        # trap_fault re-raises the underlying exception from any failures it
+        # gets that aren't faults.
         failure = self.makeFailure(RuntimeError, 'example failure')
-        self.assertRaisesFailure(failure, trap_fault, failure, TestFaultOne)
+        self.assertRaisesExactException(
+            failure.value, trap_fault, failure, TestFaultOne)
 
     def test_raises_faults_with_wrong_code(self):
-        # trap_fault re-raises any failures it gets that are faults but have
-        # the wrong fault code.
+        # trap_fault re-raises the underlying exception from any failures it
+        # gets that are faults but have the wrong fault code.
         failure = self.makeFailure(TestFaultOne)
-        self.assertRaisesFailure(failure, trap_fault, failure, TestFaultTwo)
+        self.assertRaisesExactException(
+            failure.value, trap_fault, failure, TestFaultTwo)
 
     def test_raises_faults_if_no_codes_given(self):
-        # If trap_fault is not given any fault codes, it re-raises the fault
-        # failure.
+        # If trap_fault is not given any fault codes, it re-raises the fault.
         failure = self.makeFailure(TestFaultOne)
-        self.assertRaisesFailure(failure, trap_fault, failure)
+        self.assertRaisesExactException(failure.value, trap_fault, failure)
 
     def test_returns_fault_if_code_matches(self):
         # trap_fault returns the Fault inside the Failure if the fault code

=== modified file 'lib/lp/services/twistedsupport/xmlrpc.py'
--- lib/lp/services/twistedsupport/xmlrpc.py	2011-03-29 13:57:20 +0000
+++ lib/lp/services/twistedsupport/xmlrpc.py	2018-04-06 08:39:18 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Support for XML-RPC stuff with Twisted."""
@@ -55,12 +55,13 @@
 
     :param failure: A Twisted L{Failure}.
     :param *fault_codes: `LaunchpadFault` subclasses.
-    :raise Failure: if 'failure' is not a Fault failure, or if the fault code
-        does not match the given codes.
+    :raise Exception: the underlying exception from 'failure' if 'failure'
+        is not a Fault failure, or if the fault code does not match the
+        given codes.
     :return: The Fault if it matches one of the codes.
     """
     failure.trap(xmlrpc.Fault)
     fault = failure.value
     if fault.faultCode in [cls.error_code for cls in fault_classes]:
         return fault
-    raise failure
+    failure.raiseException()

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2017-11-10 02:13:28 +0000
+++ lib/lp/testing/__init__.py	2018-04-06 08:39:18 +0000
@@ -703,9 +703,10 @@
         """Include the logChunks from fixture in the test details."""
         # Evaluate the log when called, not later, to permit the librarian to
         # be shutdown before the detail is rendered.
-        chunks = fixture.getLogChunks()
-        content = Content(UTF8_TEXT, lambda: chunks)
-        self.addDetail('librarian-log', content)
+        if 'librarian-log' not in self.getDetails():
+            chunks = fixture.getLogChunks()
+            content = Content(UTF8_TEXT, lambda: chunks)
+            self.addDetail('librarian-log', content)
 
     def setUp(self):
         super(TestCase, self).setUp()


Follow ups