← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/merge-python-tx-tftp into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/merge-python-tx-tftp into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/merge-python-tx-tftp/+merge/116741

Merge https://github.com/allenap/python-tx-tftp/tree/tsize-support and https://github.com/allenap/python-tx-tftp/tree/files-beneath-base into contrib, and update TFTPBackend because get_reader() always returns a Deferred now.
-- 
https://code.launchpad.net/~allenap/maas/merge-python-tx-tftp/+merge/116741
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/merge-python-tx-tftp into lp:maas.
=== modified file 'contrib/python-tx-tftp/tftp/backend.py'
--- contrib/python-tx-tftp/tftp/backend.py	2012-07-05 14:36:27 +0000
+++ contrib/python-tx-tftp/tftp/backend.py	2012-07-25 20:02:33 +0000
@@ -3,6 +3,7 @@
 '''
 from os import fstat
 from tftp.errors import Unsupported, FileExists, AccessViolation, FileNotFound
+from tftp.util import deferred
 from twisted.python.filepath import FilePath, InsecurePath
 import shutil
 import tempfile
@@ -33,8 +34,7 @@
         @raise BackendError: for any other errors, that were encountered while
         attempting to construct a reader
 
-        @return: an object, that provides L{IReader}, or a L{Deferred} that
-        will fire with an L{IReader}
+        @return: a L{Deferred} that will fire with an L{IReader}
 
         """
 
@@ -57,8 +57,7 @@
         @raise BackendError: for any other errors, that were encountered while
         attempting to construct a writer
 
-        @return: an object, that provides L{IWriter}, or a L{Deferred} that
-        will fire with an L{IWriter}
+        @return: a L{Deferred} that will fire with an L{IWriter}
 
         """
 
@@ -195,6 +194,9 @@
     def __init__(self, file_path):
         if file_path.exists():
             raise FileExists(file_path)
+        file_dir = file_path.parent()
+        if not file_dir.exists():
+            file_dir.makedirs()
         self.file_path = file_path
         self.destination_file = self.file_path.open('w')
         self.temp_destination = tempfile.TemporaryFile()
@@ -257,34 +259,34 @@
             self.base = FilePath(base_path)
         self.can_read, self.can_write = can_read, can_write
 
+    @deferred
     def get_reader(self, file_name):
         """
         @see: L{IBackend.get_reader}
 
-        @return: an object, providing L{IReader}
-        @rtype: L{FilesystemReader}
+        @rtype: L{Deferred}, yielding a L{FilesystemReader}
 
         """
         if not self.can_read:
             raise Unsupported("Reading not supported")
         try:
-            target_path = self.base.child(file_name)
+            target_path = self.base.descendant(file_name.split("/"))
         except InsecurePath, e:
             raise AccessViolation("Insecure path: %s" % e)
         return FilesystemReader(target_path)
 
+    @deferred
     def get_writer(self, file_name):
         """
         @see: L{IBackend.get_writer}
 
-        @return: an object, providing L{IWriter}
-        @rtype: L{FilesystemWriter}
+        @rtype: L{Deferred}, yielding a L{FilesystemWriter}
 
         """
         if not self.can_write:
             raise Unsupported("Writing not supported")
         try:
-            target_path = self.base.child(file_name)
+            target_path = self.base.descendant(file_name.split("/"))
         except InsecurePath, e:
             raise AccessViolation("Insecure path: %s" % e)
         return FilesystemWriter(target_path)

=== modified file 'contrib/python-tx-tftp/tftp/test/test_backend.py'
--- contrib/python-tx-tftp/tftp/test/test_backend.py	2012-07-05 14:36:27 +0000
+++ contrib/python-tx-tftp/tftp/test/test_backend.py	2012-07-25 20:02:33 +0000
@@ -2,11 +2,11 @@
 @author: shylent
 '''
 from tftp.backend import (FilesystemSynchronousBackend, FilesystemReader,
-    FilesystemWriter)
+    FilesystemWriter, IReader, IWriter)
 from tftp.errors import Unsupported, AccessViolation, FileNotFound, FileExists
 from twisted.python.filepath import FilePath
+from twisted.internet.defer import inlineCallbacks
 from twisted.trial import unittest
-import os.path
 import shutil
 import tempfile
 
@@ -19,29 +19,57 @@
 
 
     def setUp(self):
