Merge lp:~mnn282/bzr/sftp-unsupported-operation-more-info into lp:bzr

Proposed by mnn
Status: Needs review
Proposed branch: lp:~mnn282/bzr/sftp-unsupported-operation-more-info
Merge into: lp:bzr
Diff against target: 218 lines (+70/-15)
4 files modified
bzrlib/errors.py (+10/-0)
bzrlib/tests/test_sftp_transport.py (+37/-0)
bzrlib/transport/__init__.py (+5/-4)
bzrlib/transport/sftp.py (+18/-11)
To merge this branch: bzr merge lp:~mnn282/bzr/sftp-unsupported-operation-more-info
Reviewer Review Type Date Requested Status
Martin Packman (community) Needs Fixing
Review via email: mp+116849@code.launchpad.net

Description of the change

Unsupported operations now have more information in SFTP transport.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Idea of adding a new exception here seems reasonable.

You'll want to add it inside _translate_io_exception rather than adding a special separate branch before hand. That might mean tweaking that method a little, but it's private so there's no reason to to adapt it.

You want some tests as well, you can probably just test the translation itself rather than trying to fake out paramiko to raise the exception you're observing.

review: Needs Fixing
6546. By mnn

Moved handling of unsupported operations into SFTPTransport._translate_io_exception, which has now optional argument 'operation'

SFTPTransport._translate_io_exception and Transport._translate_error are now static methods

Revision history for this message
mnn (mnn282) wrote :

Martin Packman: So, the handling has been moved into _translate_io_exception. I also added tests, as you requested.

I was told on IRC that I should make _translate_io_exception static/class method, so tests actually won't be depended on SFTP transport itself - I had to make Transport._translate_error static too, because _translate_io_exception was invoking it.

Revision history for this message
Martin Packman (gz) wrote :

Thanks for making those changes, not all the suggestions I made were completely correct so I hope you don't mind poking some of the minor details again.

Style nit, two lines between top level things in modules, see:
<http://www.python.org/dev/peps/pep-0008/#blank-lines>

+ self.message = "\n" + message

So, I said 'msg' wasn't a great name, but annoyingly 'message' is worse due to some legacy Python exception stuff. How about 'description' or something?

Also, having english text as a param prevents that part from being translated, but this isn't too important.

Including the path, and just using the operation name rather than the text form would work for everything but the symlink/rename case, which could just be an optional target_path param to the exception.

+ self.assertEquals(e.message, "\nunable to symlink")
\ No newline at end of file

As with the other mp, this needs a terminal newline or bt.test_source fails.

