Commit 838d94c3 by willmcgugan

Fixed getmeta/hasmeta, added documentation and tests

parent e4fe27c2
......@@ -15,7 +15,8 @@ __all__ = ['DummyLock',
'NullFile',
'synchronize',
'FS',
'flags_to_mode']
'flags_to_mode',
'NoDefaultMeta']
import os, os.path
import sys
......@@ -33,9 +34,6 @@ from fs.path import *
from fs.errors import *
from fs.local_functools import wraps
SubFS = None # this is lazily imported from fs.wrapfs.subfs
class DummyLock(object):
"""A dummy lock object that doesn't do anything.
......@@ -68,6 +66,11 @@ def silence_fserrors(f, *args, **kwargs):
return None
class NoDefaultMeta(object):
"""A singleton used to signify that there is no default for getmeta"""
pass
class NullFile(object):
"""A NullFile is a file object that has no functionality.
......@@ -188,8 +191,33 @@ class FS(object):
else:
self._lock = DummyLock()
def getmeta(self, meta_name, default=Ellipsis):
"""Retrieve a meta value associated with the FS object
def getmeta(self, meta_name, default=NoDefaultMeta):
"""Retrieve a meta value associated with an FS object. Meta values are
a way of an FS implementation to report potentially useful information
associated with the file system.
A meta key is a lower case string with no spaces. Meta keys may also
be grouped in namespaces in a dotted notation, e.g. 'atomic.namespaces'.
FS implementations aren't obliged to return any meta values, but the
following are common:
* *read_only* True if the file system can not be modified
* *network* True if the file system requires network access
* *unicode_paths* True if the file system can use unicode paths
* *case_insensitive_paths* True if the file system ignores the case of paths
* *atomic.makedir* True if making a directory is an atomic operation
* *atomic.rename" True if rename is an atomic operation, (and not implemented as a copy followed by a delete)
* *atomic.setcontents" True if the implementation supports setting the contents of a file as an atomic operation (without opening a file)
The following are less common:
* *free_space* The free space (in bytes) available on the file system
FS implementations may expose non-generic meta data through a self-named namespace. e.g. 'somefs.some_meta'
Since no meta value is guaranteed to exist, it is advisable to always supply a
default value to `getmeta`.
:param meta_name: The name of the meta value to retrieve
:param default: An option default to return, if the meta value isn't present
......@@ -197,7 +225,7 @@ class FS(object):
"""
if meta_name not in self._meta:
if default is not Ellipsis:
if default is not NoDefaultMeta:
return default
raise NoMetaError(meta_name=meta_name)
return self._meta[meta_name]
......@@ -210,7 +238,7 @@ class FS(object):
"""
try:
self.getmeta('meta_name')
self.getmeta(meta_name)
except NoMetaError:
return False
return True
......@@ -512,7 +540,16 @@ class FS(object):
def getinfo(self, path):
"""Returns information for a path as a dictionary. The exact content of
this dictionary will vary depending on the implementation, but will
likely include a few common values.
likely include a few common values. The following values will be found
in info dictionaries for most implementations:
* "size" - Number of bytes used to store the file or directory
* "created_time" - A datetime object containing the time the resource
was created
* "accessed_time" - A datetime object containing the time the resource
was last accessed
* "modified_time" - A datetime object containing the time the resource
was modified
:param path: a path to retrieve information for
:rtype: dict
......@@ -521,7 +558,7 @@ class FS(object):
def desc(self, path):
"""Returns short descriptive text regarding a path. Intended mainly as
a debugging aid
a debugging aid.
:param path: A path to describe
:rtype: str
......@@ -583,8 +620,7 @@ class FS(object):
:param path: path to directory to open
:rtype: An FS object
"""
global SubFS
if SubFS is None:
from fs.wrapfs.subfs import SubFS
if not self.exists(path):
raise ResourceNotFoundError(path)
......
......@@ -161,6 +161,7 @@ class BigFS(FS):
'read_only' : True,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'network' : False,
}
def __init__(self, filename, mode="r", thread_synchronize=True):
......
......@@ -77,6 +77,7 @@ class DAVFS(FS):
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'network' : True
}
def __init__(self,url,credentials=None,get_credentials=None,thread_synchronize=True,connection_classes=None,timeout=None):
......
......@@ -80,7 +80,7 @@ class TahoeFS(CacheFS):
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True
'network' : True
}
def __init__(self, dircap, timeout=60, autorun=True, largefilesize=10*1024*1024, webapi='http://127.0.0.1:3456'):
......
......@@ -737,11 +737,14 @@ class FTPFS(FS):
_locals = threading.local()
_meta = { 'virtual': False,
_meta = { 'network' : True,
'virtual': False,
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True
'atomic.makedir' : True,
'atomic.rename' : True,
'atomic.setcontents' : False,
}
def __init__(self, host='', user='', passwd='', acct='', timeout=_GLOBAL_DEFAULT_TIMEOUT,
......
......@@ -184,10 +184,14 @@ class MemoryFS(FS):
"""
_meta = { 'virtual': False,
_meta = { 'network' : False,
'virtual': False,
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False
'case_insensitive_paths' : False,
'atomic.makedir' : True,
'atomic.rename' : True,
'atomic.setcontents' : False,
}
def _make_dir_entry(self, *args, **kwargs):
......
......@@ -72,7 +72,7 @@ class MountFS(FS):
_meta = { 'virtual': True,
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False
'case_insensitive_paths' : False,
}
DirMount = DirMount
......
......@@ -18,6 +18,7 @@ import os.path
import sys
import errno
import datetime
import platform
from fs.base import *
from fs.errors import *
......@@ -73,10 +74,14 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
methods in the os and os.path modules.
"""
_meta = { 'virtual' : False,
_meta = { 'network' : False,
'virtual' : False,
'read_only' : False,
'unicode_paths' : os.path.supports_unicode_filenames,
'case_insensitive_paths' : os.path.normcase('Aa') == 'aa',
'atomic.makedir' : True,
'atomic.rename' : True,
'atomic.setcontents' : False,
}
def __init__(self, root_path, thread_synchronize=_thread_synchronize_default, encoding=None, create=False, dir_mode=0700):
......@@ -155,6 +160,26 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
raise ValueError("path not within this FS: %s (%s)" % (os.path.normcase(path),prefix))
return path[len(self.root_path):]
def getmeta(self, meta_name, default=NoDefaultMeta):
if meta_name == 'free_space':
if platform.system() == 'Windows':
try:
import ctypes
free_bytes = ctypes.ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(self.root_path), None, None, ctypes.pointer(free_bytes))
return free_bytes.value
except ImportError:
# Fall through to call the base class
pass
else:
stat = os.statvfs(self.root_path)
return stat.f_bfree * stat.f_bsize
return super(OSFS, self).getmeta(meta_name, default)
@convert_os_errors
def open(self, path, mode="r", **kwargs):
mode = filter(lambda c: c in "rwabt+",mode)
......
......@@ -89,10 +89,7 @@ class RPCFS(FS):
"""
_meta = { 'virtual': False,
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True,
'network' : True,
}
def __init__(self, uri, transport=None):
......@@ -147,6 +144,16 @@ class RPCFS(FS):
"""Decode paths arriving over the wire."""
return path.decode("base64").decode("utf8")
def getmeta(self, meta_name, default=NoDefaultMeta):
if meta_name in self._meta:
return self._meta[meta_name]
return self.proxy.getmeta(meta_name, default)
def hasmeta(self, meta_name):
if meta_name in self._meta:
return True
return self.proxy.hasmeta(meta_name)
def open(self, path, mode="r"):
# TODO: chunked transport of large files
path = self.encode_path(path)
......
......@@ -60,7 +60,10 @@ class S3FS(FS):
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True,
'network' : True,
'atomic.makedir' : True,
'atomic.rename' : False,
'atomic.setconetns' : True
}
class meta:
......
......@@ -50,7 +50,10 @@ class SFTPFS(FS):
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True,
'network' : True,
'atomic.makedir' : True,
'atomic.rename' : True,
'atomic.setcontents' : False
}
def __init__(self, connection, root_path="/", encoding=None, **credentials):
......
......@@ -24,6 +24,10 @@ class TempFS(OSFS):
'read_only' : False,
'unicode_paths' : os.path.supports_unicode_filenames,
'case_insensitive_paths' : os.path.normcase('Aa') == 'aa',
'network' : False,
'atomic.makedir' : True,
'atomic.rename' : True,
'atomic.setcontents' : False
}
def __init__(self, identifier=None, temp_dir=None, dir_mode=0700, thread_synchronize=_thread_synchronize_default):
......
......@@ -48,6 +48,26 @@ class FSTestCases(object):
"""Check that a file exists within self.fs"""
return self.fs.exists(p)
def test_meta(self):
"""Checks getmeta / hasmeta are functioning"""
# getmeta / hasmeta are hard to test, since there is no way to validate
# the implementations response
meta_names = ["read_only",
"network",
"unicode_paths"]
stupid_meta = 'thismetashouldnotexist!"r$$%^&&*()_+'
self.assertRaises(NoMetaError, self.fs.getmeta, stupid_meta)
self.assertFalse(self.fs.hasmeta(stupid_meta))
self.assertEquals(None, self.fs.getmeta(stupid_meta, None))
self.assertEquals(3.14, self.fs.getmeta(stupid_meta, 3.14))
for meta_name in meta_names:
try:
meta = self.fs.getmeta(meta_name)
self.assertTrue(self.fs.hasmeta(meta_name))
except NoMetaError:
self.assertFalse(self.fs.hasmeta(meta_name))
def test_root_dir(self):
self.assertTrue(self.fs.isdir(""))
self.assertTrue(self.fs.isdir("/"))
......
......@@ -19,7 +19,7 @@ import re
import sys
import fnmatch
from fs.base import FS, threading, synchronize
from fs.base import FS, threading, synchronize, NoDefaultMeta
from fs.errors import *
from fs.path import *
from fs.local_functools import wraps
......@@ -118,7 +118,7 @@ class WrapFS(FS):
return (mode,mode)
@rewrite_errors
def getmeta(self, meta_name, default=Ellipsis):
def getmeta(self, meta_name, default=NoDefaultMeta):
return self.wrapped_fs.getmeta(meta_name, default)
@rewrite_errors
......
......@@ -6,6 +6,7 @@ An FS wrapper class for blocking operations that would modify the FS.
"""
from fs.base import NoDefaultMeta
from fs.wrapfs import WrapFS
from fs.errors import UnsupportedError, NoSysPathError
......@@ -20,7 +21,7 @@ class ReadOnlyFS(WrapFS):
"""
def getmeta(self, meta_name, default=Ellipsis):
def getmeta(self, meta_name, default=NoDefaultMeta):
if meta_name == 'read_only':
return True
return self.wrapped_fs(meta_name, default)
......
......@@ -76,6 +76,8 @@ class ZipFS(FS):
'read_only' : False,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'network' : False,
'atomic.setcontents' : False
}
def __init__(self, zip_file, mode="r", compression="deflated", allow_zip_64=False, encoding="CP437", thread_synchronize=True):
......@@ -149,7 +151,7 @@ class ZipFS(FS):
f = self._path_fs.open(path, 'w')
f.close()
def getmeta(self, meta_name, default=Ellipsis):
def getmeta(self, meta_name, default=NoDefaultMeta):
if meta_name == 'read_only':
return self.read_only
return super(ZipFS, self).getmeta(meta_name, default)
......
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