-        self.temp_dir = tempfile.mkdtemp()
-        self.existing_file_name = os.path.join(self.temp_dir, 'foo')
-        with open(self.existing_file_name, 'w') as f:
-            f.write(self.test_data)
-
-    def test_unsupported(self):
-        b = FilesystemSynchronousBackend(self.temp_dir, can_read=False)
-        self.assertRaises(Unsupported, b.get_reader, 'foo')
-        self.assert_(b.get_writer('bar'),
-                     "A writer should be dispatched")
-        b = FilesystemSynchronousBackend(self.temp_dir, can_write=False)
-        self.assertRaises(Unsupported, b.get_writer, 'bar')
-        self.assert_(b.get_reader('foo'),
-                     "A reader should be dispatched")
-
-    def test_insecure(self):
-        b = FilesystemSynchronousBackend(self.temp_dir)
-        self.assertRaises(AccessViolation, b.get_reader, '../foo')
-        b = FilesystemSynchronousBackend(self.temp_dir)
-        self.assertRaises(AccessViolation, b.get_writer, '../foo')
+        self.temp_dir = FilePath(tempfile.mkdtemp())
+        self.existing_file_name = self.temp_dir.descendant(("dir", "foo"))
+        self.existing_file_name.parent().makedirs()
+        self.existing_file_name.setContent(self.test_data)
+
+    @inlineCallbacks
+    def test_read_supported_by_default(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        reader = yield b.get_reader('dir/foo')
+        self.assertTrue(IReader.providedBy(reader))
+
+    @inlineCallbacks
+    def test_write_supported_by_default(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        writer = yield b.get_writer('dir/bar')
+        self.assertTrue(IWriter.providedBy(writer))
+
+    def test_read_unsupported(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path, can_read=False)
+        return self.assertFailure(b.get_reader('dir/foo'), Unsupported)
+
+    def test_write_unsupported(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path, can_write=False)
+        return self.assertFailure(b.get_writer('dir/bar'), Unsupported)
+
+    def test_insecure_reader(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        return self.assertFailure(
+            b.get_reader('../foo'), AccessViolation)
+
+    def test_insecure_writer(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        return self.assertFailure(
+            b.get_writer('../foo'), AccessViolation)
+
+    @inlineCallbacks
+    def test_read_ignores_leading_and_trailing_slashes(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        reader = yield b.get_reader('/dir/foo/')
+        segments_from_root = reader.file_path.segmentsFrom(self.temp_dir)
+        self.assertEqual(["dir", "foo"], segments_from_root)
+
+    @inlineCallbacks
+    def test_write_ignores_leading_and_trailing_slashes(self):
+        b = FilesystemSynchronousBackend(self.temp_dir.path)
+        writer = yield b.get_writer('/dir/bar/')
+        segments_from_root = writer.file_path.segmentsFrom(self.temp_dir)
+        self.assertEqual(["dir", "bar"], segments_from_root)
 
     def tearDown(self):
-        shutil.rmtree(self.temp_dir)
+        shutil.rmtree(self.temp_dir.path)
 
 
 class Reader(unittest.TestCase):
@@ -94,6 +122,7 @@
         r.read(3)
         r.finish()
         self.failUnless(r.file_obj.closed,
+
             "The session has been finished, so the file object should be in the closed state")
         r.finish()
 
@@ -110,12 +139,19 @@
     def setUp(self):
         self.temp_dir = FilePath(tempfile.mkdtemp())
         self.existing_file_name = self.temp_dir.child('foo')
-        with self.existing_file_name.open('w') as f:
-            f.write(self.test_data)
+        self.existing_file_name.setContent(self.test_data)
 
     def test_write_existing_file(self):
         self.assertRaises(FileExists, FilesystemWriter, self.temp_dir.child('foo'))
 
+    def test_write_to_non_existent_directory(self):
+        new_directory = self.temp_dir.child("new")
+        new_file = new_directory.child("baz")
+        self.assertFalse(new_directory.exists())
+        FilesystemWriter(new_file).finish()
+        self.assertTrue(new_directory.exists())
+        self.assertTrue(new_file.exists())
+
     def test_finished_write(self):
         w = FilesystemWriter(self.temp_dir.child('bar'))
         w.write(self.test_data)

=== modified file 'contrib/python-tx-tftp/tftp/test/test_protocol.py'
--- contrib/python-tx-tftp/tftp/test/test_protocol.py	2012-07-05 14:36:27 +0000
+++ contrib/python-tx-tftp/tftp/test/test_protocol.py	2012-07-25 20:02:33 +0000
@@ -202,15 +202,19 @@
         self.clock = clock
 
     def get_reader(self, file_name):
-        reader = super(FilesystemAsyncBackend, self).get_reader(file_name)
+        d_get = super(FilesystemAsyncBackend, self).get_reader(file_name)
         d = Deferred()
-        self.clock.callLater(0, d.callback, reader)
+        # d_get has already fired, so don't chain d_get to d until later,
+        # otherwise d will be fired too early.
+        self.clock.callLater(0, d_get.chainDeferred, d)
         return d
 
     def get_writer(self, file_name):
-        writer = super(FilesystemAsyncBackend, self).get_writer(file_name)
+        d_get = super(FilesystemAsyncBackend, self).get_writer(file_name)
         d = Deferred()
-        self.clock.callLater(0, d.callback, writer)
+        # d_get has already fired, so don't chain d_get to d until later,
+        # otherwise d will be fired too early.
+        self.clock.callLater(0, d_get.chainDeferred, d)
         return d
 
 
@@ -224,7 +228,7 @@
         self.backend = FilesystemAsyncBackend(self.tmp_dir_path, self.clock)
         self.tftp = TFTP(self.backend, self.clock)
 
-    def test_get_reader_can_defer(self):
+    def test_get_reader_defers(self):
         rrq_datagram = RRQDatagram('nonempty', 'NetASCiI', {})
         rrq_addr = ('127.0.0.1', 1069)
         rrq_mode = "octet"
@@ -234,7 +238,7 @@
         self.assertTrue(d.called)
         self.assertTrue(IReader.providedBy(d.result.backend))
 
-    def test_get_writer_can_defer(self):
+    def test_get_writer_defers(self):
         wrq_datagram = WRQDatagram('foobar', 'NetASCiI', {})
         wrq_addr = ('127.0.0.1', 1069)
         wrq_mode = "octet"

=== modified file 'contrib/python-tx-tftp/tftp/util.py'
--- contrib/python-tx-tftp/tftp/util.py	2012-06-27 13:38:02 +0000
+++ contrib/python-tx-tftp/tftp/util.py	2012-07-25 20:02:33 +0000
@@ -1,10 +1,12 @@
 '''
 @author: shylent
 '''
+from functools import wraps
 from twisted.internet import reactor
-
-
-__all__ = ['SequentialCall', 'Spent', 'Cancelled']
+from twisted.internet.defer import maybeDeferred
+
+
+__all__ = ['SequentialCall', 'Spent', 'Cancelled', 'deferred']
 
 
 class Spent(Exception):
@@ -120,3 +122,15 @@
     def active(self):
         """Whether or not this L{SequentialCall} object is considered active"""
         return not (self._spent or self._cancelled)
+
+
+def deferred(func):
+    """Decorates a function to ensure that it always returns a `Deferred`.
+
+    This also serves a secondary documentation purpose; functions decorated
+    with this are readily identifiable as asynchronous.
+    """
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        return maybeDeferred(func, *args, **kwargs)
+    return wrapper

=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py	2012-07-06 15:58:34 +0000
+++ src/provisioningserver/tests/test_tftp.py	2012-07-25 20:02:33 +0000
@@ -118,6 +118,7 @@
             ]
         self.assertItemsEqual(query_expected, query)
 
+    @inlineCallbacks
     def test_get_reader_regular_file(self):
         # TFTPBackend.get_reader() returns a regular FilesystemReader for
         # paths not matching re_config_file.
@@ -125,7 +126,7 @@
         temp_file = self.make_file(name="example", contents=data)
         temp_dir = path.dirname(temp_file)
         backend = TFTPBackend(temp_dir, "http://nowhere.example.com/";)
-        reader = backend.get_reader("example")
+        reader = yield backend.get_reader("example")
         self.addCleanup(reader.finish)
         self.assertEqual(len(data), reader.size)
         self.assertEqual(data, reader.read(len(data)))

=== modified file 'src/provisioningserver/tftp.py'
--- src/provisioningserver/tftp.py	2012-07-06 16:04:53 +0000
+++ src/provisioningserver/tftp.py	2012-07-25 20:02:33 +0000
@@ -22,6 +22,7 @@
     urlparse,
     )
 
+from provisioningserver.utils import deferred
 from tftp.backend import (
     FilesystemSynchronousBackend,
     IReader,
@@ -91,6 +92,7 @@
         # maas_client._ascii_url() for inspiration.
         return url.geturl().encode("ascii")
 
+    @deferred
     def get_reader(self, file_name):
         """See `IBackend.get_reader()`.