+ @staticmethod
+ def _translate_io_exception(e, path, more_info='',
...
- self._translate_error(e, path, raise_generic=False)
+ Transport._translate_error(e, path, raise_generic=False)

The neat way around needing the Transport import for this is to instead do:

    @classmethod
    def _translate_io_exception(cls, e, path, more_info='',
...
        cls._translate_error(e, path, raise_generic=False)

Leaving _translate_error as @staticerror is fine.

+ # raise errors.TransportNotPossible(more_info)

Just delete this.

review: Needs Fixing
6547. By mnn

- SFTPTransport._translate_io_exception changed to @classmethod
- changed TransportOperationNotSupported.message to description

Revision history for this message
mnn (mnn282) wrote :

> Also, having english text as a param prevents that part from being translated, but this isn't too important.
But other exception handling code in SFTPTransport has only english text as parameter too.

> Including the path, and just using the operation name rather than the text form would work for everything but the symlink/rename case, which could just be an optional target_path param to the exception.
This would require altering _translate_io_exception parameters even more.

Revision history for this message
Martin Packman (gz) wrote :

> But other exception handling code in SFTPTransport has only english text as
> parameter too.

Right I wouldn't worry to much about doing this.

> This would require altering _translate_io_exception parameters even more.

I think passing the path to the exception is potentially useful, and shouldn't mean too much extra fiddling.

Unmerged revisions

6547. By mnn

- SFTPTransport._translate_io_exception changed to @classmethod
- changed TransportOperationNotSupported.message to description

6546. By mnn

Moved handling of unsupported operations into SFTPTransport._translate_io_exception, which has now optional argument 'operation'

SFTPTransport._translate_io_exception and Transport._translate_error are now static methods

6545. By mnn

Display more information for TransportOperationNotSupported (for rename - path from and path to)

6544. By mnn

Unsupported operations now have more information in SFTP transport

- stubs for unsupported transport operations (TransportOperationNotSupported) - used by rename operations in SFTP transport

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py 2012-07-19 19:27:22 +0000
+++ bzrlib/errors.py 2012-07-31 15:32:30 +0000
@@ -1619,6 +1619,16 @@
1619 self.traceback_text = ''.join(traceback_strings)1619 self.traceback_text = ''.join(traceback_strings)
16201620
16211621
1622class TransportOperationNotSupported(TransportError):
1623
1624 _fmt = "Transport operation is not supported: %(operation)s%(description)s"
1625
1626 def __init__(self, operation, description=None):
1627 if description != None:
1628 self.description = "\n" + description
1629 self.operation = operation
1630
1631
1622# A set of semi-meaningful errors which can be thrown1632# A set of semi-meaningful errors which can be thrown
1623class TransportNotPossible(TransportError):1633class TransportNotPossible(TransportError):
16241634
16251635
=== modified file 'bzrlib/tests/test_sftp_transport.py'
--- bzrlib/tests/test_sftp_transport.py 2012-02-23 23:26:35 +0000
+++ bzrlib/tests/test_sftp_transport.py 2012-07-31 15:32:30 +0000
@@ -497,3 +497,40 @@
497 # No prompts should've been printed, stdin shouldn't have been read497 # No prompts should've been printed, stdin shouldn't have been read
498 self.assertEquals("", stdout.getvalue())498 self.assertEquals("", stdout.getvalue())
499 self.assertEquals(0, ui.ui_factory.stdin.tell())499 self.assertEquals(0, ui.ui_factory.stdin.tell())
500
501class TestTranslateIOException(TestCase):
502 """Test that unsupported operation exception from Paramiko is correctly
503 translated into TransportOperationNotSupported."""
504
505 def test_rename_translation(self):
506 """Test unsupported rename operation by matching raised exception"""
507
508 e = self.assertRaises(
509 errors.TransportOperationNotSupported,
510 _mod_sftp.SFTPTransport._translate_io_exception,
511 IOError("Operation unsupported"), "",
512 "unable to rename", operation="rename")
513 self.assertEquals(e.operation, "rename")
514 self.assertEquals(e.message, "\nunable to rename")
515
516 def test_readlink_translation(self):
517 """Test unsupported readlink operation by matching raised exception"""
518
519 e = self.assertRaises(
520 errors.TransportOperationNotSupported,
521 _mod_sftp.SFTPTransport._translate_io_exception,
522 IOError("Operation unsupported"), "",
523 "unable to readlink", operation="readlink")
524 self.assertEquals(e.operation, "readlink")
525 self.assertEquals(e.message, "\nunable to readlink")
526
527 def test_symlink_translation(self):
528 """Test unsupported symlink operation by matching raised exception"""
529
530 e = self.assertRaises(
531 errors.TransportOperationNotSupported,
532 _mod_sftp.SFTPTransport._translate_io_exception,
533 IOError("Operation unsupported"), "",
534 "unable to symlink", operation="symlink")
535 self.assertEquals(e.operation, "symlink")
536 self.assertEquals(e.message, "\nunable to symlink")
500537
=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py 2012-03-13 17:25:29 +0000
+++ bzrlib/transport/__init__.py 2012-07-31 15:32:30 +0000
@@ -244,7 +244,7 @@
244 def fdatasync(self):244 def fdatasync(self):
245 """Force data out to physical disk if possible.245 """Force data out to physical disk if possible.
246246
247 :raises TransportNotPossible: If this transport has no way to 247 :raises TransportNotPossible: If this transport has no way to
248 flush to disk.248 flush to disk.
249 """249 """
250 raise errors.TransportNotPossible(250 raise errors.TransportNotPossible(
@@ -321,7 +321,7 @@
321 # where the biggest benefit between combining reads and321 # where the biggest benefit between combining reads and
322 # and seeking is. Consider a runtime auto-tune.322 # and seeking is. Consider a runtime auto-tune.
323 _bytes_to_read_before_seek = 0323 _bytes_to_read_before_seek = 0
324 324
325 hooks = TransportHooks()325 hooks = TransportHooks()
326326
327 def __init__(self, base):327 def __init__(self, base):
@@ -330,7 +330,8 @@
330 (self._raw_base, self._segment_parameters) = (330 (self._raw_base, self._segment_parameters) = (
331 urlutils.split_segment_parameters(base))331 urlutils.split_segment_parameters(base))
332332
333 def _translate_error(self, e, path, raise_generic=True):333 @staticmethod
334 def _translate_error(e, path, raise_generic=True):
334 """Translate an IOError or OSError into an appropriate bzr error.335 """Translate an IOError or OSError into an appropriate bzr error.
335336
336 This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS337 This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS
@@ -1631,7 +1632,7 @@
16311632
1632def get_transport_from_url(url, possible_transports=None):1633def get_transport_from_url(url, possible_transports=None):
1633 """Open a transport to access a URL.1634 """Open a transport to access a URL.
1634 1635
1635 :param base: a URL1636 :param base: a URL
1636 :param transports: optional reusable transports list. If not None, created1637 :param transports: optional reusable transports list. If not None, created
1637 transports will be added to the list.1638 transports will be added to the list.
16381639
=== modified file 'bzrlib/transport/sftp.py'
--- bzrlib/transport/sftp.py 2011-12-19 13:23:58 +0000
+++ bzrlib/transport/sftp.py 2012-07-31 15:32:30 +0000
@@ -55,6 +55,7 @@
55 _file_streams,55 _file_streams,
56 ssh,56 ssh,
57 ConnectedTransport,57 ConnectedTransport,
58 Transport
58 )59 )
5960
60# Disable one particular warning that comes from paramiko in Python2.5; if61# Disable one particular warning that comes from paramiko in Python2.5; if
@@ -670,8 +671,9 @@
670 _file_streams[self.abspath(relpath)] = handle671 _file_streams[self.abspath(relpath)] = handle
671 return FileFileStream(self, relpath, handle)672 return FileFileStream(self, relpath, handle)
672673
673 def _translate_io_exception(self, e, path, more_info='',674 @classmethod
674 failure_exc=PathError):675 def _translate_io_exception(cls, e, path, more_info='',
676 failure_exc=PathError, operation=None):
675 """Translate a paramiko or IOError into a friendlier exception.677 """Translate a paramiko or IOError into a friendlier exception.
676678
677 :param e: The original exception679 :param e: The original exception
@@ -683,9 +685,10 @@
683 no more information.685 no more information.
684 If this parameter is set, it defines the exception686 If this parameter is set, it defines the exception
685 to raise in these cases.687 to raise in these cases.
688 :param operation: Operation that failed (needs to check if it's supported)
686 """689 """
687 # paramiko seems to generate detailless errors.690 # paramiko seems to generate detailless errors.
688 self._translate_error(e, path, raise_generic=False)691 Transport._translate_error(e, path, raise_generic=False)
689 if getattr(e, 'args', None) is not None:692 if getattr(e, 'args', None) is not None:
690 if (e.args == ('No such file or directory',) or693 if (e.args == ('No such file or directory',) or
691 e.args == ('No such file',)):694 e.args == ('No such file',)):
@@ -703,7 +706,7 @@
703 or getattr(e, 'errno', None) == errno.ENOTEMPTY):706 or getattr(e, 'errno', None) == errno.ENOTEMPTY):
704 raise errors.DirectoryNotEmpty(path, str(e))707 raise errors.DirectoryNotEmpty(path, str(e))
705 if e.args == ('Operation unsupported',):708 if e.args == ('Operation unsupported',):
706 raise errors.TransportNotPossible()709 raise errors.TransportOperationNotSupported(operation, more_info)
707 mutter('Raising exception with args %s', e.args)710 mutter('Raising exception with args %s', e.args)
708 if getattr(e, 'errno', None) is not None:711 if getattr(e, 'errno', None) is not None:
709 mutter('Raising exception with errno %s', e.errno)712 mutter('Raising exception with errno %s', e.errno)
@@ -732,7 +735,7 @@
732 self._remote_path(rel_to))735 self._remote_path(rel_to))
733 except (IOError, paramiko.SSHException), e:736 except (IOError, paramiko.SSHException), e:
734 self._translate_io_exception(e, rel_from,737 self._translate_io_exception(e, rel_from,
735 ': unable to rename to %r' % (rel_to))738 ': unable to rename to %r' % (rel_to), "rename")
736739
737 def _rename_and_overwrite(self, abs_from, abs_to):740 def _rename_and_overwrite(self, abs_from, abs_to):
738 """Do a fancy rename on the remote server.741 """Do a fancy rename on the remote server.
@@ -746,7 +749,7 @@
746 unlink_func=sftp.remove)749 unlink_func=sftp.remove)
747 except (IOError, paramiko.SSHException), e:750 except (IOError, paramiko.SSHException), e:
748 self._translate_io_exception(e, abs_from,751 self._translate_io_exception(e, abs_from,
749 ': unable to rename to %r' % (abs_to))752 ': unable to rename to %r' % (abs_to), operation="rename")
750753
751 def move(self, rel_from, rel_to):754 def move(self, rel_from, rel_to):
752 """Move the item at rel_from to the location at rel_to"""755 """Move the item at rel_from to the location at rel_to"""
@@ -760,7 +763,8 @@
760 try:763 try:
761 self._get_sftp().remove(path)764 self._get_sftp().remove(path)
762 except (IOError, paramiko.SSHException), e:765 except (IOError, paramiko.SSHException), e:
763 self._translate_io_exception(e, path, ': unable to delete')766 self._translate_io_exception(e, path, ': unable to delete',
767 operation="delete")
764768
765 def external_url(self):769 def external_url(self):
766 """See bzrlib.transport.Transport.external_url."""770 """See bzrlib.transport.Transport.external_url."""
@@ -793,7 +797,8 @@
793 try:797 try:
794 return self._get_sftp().rmdir(path)798 return self._get_sftp().rmdir(path)
795 except (IOError, paramiko.SSHException), e:799 except (IOError, paramiko.SSHException), e:
796 self._translate_io_exception(e, path, ': failed to rmdir')800 self._translate_io_exception(e, path, ': failed to rmdir',
801 operation="rmdir")
797802
798 def stat(self, relpath):803 def stat(self, relpath):
799 """Return the stat information for a file."""804 """Return the stat information for a file."""
@@ -809,7 +814,8 @@
809 try:814 try:
810 return self._get_sftp().readlink(path)815 return self._get_sftp().readlink(path)
811 except (IOError, paramiko.SSHException), e:816 except (IOError, paramiko.SSHException), e:
812 self._translate_io_exception(e, path, ': unable to readlink')817 self._translate_io_exception(e, path, ': unable to readlink',
818 operation="readlink")
813819
814 def symlink(self, source, link_name):820 def symlink(self, source, link_name):
815 """See Transport.symlink."""821 """See Transport.symlink."""
@@ -823,7 +829,8 @@
823 )829 )
824 except (IOError, paramiko.SSHException), e:830 except (IOError, paramiko.SSHException), e:
825 self._translate_io_exception(e, link_name,831 self._translate_io_exception(e, link_name,
826 ': unable to create symlink to %r' % (source))832 ': unable to create symlink to %r' % (source),
833 operation="symlink")
827834
828 def lock_read(self, relpath):835 def lock_read(self, relpath):
829 """836 """
@@ -884,7 +891,7 @@
884 return SFTPFile(self._get_sftp(), handle, 'wb', -1)891 return SFTPFile(self._get_sftp(), handle, 'wb', -1)
885 except (paramiko.SSHException, IOError), e:892 except (paramiko.SSHException, IOError), e:
886 self._translate_io_exception(e, abspath, ': unable to open',893 self._translate_io_exception(e, abspath, ': unable to open',
887 failure_exc=FileExists)894 failure_exc=FileExists, operation="open")
888895
889 def _can_roundtrip_unix_modebits(self):896 def _can_roundtrip_unix_modebits(self):
890 if sys.platform == 'win32':897 if sys.platform == 'win32':