diff -r 88fe1ac48460 Doc/library/ftplib.rst --- a/Doc/library/ftplib.rst Mon Mar 07 08:31:52 2011 +0100 +++ b/Doc/library/ftplib.rst Tue Mar 08 11:56:31 2011 +0000 @@ -320,6 +320,18 @@ in :meth:`transfercmd`. +.. method:: FTP.mlsd(path="", facts=None): + + List a directory in a standardized format by using MLSD command + (:rfc:`3659`). If *path* is omitted the current directory is assumed. + *facts* is a sequence of strings representing the type of information + desired (e.g. *["type", "size", "perm"]*). Return a generator of + (*filename*, *facts*) pairs, where *filename* is a string and *facts* is a + dictionary representing the server response. + + .. versionadded:: 3.3 + + .. method:: FTP.nlst(argument[, ...]) Return a list of file names as returned by the ``NLST`` command. The diff -r 88fe1ac48460 Lib/ftplib.py --- a/Lib/ftplib.py Mon Mar 07 08:31:52 2011 +0100 +++ b/Lib/ftplib.py Tue Mar 08 11:56:31 2011 +0000 @@ -527,6 +527,27 @@ cmd = cmd + (' ' + arg) self.retrlines(cmd, func) + def mlsd(self, path="", facts=None): + '''List a directory in a standardized format by using MLSD + command (RFC-3659). If path is omitted the current directory + is assumed. "facts" is a sequence of strings representing the type + of information desired (e.g. ["type", "size", "perm"]). + Return a generator of ("filename", "facts-dict") pairs. Content of + "facts-dict" dictionary, depends on the server and provided "facts" + argument. + ''' + if facts is not None: + self.sendcmd('OPTS MLST ' + ''.join(fact + ';' for fact in facts)) + lines = [] + cmd = 'MLSD %s' % path if path else 'MLSD' + self.retrlines(cmd, lines.append) + return (self._mlsd_entry(line) for line in lines) + + def _mlsd_entry(self, s): + facts, _, name = s.rstrip(CRLF).partition(' ') + facts = dict(fact.partition('=')[::2] for fact in facts[:-1].split(';')) + return name, facts + def rename(self, fromname, toname): '''Rename a file.''' resp = self.sendcmd('RNFR ' + fromname) diff -r 88fe1ac48460 Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Mon Mar 07 08:31:52 2011 +0100 +++ b/Lib/test/test_ftplib.py Tue Mar 08 11:56:31 2011 +0000 @@ -26,7 +26,21 @@ RETR_DATA = 'abcde12345\r\n' * 1000 LIST_DATA = 'foo\r\nbar\r\n' NLST_DATA = 'foo\r\nbar\r\n' - +MLSD_DATA = ('Type=cdir;Perm=el;Unique=keVO1+ZF4; test\r\n' + 'Type=pdir;Perm=e;Unique=keVO1+d?3; ..\r\n' + 'Type=OS.unix=slink:/foobar;Perm=;Unique=keVO1+4G4; foobar\r\n' + 'Type=OS.unix=chr-13/29;Perm=;Unique=keVO1+5G4; device\r\n' + 'Type=OS.unix=blk-11/108;Perm=;Unique=keVO1+6G4; block\r\n' + 'Type=file;Perm=awr;Unique=keVO1+8G4; writable\r\n' + 'Type=dir;Perm=cpmel;Unique=keVO1+7G4; promiscuous\r\n' + 'Type=dir;Perm=;Unique=keVO1+1t2; no-exec\r\n' + 'Type=file;Perm=r;Unique=keVO1+EG4; two words\r\n' + 'Type=file;Perm=r;Unique=keVO1+IH4; leading space\r\n' + 'Type=file;Perm=r;Unique=keVO1+6G4; f; ile1\r\n' + 'Type=dir;Perm=cpmel;Unique=keVO1+7G4; incoming\r\n' + 'Type=file;Perm=r;Unique=keVO1+1G4; file2\r\n' + 'Type=file;Perm=r;Unique=keVO1+1G4; file3\r\n' + 'Type=file;Perm=r;Unique=keVO1+1G4; file4\r\n') class DummyDTPHandler(asynchat.async_chat): dtp_conn_closed = False @@ -208,6 +222,13 @@ self.dtp.push(NLST_DATA) self.dtp.close_when_done() + def cmd_opts(self, arg): + self.push('200 opts ok') + + def cmd_mlsd(self, arg): + self.push('125 mlsd ok') + self.dtp.push(MLSD_DATA) + self.dtp.close_when_done() class DummyFTPServer(asyncore.dispatcher, threading.Thread): @@ -550,6 +571,11 @@ self.client.dir(lambda x: l.append(x)) self.assertEqual(''.join(l), LIST_DATA.replace('\r\n', '')) + def test_mlsd(self): + self.client.mlsd() + self.client.mlsd('/') + self.client.mlsd(path='/', facts=['perm', 'type', 'unique']) + def test_makeport(self): with self.client.makeport(): # IPv4 is in use, just make sure send_eprt has not been used