Commit 6641f314 by willmcgugan

Modified listdir and walk to take callables in addition to wildcards

parent 30c928cc
...@@ -31,3 +31,6 @@ ...@@ -31,3 +31,6 @@
* New implementation of print_fs, accessible through tree method on base class * New implementation of print_fs, accessible through tree method on base class
0.4: 0.4:
* Modified listdir and walk methods to accept callables as well as strings
for wildcards
...@@ -23,6 +23,7 @@ import shutil ...@@ -23,6 +23,7 @@ import shutil
import fnmatch import fnmatch
import datetime import datetime
import time import time
import re
try: try:
import threading import threading
except ImportError: except ImportError:
...@@ -51,7 +52,6 @@ class DummyLock(object): ...@@ -51,7 +52,6 @@ class DummyLock(object):
pass pass
def silence_fserrors(f, *args, **kwargs): def silence_fserrors(f, *args, **kwargs):
"""Perform a function call and return None if FSError is thrown """Perform a function call and return None if FSError is thrown
...@@ -282,7 +282,7 @@ class FS(object): ...@@ -282,7 +282,7 @@ class FS(object):
:param path: root of the path to list :param path: root of the path to list
:type path: string :type path: string
:param wildcard: Only returns paths that match this wildcard :param wildcard: Only returns paths that match this wildcard
:type wildcard: string :type wildcard: string containing a wildcard, or a callable that accepts a path and returns a boolean
:param full: returns full paths (relative to the root) :param full: returns full paths (relative to the root)
:type full: bool :type full: bool
:param absolute: returns absolute paths (paths begining with /) :param absolute: returns absolute paths (paths begining with /)
...@@ -350,8 +350,10 @@ class FS(object): ...@@ -350,8 +350,10 @@ class FS(object):
raise ValueError("dirs_only and files_only can not both be True") raise ValueError("dirs_only and files_only can not both be True")
if wildcard is not None: if wildcard is not None:
match = fnmatch.fnmatch if not callable(wildcard):
entries = [p for p in entries if match(p, wildcard)] wildcard_re = re.compile(fnmatch.translate(wildcard))
wildcard = lambda fn:bool (wildcard_re.match(fn))
entries = [p for p in entries if wildcard(p)]
if dirs_only: if dirs_only:
entries = [p for p in entries if self.isdir(pathjoin(path, p))] entries = [p for p in entries if self.isdir(pathjoin(path, p))]
...@@ -529,7 +531,9 @@ class FS(object): ...@@ -529,7 +531,9 @@ class FS(object):
:param path: root path to start walking :param path: root path to start walking
:param wildcard: if given, only return files that match this wildcard :param wildcard: if given, only return files that match this wildcard
:type wildcard: A string containing a wildcard (e.g. *.txt) or a callable that takes the file path and returns a boolean
:param dir_wildcard: if given, only walk directories that match the wildcard :param dir_wildcard: if given, only walk directories that match the wildcard
:type dir_wildcard: A string containing a wildcard (e.g. *.txt) or a callable that takes the directory name and returns a boolean
:param search: -- a string dentifying the method used to walk the directories. There are two such methods: :param search: -- a string dentifying the method used to walk the directories. There are two such methods:
* 'breadth' Yields paths in the top directories first * 'breadth' Yields paths in the top directories first
* 'depth' Yields the deepest paths first * 'depth' Yields the deepest paths first
...@@ -537,6 +541,19 @@ class FS(object): ...@@ -537,6 +541,19 @@ class FS(object):
""" """
if wildcard is None:
wildcard = lambda f:True
elif not callable(wildcard):
wildcard_re = re.compile(fnmatch.translate(wildcard))
wildcard = lambda fn:bool (wildcard_re.match(fn))
if dir_wildcard is None:
dir_wildcard = lambda f:True
elif not callable(dir_wildcard):
dir_wildcard_re = re.compile(fnmatch.translate(dir_wildcard))
dir_wildcard = lambda fn:bool (dir_wildcard_re.match(fn))
def listdir(path, *args, **kwargs): def listdir(path, *args, **kwargs):
if ignore_errors: if ignore_errors:
try: try:
...@@ -547,26 +564,20 @@ class FS(object): ...@@ -547,26 +564,20 @@ class FS(object):
return self.listdir(path, *args, **kwargs) return self.listdir(path, *args, **kwargs)
if search == "breadth": if search == "breadth":
dirs = [path] dirs = [path]
while dirs: while dirs:
current_path = dirs.pop() current_path = dirs.pop()
paths = [] paths = []
for filename in listdir(current_path): for filename in listdir(current_path):
path = pathjoin(current_path, filename) path = pathjoin(current_path, filename)
if self.isdir(path): if self.isdir(path):
if dir_wildcard is not None: if dir_wildcard(path):
if fnmatch.fnmatch(path, dir_wilcard):
dirs.append(path) dirs.append(path)
else: else:
dirs.append(path) if wildcard(filename):
else:
if wildcard is not None:
if fnmatch.fnmatch(path, wildcard):
paths.append(filename)
else:
paths.append(filename) paths.append(filename)
yield (current_path, paths) yield (current_path, paths)
elif search == "depth": elif search == "depth":
...@@ -579,6 +590,7 @@ class FS(object): ...@@ -579,6 +590,7 @@ class FS(object):
for p in recurse(path): for p in recurse(path):
yield p yield p
else: else:
raise ValueError("Search should be 'breadth' or 'depth'") raise ValueError("Search should be 'breadth' or 'depth'")
...@@ -588,11 +600,13 @@ class FS(object): ...@@ -588,11 +600,13 @@ class FS(object):
dir_wildcard=None, dir_wildcard=None,
search="breadth", search="breadth",
ignore_errors=False ): ignore_errors=False ):
"""Like the 'walk' method, but just yields files. """Like the 'walk' method, but just yields file paths.
:param path: root path to start walking :param path: root path to start walking
:param wildcard: if given, only return files that match this wildcard :param wildcard: if given, only return files that match this wildcard
:type wildcard: A string containing a wildcard (e.g. *.txt) or a callable that takes the file path and returns a boolean
:param dir_wildcard: if given, only walk directories that match the wildcard :param dir_wildcard: if given, only walk directories that match the wildcard
:type dir_wildcard: A string containing a wildcard (e.g. *.txt) or a callable that takes the directory name and returns a boolean
:param search: same as walk method :param search: same as walk method
:param ignore_errors: ignore any errors reading the directory :param ignore_errors: ignore any errors reading the directory
""" """
...@@ -609,6 +623,7 @@ class FS(object): ...@@ -609,6 +623,7 @@ class FS(object):
:param path: root path to start walking :param path: root path to start walking
:param wildcard: if given, only return dictories that match this wildcard :param wildcard: if given, only return dictories that match this wildcard
:type wildcard: A string containing a wildcard (e.g. *.txt) or a callable that takes the directory name and returns a boolean
:param search: same as the walk method :param search: same as the walk method
:param ignore_errors: ignore any errors reading the directory :param ignore_errors: ignore any errors reading the directory
""" """
......
...@@ -306,8 +306,9 @@ class MemoryFS(FS): ...@@ -306,8 +306,9 @@ class MemoryFS(FS):
parent_dir.contents[dirname] = self._make_dir_entry("dir", dirname) parent_dir.contents[dirname] = self._make_dir_entry("dir", dirname)
@synchronize
def _orphan_files(self, file_dir_entry): def _orphan_files(self, file_dir_entry):
for f in file_dir_entry.open_files: for f in file_dir_entry.open_files[:]:
f.close() f.close()
@synchronize @synchronize
...@@ -414,7 +415,6 @@ class MemoryFS(FS): ...@@ -414,7 +415,6 @@ class MemoryFS(FS):
parent_dir = self._get_dir_entry(pathname) parent_dir = self._get_dir_entry(pathname)
del parent_dir.contents[dirname] del parent_dir.contents[dirname]
@synchronize @synchronize
def rename(self, src, dst): def rename(self, src, dst):
src_dir,src_name = pathsplit(src) src_dir,src_name = pathsplit(src)
...@@ -441,7 +441,7 @@ class MemoryFS(FS): ...@@ -441,7 +441,7 @@ class MemoryFS(FS):
dst_dir_entry.xattrs.update(src_xattrs) dst_dir_entry.xattrs.update(src_xattrs)
del src_dir_entry.contents[src_name] del src_dir_entry.contents[src_name]
@synchronize
def settimes(self, path, accessed_time=None, modified_time=None): def settimes(self, path, accessed_time=None, modified_time=None):
now = datetime.datetime.now() now = datetime.datetime.now()
if accessed_time is None: if accessed_time is None:
...@@ -456,7 +456,6 @@ class MemoryFS(FS): ...@@ -456,7 +456,6 @@ class MemoryFS(FS):
return True return True
return False return False
@synchronize @synchronize
def _on_close_memory_file(self, open_file, path, value): def _on_close_memory_file(self, open_file, path, value):
dir_entry = self._get_dir_entry(path) dir_entry = self._get_dir_entry(path)
...@@ -475,7 +474,6 @@ class MemoryFS(FS): ...@@ -475,7 +474,6 @@ class MemoryFS(FS):
dir_entry = self._get_dir_entry(path) dir_entry = self._get_dir_entry(path)
dir_entry.modified_time = datetime.datetime.now() dir_entry.modified_time = datetime.datetime.now()
@synchronize @synchronize
def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
dir_entry = self._get_dir_entry(path) dir_entry = self._get_dir_entry(path)
......
"""
Test the walk function and related code
"""
import unittest
from fs.memoryfs import MemoryFS
class TestWalk(unittest.TestCase):
def setUp(self):
self.fs = MemoryFS()
self.fs.createfile('a.txt', 'hello')
self.fs.createfile('b.txt', 'world')
self.fs.makeopendir('foo').createfile('c', '123')
self.fs.makeopendir('.svn').createfile('ignored')
def test_wildcard(self):
for dir_path, paths in self.fs.walk(wildcard='*.txt'):
for path in paths:
self.assert_(path.endswith('.txt'))
for dir_path, paths in self.fs.walk(wildcard=lambda fn:fn.endswith('.txt')):
for path in paths:
self.assert_(path.endswith('.txt'))
def test_dir_wildcard(self):
for dir_path, paths in self.fs.walk(dir_wildcard=lambda fn:not fn.endswith('.svn')):
for path in paths:
self.assert_('.svn' not in path)
\ No newline at end of file
...@@ -174,5 +174,5 @@ class TestZipFSErrors(unittest.TestCase): ...@@ -174,5 +174,5 @@ class TestZipFSErrors(unittest.TestCase):
def test_missing_zipfile(self): def test_missing_zipfile(self):
missingzip = os.path.join(self.workdir,"missing.zip") missingzip = os.path.join(self.workdir,"missing.zip")
self.assertRaises(zipfs.ZipMissingError,zipfs.ZipFS,missingzip) self.assertRaises(zipfs.ZipNotFoundError,zipfs.ZipFS,missingzip)
...@@ -15,8 +15,9 @@ standard unix shell functionality of hiding dot-files in directory listings. ...@@ -15,8 +15,9 @@ standard unix shell functionality of hiding dot-files in directory listings.
""" """
import re
import sys import sys
from fnmatch import fnmatch import fnmatch
from fs.base import FS, threading, synchronize from fs.base import FS, threading, synchronize
from fs.errors import * from fs.errors import *
...@@ -146,17 +147,22 @@ class WrapFS(FS): ...@@ -146,17 +147,22 @@ class WrapFS(FS):
@rewrite_errors @rewrite_errors
def listdir(self, path="", **kwds): def listdir(self, path="", **kwds):
wildcard = kwds.pop("wildcard","*") wildcard = kwds.pop("wildcard","*")
if wildcard is None:
wildcard = lambda fn:True
elif not callable(wildcard):
wildcard_re = re.compile(fnmatch.translate(wildcard))
wildcard = lambda fn:bool (wildcard_re.match(fn))
info = kwds.get("info",False) info = kwds.get("info",False)
entries = [] entries = []
for e in self.wrapped_fs.listdir(self._encode(path),**kwds): for e in self.wrapped_fs.listdir(self._encode(path),**kwds):
if info: if info:
e = e.copy() e = e.copy()
e["name"] = self._decode(e["name"]) e["name"] = self._decode(e["name"])
if wildcard is not None and not fnmatch(e["name"],wildcard): if not wildcard(e["name"]):
continue continue
else: else:
e = self._decode(e) e = self._decode(e)
if wildcard is not None and not fnmatch(e,wildcard): if not wildcard(e):
continue continue
entries.append(e) entries.append(e)
return entries return entries
......
...@@ -16,7 +16,6 @@ except ImportError: ...@@ -16,7 +16,6 @@ except ImportError:
from fs.base import FS from fs.base import FS
from fs.wrapfs import WrapFS from fs.wrapfs import WrapFS
from fs.path import *
class LazyFS(WrapFS): class LazyFS(WrapFS):
......
...@@ -26,7 +26,7 @@ class ZipOpenError(CreateFailedError): ...@@ -26,7 +26,7 @@ class ZipOpenError(CreateFailedError):
pass pass
class ZipMissingError(CreateFailedError): class ZipNotFoundError(CreateFailedError):
"""Thrown when the requested zip file does not exist""" """Thrown when the requested zip file does not exist"""
pass pass
...@@ -80,7 +80,7 @@ class ZipFS(FS): ...@@ -80,7 +80,7 @@ class ZipFS(FS):
:param encoding: -- The encoding to use for unicode filenames :param encoding: -- The encoding to use for unicode filenames
:param thread_synchronize: -- Set to True (default) to enable thread-safety :param thread_synchronize: -- Set to True (default) to enable thread-safety
:raises ZipOpenError: Thrown when the zip file could not be opened :raises ZipOpenError: Thrown when the zip file could not be opened
:raises ZipMissingError: Thrown when the zip file does not exist (derived from ZipOpenError) :raises ZipNotFoundError: Thrown when the zip file does not exist (derived from ZipOpenError)
""" """
super(ZipFS, self).__init__(thread_synchronize=thread_synchronize) super(ZipFS, self).__init__(thread_synchronize=thread_synchronize)
...@@ -105,7 +105,7 @@ class ZipFS(FS): ...@@ -105,7 +105,7 @@ class ZipFS(FS):
if str(ioe).startswith('[Errno 22] Invalid argument'): if str(ioe).startswith('[Errno 22] Invalid argument'):
raise ZipOpenError("Not a zip file or corrupt (%s)" % str(zip_file), raise ZipOpenError("Not a zip file or corrupt (%s)" % str(zip_file),
details=bzf) details=bzf)
raise ZipMissingError("Zip file not found (%s)" % str(zip_file), raise ZipNotFoundError("Zip file not found (%s)" % str(zip_file),
details=ioe) details=ioe)
self.zip_path = str(zip_file) self.zip_path = str(zip_file)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment