Skip to content

Commit 9abeaf2

Browse files
authored
Merge branch 'master' into feat-docstring
2 parents cf1c39c + d2da568 commit 9abeaf2

File tree

3 files changed

+79
-72
lines changed

3 files changed

+79
-72
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010

1111
- settext, appendtext, appendbytes, setbytes now raise a TypeError if
1212
the type is wrong, rather than ValueError
13+
- More efficient feature detection for FTPFS
14+
- Fixes for `fs.filesize`
1315

1416
## [2.0.11]
1517

fs/ftpfs.py

Lines changed: 77 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from contextlib import contextmanager
1515
from ftplib import FTP
16-
from ftplib import error_reply
1716
from ftplib import error_perm
1817
from ftplib import error_temp
1918

@@ -47,16 +46,18 @@ def ftp_errors(fs, path=None):
4746
raise errors.RemoteConnectionError(
4847
msg='unable to connect to {}'.format(fs.host)
4948
)
50-
except error_temp as e:
49+
except error_temp as error:
5150
if path is not None:
5251
raise errors.ResourceError(
5352
path,
54-
msg="ftp error on resource '{}' ({})".format(path, e)
53+
msg="ftp error on resource '{}' ({})".format(path, error)
5554
)
5655
else:
57-
raise errors.OperationFailed(msg='ftp error ({})'.format(e))
58-
except error_perm as e:
59-
code, message = parse_ftp_error(e)
56+
raise errors.OperationFailed(
57+
msg='ftp error ({})'.format(error)
58+
)
59+
except error_perm as error:
60+
code, message = _parse_ftp_error(error)
6061
if code == 552:
6162
raise errors.InsufficientStorage(
6263
path=path,
@@ -79,8 +80,10 @@ def manage_ftp(ftp):
7980
except: # pragma: nocover
8081
pass
8182

82-
def parse_ftp_error(e):
83-
code, _, message = text_type(e).partition(' ')
83+
84+
def _parse_ftp_error(error):
85+
"""Extract code and message from ftp error."""
86+
code, _, message = text_type(error).partition(' ')
8487
if code.isdigit():
8588
code = int(code)
8689
return code, message
@@ -101,6 +104,7 @@ def _decode(st, _):
101104
class FTPFile(io.IOBase):
102105

103106
def __init__(self, ftpfs, path, mode):
107+
super(FTPFile, self).__init__()
104108
self.fs = ftpfs
105109
self.path = path
106110
self.mode = Mode(mode)
@@ -111,7 +115,8 @@ def __init__(self, ftpfs, path, mode):
111115
self._write_conn = None
112116

113117
def _open_ftp(self):
114-
ftp = self.fs._open_ftp(self.fs.ftp.encoding)
118+
"""Open an ftp object for the file."""
119+
ftp = self.fs._open_ftp()
115120
ftp.voidcmd(str('TYPE I'))
116121
return ftp
117122

@@ -306,6 +311,7 @@ def __init__(self,
306311
self.timeout = timeout
307312
self.port = port
308313

314+
self.encoding = 'latin-1'
309315
self._ftp = None
310316
self._welcome = None
311317
self._features = None
@@ -321,21 +327,52 @@ def __str__(self):
321327
)
322328
return _fmt.format(host=self.host, port=self.port)
323329

324-
def _open_ftp(self, encoding="utf-8"):
330+
@classmethod
331+
def _parse_features(cls, feat_response):
332+
"""Parse a dict of features from FTP feat response."""
333+
features = {}
334+
if feat_response.split('-')[0] == '211':
335+
for line in feat_response.splitlines():
336+
if line.startswith(' '):
337+
key, _, value = line[1:].partition(' ')
338+
features[key] = value
339+
return features
340+
341+
def _open_ftp(self):
342+
"""Open a new ftp object."""
325343
_ftp = FTP()
326344
_ftp.set_debuglevel(0)
327345
with ftp_errors(self):
328-
_ftp.encoding = encoding
329346
_ftp.connect(self.host, self.port, self.timeout)
330347
_ftp.login(self.user, self.passwd, self.acct)
348+
self._features = {}
349+
try:
350+
feat_response = _decode(_ftp.sendcmd("FEAT"), 'latin-1')
351+
except error_perm:
352+
self.encoding = 'latin-1'
353+
else:
354+
self._features = self._parse_features(feat_response)
355+
self.encoding = (
356+
'utf-8'
357+
if 'UTF8' in self._features
358+
else 'latin-1'
359+
)
360+
if not PY2:
361+
_ftp.file = _ftp.sock.makefile(
362+
'r',
363+
encoding=self.encoding
364+
)
365+
_ftp.encoding = self.encoding
366+
self._welcome = _ftp.welcome
331367
return _ftp
332368

333369
def _manage_ftp(self):
334-
ftp = self._open_ftp(self.ftp.encoding)
370+
ftp = self._open_ftp()
335371
return manage_ftp(ftp)
336372

337373
@property
338374
def ftp_url(self):
375+
"""Get the FTP url this filesystem will open."""
339376
url = (
340377
"ftp://{}".format(self.host)
341378
if self.port == 21
@@ -347,40 +384,18 @@ def ftp_url(self):
347384
def ftp(self):
348385
"""`~ftplib.FTP`: the underlying FTP client.
349386
"""
387+
return self._get_ftp()
388+
389+
def _get_ftp(self):
350390
if self._ftp is None:
351-
_ftp = self._open_ftp('latin-1')
352-
try:
353-
encoding = (
354-
'utf-8'
355-
if 'UTF8' in _ftp.sendcmd('FEAT')
356-
else 'latin-1'
357-
)
358-
except error_perm:
359-
encoding = 'latin-1'
360-
self._ftp = (
361-
_ftp
362-
if encoding == 'latin-1'
363-
else self._open_ftp(encoding)
364-
)
365-
self._welcome = self._ftp.getwelcome()
391+
self._ftp = self._open_ftp()
366392
return self._ftp
367393

368394
@property
369395
def features(self):
370396
"""`dict`: features of the remote FTP server.
371397
"""
372-
if self._features is None:
373-
try:
374-
response = _decode(self.ftp.sendcmd("FEAT"), "ascii")
375-
except error_perm:
376-
self._features = {}
377-
else:
378-
self._features = {}
379-
if response.split('-')[0] == '211':
380-
for line in response.splitlines():
381-
if line.startswith(' '):
382-
k, _, v = line[1:].partition(' ')
383-
self._features[k] = v
398+
self._get_ftp()
384399
return self._features
385400

386401
def _read_dir(self, path):
@@ -401,6 +416,7 @@ def _read_dir(self, path):
401416

402417
@property
403418
def supports_mlst(self):
419+
"""Check if server supports MLST feature."""
404420
return 'MLST' in self.features
405421

406422
def create(self, path, wipe=False):
@@ -414,14 +430,15 @@ def create(self, path, wipe=False):
414430
)
415431

416432
@classmethod
417-
def _parse_ftp_time(cls, t):
433+
def _parse_ftp_time(cls, time_text):
434+
"""Parse a time from an ftp directory listing."""
418435
try:
419-
tm_year = int(t[0:4])
420-
tm_month = int(t[4:6])
421-
tm_day = int(t[6:8])
422-
tm_hour = int(t[8:10])
423-
tm_min = int(t[10:12])
424-
tm_sec = int(t[12:14])
436+
tm_year = int(time_text[0:4])
437+
tm_month = int(time_text[4:6])
438+
tm_day = int(time_text[6:8])
439+
tm_hour = int(time_text[8:10])
440+
tm_min = int(time_text[10:12])
441+
tm_sec = int(time_text[12:14])
425442
except ValueError:
426443
return None
427444
epoch_time = calendar.timegm((
@@ -439,11 +456,11 @@ def _parse_facts(cls, line):
439456
name = None
440457
facts = {}
441458
for fact in line.split(';'):
442-
k, sep, v = fact.partition('=')
459+
key, sep, value = fact.partition('=')
443460
if sep:
444-
k = k.strip().lower()
445-
v = v.strip()
446-
facts[k] = v
461+
key = key.strip().lower()
462+
value = value.strip()
463+
facts[key] = value
447464
else:
448465
name = basename(fact.rstrip('/').strip())
449466
return name if name not in ('.', '..') else None, facts
@@ -520,6 +537,7 @@ def getinfo(self, path, namespaces=None):
520537

521538
def getmeta(self, namespace="standard"):
522539
_meta = {}
540+
self._get_ftp()
523541
if namespace == "standard":
524542
_meta = self._meta.copy()
525543
_meta['unicode_paths'] = "UTF8" in self.features
@@ -548,8 +566,8 @@ def makedir(self, path, permissions=None, recreate=False):
548566
if not (recreate and self.isdir(path)):
549567
try:
550568
self.ftp.mkd(_encode(_path, self.ftp.encoding))
551-
except error_perm as e:
552-
code, _ = parse_ftp_error(e)
569+
except error_perm as error:
570+
code, _ = _parse_ftp_error(error)
553571
if code == 550:
554572
if self.isdir(path):
555573
raise errors.DirectoryExists(path)
@@ -575,13 +593,12 @@ def openbin(self, path, mode="r", buffering=-1, **options):
575593
raise errors.FileExpected(path)
576594
if _mode.exclusive:
577595
raise errors.FileExists(path)
578-
f = FTPFile(self, _path, mode)
579-
return f
596+
ftp_file = FTPFile(self, _path, mode)
597+
return ftp_file
580598

581599
def remove(self, path):
582600
self.check()
583601
_path = self.validatepath(path)
584-
dir_name, file_name = split(_path)
585602
with self._lock:
586603
if self.isdir(path):
587604
raise errors.FileExpected(path=path)
@@ -592,13 +609,12 @@ def removedir(self, path):
592609
_path = self.validatepath(path)
593610
if _path == '/':
594611
raise errors.RemoveRootError()
595-
_dir_name, file_name = split(_path)
596612

597613
with ftp_errors(self, path):
598614
try:
599615
self.ftp.rmd(_encode(_path, self.ftp.encoding))
600-
except error_perm as e:
601-
code, _ = parse_ftp_error(e)
616+
except error_perm as error:
617+
code, _ = _parse_ftp_error(error)
602618
if code == 550:
603619
if self.isfile(path):
604620
raise errors.DirectoryExpected(path)
@@ -617,7 +633,7 @@ def _scandir(self, path, namespaces=None):
617633
str("MLSD ") + _encode(_path, self.ftp.encoding),
618634
lambda l: lines.append(_decode(l, self.ftp.encoding))
619635
)
620-
except error_perm as e:
636+
except error_perm:
621637
if not self.getinfo(path).is_dir:
622638
raise errors.DirectoryExpected(path)
623639
raise # pragma: no cover
@@ -667,8 +683,8 @@ def getbytes(self, path):
667683
str("RETR ") + _encode(_path, self.ftp.encoding),
668684
data.write
669685
)
670-
except error_perm as e:
671-
code, _ = parse_ftp_error(e)
686+
except error_perm as error:
687+
code, _ = _parse_ftp_error(error)
672688
if code == 550:
673689
if self.isdir(path):
674690
raise errors.FileExpected(path)
@@ -685,8 +701,3 @@ def close(self):
685701
pass
686702
self._ftp = None
687703
super(FTPFS, self).close()
688-
689-
690-
if __name__ == "__main__": # pragma: no cover
691-
fs = FTPFS('ftp.mirror.nl', 'anonymous', '[email protected]')
692-
print(list(fs.scandir('/')))

tests/test_ftpfs.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,6 @@ def test_connection_error(self):
143143
with self.assertRaises(errors.RemoteConnectionError):
144144
fs.open('foo.txt')
145145

146-
def test_features(self):
147-
def broken_sendcmd(cmd):
148-
raise ftplib.error_perm('nope')
149-
self.fs.ftp.sendcmd = broken_sendcmd
150-
self.assertEqual(self.fs.features, {})
151-
152146
def test_getmeta_unicode_path(self):
153147
self.assertTrue(self.fs.getmeta().get('unicode_paths'))
154148
self.fs.features

0 commit comments

Comments
 (0)