Commit 838d94c3 by willmcgugan

Fixed getmeta/hasmeta, added documentation and tests

parent e4fe27c2
...@@ -15,7 +15,8 @@ __all__ = ['DummyLock', ...@@ -15,7 +15,8 @@ __all__ = ['DummyLock',
'NullFile', 'NullFile',
'synchronize', 'synchronize',
'FS', 'FS',
'flags_to_mode'] 'flags_to_mode',
'NoDefaultMeta']
import os, os.path import os, os.path
import sys import sys
...@@ -33,9 +34,6 @@ from fs.path import * ...@@ -33,9 +34,6 @@ from fs.path import *
from fs.errors import * from fs.errors import *
from fs.local_functools import wraps from fs.local_functools import wraps
SubFS = None # this is lazily imported from fs.wrapfs.subfs
class DummyLock(object): class DummyLock(object):
"""A dummy lock object that doesn't do anything. """A dummy lock object that doesn't do anything.
...@@ -68,6 +66,11 @@ def silence_fserrors(f, *args, **kwargs): ...@@ -68,6 +66,11 @@ def silence_fserrors(f, *args, **kwargs):
return None return None
class NoDefaultMeta(object):
"""A singleton used to signify that there is no default for getmeta"""
pass
class NullFile(object): class NullFile(object):
"""A NullFile is a file object that has no functionality. """A NullFile is a file object that has no functionality.
...@@ -188,8 +191,33 @@ class FS(object): ...@@ -188,8 +191,33 @@ class FS(object):
else: else:
self._lock = DummyLock() self._lock = DummyLock()
def getmeta(self, meta_name, default=Ellipsis): def getmeta(self, meta_name, default=NoDefaultMeta):
"""Retrieve a meta value associated with the FS object """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 meta_name: The name of the meta value to retrieve
:param default: An option default to return, if the meta value isn't present :param default: An option default to return, if the meta value isn't present
...@@ -197,7 +225,7 @@ class FS(object): ...@@ -197,7 +225,7 @@ class FS(object):
""" """
if meta_name not in self._meta: if meta_name not in self._meta:
if default is not Ellipsis: if default is not NoDefaultMeta:
return default return default
raise NoMetaError(meta_name=meta_name) raise NoMetaError(meta_name=meta_name)
return self._meta[meta_name] return self._meta[meta_name]
...@@ -210,7 +238,7 @@ class FS(object): ...@@ -210,7 +238,7 @@ class FS(object):
""" """
try: try:
self.getmeta('meta_name') self.getmeta(meta_name)
except NoMetaError: except NoMetaError:
return False return False
return True return True
...@@ -512,7 +540,16 @@ class FS(object): ...@@ -512,7 +540,16 @@ class FS(object):
def getinfo(self, path): def getinfo(self, path):
"""Returns information for a path as a dictionary. The exact content of """Returns information for a path as a dictionary. The exact content of
this dictionary will vary depending on the implementation, but will 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 :param path: a path to retrieve information for
:rtype: dict :rtype: dict
...@@ -521,7 +558,7 @@ class FS(object): ...@@ -521,7 +558,7 @@ class FS(object):
def desc(self, path): def desc(self, path):
"""Returns short descriptive text regarding a path. Intended mainly as """Returns short descriptive text regarding a path. Intended mainly as
a debugging aid a debugging aid.
:param path: A path to describe :param path: A path to describe
:rtype: str :rtype: str
...@@ -583,8 +620,7 @@ class FS(object): ...@@ -583,8 +620,7 @@ class FS(object):
:param path: path to directory to open :param path: path to directory to open
:rtype: An FS object :rtype: An FS object
""" """
global SubFS
if SubFS is None:
from fs.wrapfs.subfs import SubFS from fs.wrapfs.subfs import SubFS
if not self.exists(path): if not self.exists(path):
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
......
...@@ -161,6 +161,7 @@ class BigFS(FS): ...@@ -161,6 +161,7 @@ class BigFS(FS):
'read_only' : True, 'read_only' : True,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, 'case_insensitive_paths' : False,
'network' : False,
} }
def __init__(self, filename, mode="r", thread_synchronize=True): def __init__(self, filename, mode="r", thread_synchronize=True):
......
...@@ -77,6 +77,7 @@ class DAVFS(FS): ...@@ -77,6 +77,7 @@ class DAVFS(FS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, 'case_insensitive_paths' : False,
'network' : True
} }
def __init__(self,url,credentials=None,get_credentials=None,thread_synchronize=True,connection_classes=None,timeout=None): def __init__(self,url,credentials=None,get_credentials=None,thread_synchronize=True,connection_classes=None,timeout=None):
......
...@@ -80,7 +80,7 @@ class TahoeFS(CacheFS): ...@@ -80,7 +80,7 @@ class TahoeFS(CacheFS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, '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'): 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): ...@@ -737,11 +737,14 @@ class FTPFS(FS):
_locals = threading.local() _locals = threading.local()
_meta = { 'virtual': False, _meta = { 'network' : True,
'virtual': False,
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, '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, def __init__(self, host='', user='', passwd='', acct='', timeout=_GLOBAL_DEFAULT_TIMEOUT,
......
...@@ -184,10 +184,14 @@ class MemoryFS(FS): ...@@ -184,10 +184,14 @@ class MemoryFS(FS):
""" """
_meta = { 'virtual': False, _meta = { 'network' : False,
'virtual': False,
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, '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): def _make_dir_entry(self, *args, **kwargs):
......
...@@ -72,7 +72,7 @@ class MountFS(FS): ...@@ -72,7 +72,7 @@ class MountFS(FS):
_meta = { 'virtual': True, _meta = { 'virtual': True,
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False 'case_insensitive_paths' : False,
} }
DirMount = DirMount DirMount = DirMount
......
...@@ -18,6 +18,7 @@ import os.path ...@@ -18,6 +18,7 @@ import os.path
import sys import sys
import errno import errno
import datetime import datetime
import platform
from fs.base import * from fs.base import *
from fs.errors import * from fs.errors import *
...@@ -73,10 +74,14 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS): ...@@ -73,10 +74,14 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
methods in the os and os.path modules. methods in the os and os.path modules.
""" """
_meta = { 'virtual' : False, _meta = { 'network' : False,
'virtual' : False,
'read_only' : False, 'read_only' : False,
'unicode_paths' : os.path.supports_unicode_filenames, 'unicode_paths' : os.path.supports_unicode_filenames,
'case_insensitive_paths' : os.path.normcase('Aa') == 'aa', '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): 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): ...@@ -155,6 +160,26 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
raise ValueError("path not within this FS: %s (%s)" % (os.path.normcase(path),prefix)) raise ValueError("path not within this FS: %s (%s)" % (os.path.normcase(path),prefix))
return path[len(self.root_path):] 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 @convert_os_errors
def open(self, path, mode="r", **kwargs): def open(self, path, mode="r", **kwargs):
mode = filter(lambda c: c in "rwabt+",mode) mode = filter(lambda c: c in "rwabt+",mode)
......
...@@ -89,10 +89,7 @@ class RPCFS(FS): ...@@ -89,10 +89,7 @@ class RPCFS(FS):
""" """
_meta = { 'virtual': False, _meta = { 'virtual': False,
'read_only' : False, 'network' : True,
'unicode_paths' : True,
'case_insensitive_paths' : False,
'may_block' : True,
} }
def __init__(self, uri, transport=None): def __init__(self, uri, transport=None):
...@@ -147,6 +144,16 @@ class RPCFS(FS): ...@@ -147,6 +144,16 @@ class RPCFS(FS):
"""Decode paths arriving over the wire.""" """Decode paths arriving over the wire."""
return path.decode("base64").decode("utf8") 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"): def open(self, path, mode="r"):
# TODO: chunked transport of large files # TODO: chunked transport of large files
path = self.encode_path(path) path = self.encode_path(path)
......
...@@ -60,7 +60,10 @@ class S3FS(FS): ...@@ -60,7 +60,10 @@ class S3FS(FS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, 'case_insensitive_paths' : False,
'may_block' : True, 'network' : True,
'atomic.makedir' : True,
'atomic.rename' : False,
'atomic.setconetns' : True
} }
class meta: class meta:
......
...@@ -50,7 +50,10 @@ class SFTPFS(FS): ...@@ -50,7 +50,10 @@ class SFTPFS(FS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, '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): def __init__(self, connection, root_path="/", encoding=None, **credentials):
......
...@@ -24,6 +24,10 @@ class TempFS(OSFS): ...@@ -24,6 +24,10 @@ class TempFS(OSFS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : os.path.supports_unicode_filenames, 'unicode_paths' : os.path.supports_unicode_filenames,
'case_insensitive_paths' : os.path.normcase('Aa') == 'aa', '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): def __init__(self, identifier=None, temp_dir=None, dir_mode=0700, thread_synchronize=_thread_synchronize_default):
......
...@@ -48,6 +48,26 @@ class FSTestCases(object): ...@@ -48,6 +48,26 @@ class FSTestCases(object):
"""Check that a file exists within self.fs""" """Check that a file exists within self.fs"""
return self.fs.exists(p) 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): def test_root_dir(self):
self.assertTrue(self.fs.isdir("")) self.assertTrue(self.fs.isdir(""))
self.assertTrue(self.fs.isdir("/")) self.assertTrue(self.fs.isdir("/"))
......
...@@ -19,7 +19,7 @@ import re ...@@ -19,7 +19,7 @@ import re
import sys import sys
import fnmatch import fnmatch
from fs.base import FS, threading, synchronize from fs.base import FS, threading, synchronize, NoDefaultMeta
from fs.errors import * from fs.errors import *
from fs.path import * from fs.path import *
from fs.local_functools import wraps from fs.local_functools import wraps
...@@ -118,7 +118,7 @@ class WrapFS(FS): ...@@ -118,7 +118,7 @@ class WrapFS(FS):
return (mode,mode) return (mode,mode)
@rewrite_errors @rewrite_errors
def getmeta(self, meta_name, default=Ellipsis): def getmeta(self, meta_name, default=NoDefaultMeta):
return self.wrapped_fs.getmeta(meta_name, default) return self.wrapped_fs.getmeta(meta_name, default)
@rewrite_errors @rewrite_errors
......
...@@ -6,6 +6,7 @@ An FS wrapper class for blocking operations that would modify the FS. ...@@ -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.wrapfs import WrapFS
from fs.errors import UnsupportedError, NoSysPathError from fs.errors import UnsupportedError, NoSysPathError
...@@ -20,7 +21,7 @@ class ReadOnlyFS(WrapFS): ...@@ -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': if meta_name == 'read_only':
return True return True
return self.wrapped_fs(meta_name, default) return self.wrapped_fs(meta_name, default)
......
...@@ -76,6 +76,8 @@ class ZipFS(FS): ...@@ -76,6 +76,8 @@ class ZipFS(FS):
'read_only' : False, 'read_only' : False,
'unicode_paths' : True, 'unicode_paths' : True,
'case_insensitive_paths' : False, '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): 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): ...@@ -149,7 +151,7 @@ class ZipFS(FS):
f = self._path_fs.open(path, 'w') f = self._path_fs.open(path, 'w')
f.close() f.close()
def getmeta(self, meta_name, default=Ellipsis): def getmeta(self, meta_name, default=NoDefaultMeta):
if meta_name == 'read_only': if meta_name == 'read_only':
return self.read_only return self.read_only
return super(ZipFS, self).getmeta(meta_name, default) 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