Skip to content

Commit

Permalink
Merge 69a3a39 into 4efe083
Browse files Browse the repository at this point in the history
  • Loading branch information
althonos committed Mar 26, 2021
2 parents 4efe083 + 69a3a39 commit a1abb51
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 130 deletions.
35 changes: 27 additions & 8 deletions fs/_ftp_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@


def get_decoders():
"""
Returns all available FTP LIST line decoders with their matching regexes.
"""
"""Return all available FTP LIST line decoders with their matching regexes."""
decoders = [
(RE_LINUX, decode_linux),
(RE_WINDOWSNT, decode_windowsnt),
Expand Down Expand Up @@ -148,13 +146,34 @@ def _decode_windowsnt_time(mtime):


def decode_windowsnt(line, match):
"""
Decodes a Windows NT FTP LIST line like one of these:
"""Decode a Windows NT FTP LIST line.
Examples:
Decode a directory line::
>>> line = "11-02-18 02:12PM <DIR> images"
>>> match = RE_WINDOWSNT.match(line)
>>> pprint(decode_windowsnt(line, match))
{'basic': {'is_dir': True, 'name': 'images'},
'details': {'modified': 1518358320.0, 'type': 1},
'ftp': {'ls': '11-02-18 02:12PM <DIR> images'}}
Decode a file line::
>>> line = "11-02-18 03:33PM 9276 logo.gif"
>>> match = RE_WINDOWSNT.match(line)
>>> pprint(decode_windowsnt(line, match))
{'basic': {'is_dir': False, 'name': 'logo.gif'},
'details': {'modified': 1518363180.0, 'size': 9276, 'type': 2},
'ftp': {'ls': '11-02-18 03:33PM 9276 logo.gif'}}
Alternatively, the time might also be present in 24-hour format::
`11-02-18 02:12PM <DIR> images`
`11-02-18 03:33PM 9276 logo.gif`
>>> line = "11-02-18 15:33 9276 logo.gif"
>>> match = RE_WINDOWSNT.match(line)
>>> decode_windowsnt(line, match)["details"]["modified"]
1518363180.0
Alternatively, the time (02:12PM) might also be present in 24-hour format (14:12).
"""
is_dir = match.group("size") == "<DIR>"

Expand Down
2 changes: 1 addition & 1 deletion fs/_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def make_repr(class_name, *args, **kwargs):
>>> MyClass('Will')
MyClass('foo', name='Will')
>>> MyClass(None)
MyClass()
MyClass('foo')
"""
arguments = [repr(arg) for arg in args]
Expand Down
30 changes: 17 additions & 13 deletions fs/_url_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

def url_quote(path_snippet):
# type: (Text) -> Text
"""
On Windows, it will separate drive letter and quote windows
path alone. No magic on Unix-alie path, just pythonic
`pathname2url`
"""Quote a URL without quoting the Windows drive letter, if any.
On Windows, it will separate drive letter and quote Windows
path alone. No magic on Unix-like path, just pythonic
`~urllib.request.pathname2url`.
Arguments:
path_snippet: a file path, relative or absolute.
path_snippet (str): a file path, relative or absolute.
"""
if _WINDOWS_PLATFORM and _has_drive_letter(path_snippet):
drive_letter, path = path_snippet.split(":", 1)
Expand All @@ -34,17 +36,19 @@ def url_quote(path_snippet):

def _has_drive_letter(path_snippet):
# type: (Text) -> bool
"""
The following path will get True
D:/Data
C:\\My Dcouments\\ test
"""Check whether a path contains a drive letter.
And will get False
Arguments:
path_snippet (str): a file path, relative or absolute.
/tmp/abc:test
Example:
>>> _has_drive_letter("D:/Data")
True
>>> _has_drive_letter(r"C:\\System32\\ test")
True
>>> _has_drive_letter("/tmp/abc:test")
False
Arguments:
path_snippet: a file path, relative or absolute.
"""
windows_drive_pattern = ".:[/\\\\].*$"
return re.match(windows_drive_pattern, path_snippet) is not None
48 changes: 34 additions & 14 deletions fs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,16 @@ def getinfo(self, path, namespaces=None):
Arguments:
path (str): A path to a resource on the filesystem.
namespaces (list, optional): Info namespaces to query
(defaults to *[basic]*).
namespaces (list, optional): Info namespaces to query. The
`"basic"` namespace is alway included in the returned
info, whatever the value of `namespaces` may be.
Returns:
~fs.info.Info: resource information object.
Raises:
fs.errors.ResourceNotFound: If ``path`` does not exist.
For more information regarding resource information, see :ref:`info`.
"""
Expand Down Expand Up @@ -235,10 +239,12 @@ def openbin(
io.IOBase: a *file-like* object.
Raises:
fs.errors.FileExpected: If the path is not a file.
fs.errors.FileExists: If the file exists, and *exclusive mode*
is specified (``x`` in the mode).
fs.errors.ResourceNotFound: If the path does not exist.
fs.errors.FileExpected: If ``path`` exists and is not a file.
fs.errors.FileExists: If the ``path`` exists, and
*exclusive mode* is specified (``x`` in the mode).
fs.errors.ResourceNotFound: If ``path`` does not exist and
``mode`` does not imply creating the file, or if any
ancestor of ``path`` does not exist.
"""

Expand Down Expand Up @@ -402,6 +408,7 @@ def copy(self, src_path, dst_path, overwrite=False):
and ``overwrite`` is `False`.
fs.errors.ResourceNotFound: If a parent directory of
``dst_path`` does not exist.
fs.errors.FileExpected: If ``src_path`` is not a file.
"""
with self._lock:
Expand Down Expand Up @@ -587,6 +594,7 @@ def readbytes(self, path):
bytes: the file contents.
Raises:
fs.errors.FileExpected: if ``path`` exists but is not a file.
fs.errors.ResourceNotFound: if ``path`` does not exist.
"""
Expand All @@ -603,6 +611,10 @@ def download(self, path, file, chunk_size=None, **options):
This may be more efficient that opening and copying files
manually if the filesystem supplies an optimized method.
Note that the file object ``file`` will *not* be closed by this
method. Take care to close it after this method completes
(ideally with a context manager).
Arguments:
path (str): Path to a resource.
file (file-like): A file-like object open for writing in
Expand All @@ -613,13 +625,12 @@ def download(self, path, file, chunk_size=None, **options):
**options: Implementation specific options required to open
the source file.
Note that the file object ``file`` will *not* be closed by this
method. Take care to close it after this method completes
(ideally with a context manager).
Example:
>>> with open('starwars.mov', 'wb') as write_file:
... my_fs.download('/movies/starwars.mov', write_file)
... my_fs.download('/Videos/starwars.mov', write_file)
Raises:
fs.errors.ResourceNotFound: if ``path`` does not exist.
"""
with self._lock:
Expand Down Expand Up @@ -726,6 +737,9 @@ def getsize(self, path):
Returns:
int: the *size* of the resource.
Raises:
fs.errors.ResourceNotFound: if ``path`` does not exist.
The *size* of a file is the total number of readable bytes,
which may not reflect the exact number of bytes of reserved
disk space (or other storage medium).
Expand Down Expand Up @@ -986,6 +1000,7 @@ def lock(self):
Example:
>>> with my_fs.lock(): # May block
... # code here has exclusive access to the filesystem
... pass
It is a good idea to put a lock around any operations that you
would like to be *atomic*. For instance if you are copying
Expand Down Expand Up @@ -1017,6 +1032,8 @@ def movedir(self, src_path, dst_path, create=False):
Raises:
fs.errors.ResourceNotFound: if ``dst_path`` does not exist,
and ``create`` is `False`.
fs.errors.DirectoryExpected: if ``src_path`` or one of its
ancestors is not a directory.
"""
with self._lock:
Expand Down Expand Up @@ -1561,9 +1578,9 @@ def match(self, patterns, name):
names).
Example:
>>> home_fs.match(['*.py'], '__init__.py')
>>> my_fs.match(['*.py'], '__init__.py')
True
>>> home_fs.match(['*.jpg', '*.png'], 'foo.gif')
>>> my_fs.match(['*.jpg', '*.png'], 'foo.gif')
False
Note:
Expand Down Expand Up @@ -1616,13 +1633,16 @@ def hash(self, path, name):
Arguments:
path(str): A path on the filesystem.
name(str):
One of the algorithms supported by the hashlib module, e.g. `"md5"`
One of the algorithms supported by the `hashlib` module,
e.g. `"md5"` or `"sha256"`.
Returns:
str: The hex digest of the hash.
Raises:
fs.errors.UnsupportedHash: If the requested hash is not supported.
fs.errors.ResourceNotFound: If ``path`` does not exist.
fs.errors.FileExpected: If ``path`` exists but is not a file.
"""
self.validatepath(path)
Expand Down
6 changes: 3 additions & 3 deletions fs/filesize.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def traditional(size):
`str`: A string containing an abbreviated file size and units.
Example:
>>> filesize.traditional(30000)
>>> fs.filesize.traditional(30000)
'29.3 KB'
"""
Expand All @@ -87,7 +87,7 @@ def binary(size):
`str`: A string containing a abbreviated file size and units.
Example:
>>> filesize.binary(30000)
>>> fs.filesize.binary(30000)
'29.3 KiB'
"""
Expand All @@ -112,7 +112,7 @@ def decimal(size):
`str`: A string containing a abbreviated file size and units.
Example:
>>> filesize.decimal(30000)
>>> fs.filesize.decimal(30000)
'30.0 kB'
"""
Expand Down
15 changes: 9 additions & 6 deletions fs/ftpfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,22 +358,25 @@ class FTPFS(FS):
FTPS, or FTP Secure. TLS will be enabled when using the ftps:// protocol,
or when setting the `tls` argument to True in the constructor.
Examples:
Create with the constructor::
>>> from fs.ftpfs import FTPFS
>>> ftp_fs = FTPFS()
>>> ftp_fs = FTPFS("demo.wftpserver.com")
Or via an FS URL::
>>> import fs
>>> ftp_fs = fs.open_fs('ftp://')
>>> ftp_fs = fs.open_fs('ftp://test.rebex.net')
Or via an FS URL, using TLS::
>>> import fs
>>> ftp_fs = fs.open_fs('ftps://')
>>> ftp_fs = fs.open_fs('ftps://demo.wftpserver.com')
You can also use a non-anonymous username, and optionally a
password, even within a FS URL::
>>> ftp_fs = FTPFS("test.rebex.net", user="demo", passwd="password")
>>> ftp_fs = fs.open_fs('ftp://demo:password@test.rebex.net')
"""

Expand Down
15 changes: 6 additions & 9 deletions fs/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,8 @@ def count(self):
"""Count files / directories / data in matched paths.
Example:
>>> import fs
>>> fs.open_fs('~/projects').glob('**/*.py').count()
Counts(files=18519, directories=0, data=206690458)
>>> my_fs.glob('**/*.py').count()
Counts(files=2, directories=0, data=55)
Returns:
`~Counts`: A named tuple containing results.
Expand All @@ -199,9 +198,8 @@ def count_lines(self):
`~LineCounts`: A named tuple containing line counts.
Example:
>>> import fs
>>> fs.open_fs('~/projects').glob('**/*.py').count_lines()
LineCounts(lines=5767102, non_blank=4915110)
>>> my_fs.glob('**/*.py').count_lines()
LineCounts(lines=4, non_blank=3)
"""
lines = 0
Expand All @@ -222,9 +220,8 @@ def remove(self):
int: Number of file and directories removed.
Example:
>>> import fs
>>> fs.open_fs('~/projects/my_project').glob('**/*.pyc').remove()
29
>>> my_fs.glob('**/*.pyc').remove()
2
"""
removes = 0
Expand Down

0 comments on commit a1abb51

Please sign in to comment.