summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--docs/expose.rst4
-rw-r--r--docs/expose/importhook.rst2
-rw-r--r--docs/expose/index.rst1
-rw-r--r--fs/contrib/davfs/__init__.py2
-rw-r--r--fs/expose/importhook.py238
-rw-r--r--fs/opener.py3
-rw-r--r--fs/tests/test_importhook.py141
8 files changed, 390 insertions, 2 deletions
diff --git a/ChangeLog b/ChangeLog
index 44d58e8..bcb9913 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -39,6 +39,7 @@
* TahoeFS: access files stored in a Tahoe-LAFS grid
* New fs.expose implementations:
* dokan: mount an FS object as a drive using Dokan (win32-only)
+ * importhook: import modules from files in an FS object
* Modified listdir and walk methods to accept callables as well as strings
for wildcards.
* Added listdirinfo method, which yields both the entry names and the
diff --git a/docs/expose.rst b/docs/expose.rst
index e4ee403..0e50274 100644
--- a/docs/expose.rst
+++ b/docs/expose.rst
@@ -20,6 +20,10 @@ XMLRPC
------
Makes an FS object available over XMLRPC. See :mod:`fs.expose.xmlrpc`
+Import Hook
+-----------
+Allows importing python modules from the files in an FS object. See :mod:`fs.expose.importhook`
+
Django Storage
--------------
Connects FS objects to Django. See :mod:`fs.expose.django_storage`
diff --git a/docs/expose/importhook.rst b/docs/expose/importhook.rst
new file mode 100644
index 0000000..dd30115
--- /dev/null
+++ b/docs/expose/importhook.rst
@@ -0,0 +1,2 @@
+.. automodule:: fs.expose.importhook
+ :members:
diff --git a/docs/expose/index.rst b/docs/expose/index.rst
index 19ee319..6a185e5 100644
--- a/docs/expose/index.rst
+++ b/docs/expose/index.rst
@@ -10,4 +10,5 @@ The ``fs.expose`` module contains a number of options for making an FS implement
dokan.rst
sftp.rst
xmlrpc.rst
+ importhook.rst
django_storage.rst
diff --git a/fs/contrib/davfs/__init__.py b/fs/contrib/davfs/__init__.py
index 5190807..b6e291e 100644
--- a/fs/contrib/davfs/__init__.py
+++ b/fs/contrib/davfs/__init__.py
@@ -254,7 +254,7 @@ class DAVFS(FS):
resp = con.getresponse()
self._cookiejar.extract_cookies(FakeResp(resp),FakeReq(con,url.scheme,url.path))
except Exception, e:
- #logger.debug("DAVFS <ERR %s %s/%s",resp.status,method,url.hostname,url.path)
+ #logger.debug("DAVFS <ERR %s %s/%s",e,method,url.hostname,url.path)
self._del_connection(con)
raise
else:
diff --git a/fs/expose/importhook.py b/fs/expose/importhook.py
new file mode 100644
index 0000000..0cd0413
--- /dev/null
+++ b/fs/expose/importhook.py
@@ -0,0 +1,238 @@
+"""
+fs.expose.importhook
+====================
+
+Expose an FS object to the python import machinery, via a PEP-302 loader.
+
+This module allows you to import python modules from an arbitrary FS object,
+by placing FS urls on sys.path and/or inserting objects into sys.meta_path.
+
+The main class in this module is FSImportHook, which is a PEP-302-compliant
+module finder and loader. If you place an instance of FSImportHook on
+sys.meta_path, you will be able to import modules from the exposed filesystem::
+
+ >>> from fs.memoryfs import MemoryFS
+ >>> m = MemoryFS()
+ >>> m.setcontents("helloworld.py","print 'hello world!")
+ >>>
+ >>> import sys
+ >>> from fs.expose.importhook import FSImportHook
+ >>> sys.meta_path.append(FSImportHook(m))
+ >>> import helloworld
+ hello world!
+
+It is also possible to install FSImportHook as an import path handler. This
+allows you to place filesystem URLs on sys.path and have them automagically
+opened for importing. This example would allow modules to be imported from
+an SFTP server::
+
+ >>> from fs.expose.importhook import FSImportHook
+ >>> FSImportHook.install()
+ >>> sys.path.append("sftp://some.remote.machine/mypath/")
+
+"""
+
+import sys
+import imp
+import marshal
+
+from fs.base import FS
+from fs.opener import fsopendir, OpenerError
+from fs.errors import *
+from fs.path import *
+
+class FSImportHook(object):
+ """PEP-302-compliant module finder and loader for FS objects.
+
+ FSImportHook is a module finder and loader that takes its data from an
+ arbitrary FS object. The FS must have .py or .pyc files stored in the
+ standard module structure.
+
+ For easy use with sys.path, FSImportHook will also accept a filesystem
+ URL, which is automatically opened using fs.opener.
+ """
+
+ _VALID_MODULE_TYPES = set((imp.PY_SOURCE,imp.PY_COMPILED))
+
+ def __init__(self,fs_or_url):
+ # If given a string, try to open it as an FS url.
+ # Don't open things on the local filesystem though.
+ if isinstance(fs_or_url,basestring):
+ if ":" not in fs_or_url:
+ raise ImportError
+ try:
+ self.fs = fsopendir(fs_or_url)
+ except OpenerError:
+ raise ImportError
+ self.path = fs_or_url
+ # Otherwise, it must be an FS object of some sort.
+ else:
+ if not isinstance(fs_or_url,FS):
+ raise ImportError
+ self.fs = fs_or_url
+ self.path = None
+
+ @classmethod
+ def install(cls):
+ """Install this class into the import machinery.
+
+ This classmethod installs the custom FSImportHook class into the
+ import machinery of the running process, if it is not already
+ installed.
+ """
+ for i,imp in enumerate(sys.path_hooks):
+ try:
+ if issubclass(cls,imp):
+ break
+ except TypeError:
+ pass
+ else:
+ sys.path_hooks.append(cls)
+ sys.path_importer_cache.clear()
+
+ @classmethod
+ def uninstall(cls):
+ """Uninstall this class from the import machinery.
+
+ This classmethod uninstalls the custom FSImportHook class from the
+ import machinery of the running process.
+ """
+ to_rem = []
+ for i,imp in enumerate(sys.path_hooks):
+ try:
+ if issubclass(cls,imp):
+ to_rem.append(imp)
+ break
+ except TypeError:
+ pass
+ for imp in to_rem:
+ sys.path_hooks.remove(imp)
+ sys.path_importer_cache.clear()
+
+ def find_module(self,fullname,path=None):
+ """Find the FS loader for the given module.
+
+ This object is always its own loader, so this really just checks
+ whether it's a valid module on the exposed filesystem.
+ """
+ try:
+ self._get_module_info(fullname)
+ except ImportError:
+ return None
+ else:
+ return self
+
+ def _get_module_info(self,fullname):
+ """Get basic information about the given module.
+
+ If the specified module exists, this method returns a tuple giving
+ its filepath, file type and whether it's a package. Otherwise,
+ it raise ImportError.
+ """
+ prefix = fullname.replace(".","/")
+ # Is it a regular module?
+ (path,type) = self._find_module_file(prefix)
+ if path is not None:
+ return (path,type,False)
+ # Is it a package?
+ prefix = pathjoin(prefix,"__init__")
+ (path,type) = self._find_module_file(prefix)
+ if path is not None:
+ return (path,type,True)
+ # No, it's nothing
+ raise ImportError(fullname)
+
+ def _find_module_file(self,prefix):
+ """Find a module file from the given path prefix.
+
+ This method iterates over the possible module suffixes, checking each
+ in turn and returning the first match found. It returns a two-tuple
+ (path,type) or (None,None) if there's no module.
+ """
+ for (suffix,mode,type) in imp.get_suffixes():
+ if type in self._VALID_MODULE_TYPES:
+ path = prefix + suffix
+ if self.fs.isfile(path):
+ return (path,type)
+ return (None,None)
+
+ def load_module(self,fullname):
+ """Load the specified module.
+
+ This method locates the file for the specified module, loads and
+ executes it and returns the created module object.
+ """
+ # Reuse an existing module if present.
+ try:
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ # Try to create from source or bytecode.
+ info = self._get_module_info(fullname)
+ code = self.get_code(fullname,info)
+ if code is None:
+ raise ImportError(fullname)
+ mod = imp.new_module(fullname)
+ mod.__file__ = "<loading>"
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ try:
+ exec code in mod.__dict__
+ mod.__file__ = self.get_filename(fullname,info)
+ if self.is_package(fullname,info):
+ if self.path is None:
+ mod.__path__ = []
+ else:
+ mod.__path__ = [self.path]
+ return mod
+ except Exception:
+ sys.modules.pop(fullname,None)
+ raise
+
+ def is_package(self,fullname,info=None):
+ """Check whether the specified module is a package."""
+ if info is None:
+ info = self._get_module_info(fullname)
+ (path,type,ispkg) = info
+ return ispkg
+
+ def get_code(self,path,info=None):
+ """Get the bytecode for the specified module."""
+ if info is None:
+ info = self._get_module_info(fullname)
+ (path,type,ispkg) = info
+ code = self.fs.getcontents(path)
+ if type == imp.PY_SOURCE:
+ code = code.replace("\r\n","\n")
+ return compile(code,path,"exec")
+ elif type == imp.PY_COMPILED:
+ if code[:4] != imp.get_magic():
+ return None
+ return marshal.loads(code[8:])
+ else:
+ return None
+ return code
+
+ def get_source(self,fullname,info=None):
+ """Get the sourcecode for the specified module, if present."""
+ if info is None:
+ info = self._get_module_info(fullname)
+ (path,type,ispkg) = info
+ if type != imp.PY_SOURCE:
+ return None
+ return self.fs.getcontents(path).replace("\r\n","\n")
+
+ def get_data(self,path):
+ """Read the specified data file."""
+ try:
+ return self.fs.getcontents(path)
+ except FSError, e:
+ raise IOError(str(e))
+
+ def get_filename(self,fullname,info=None):
+ """Get the __file__ attribute for the specified module."""
+ if info is None:
+ info = self._get_module_info(fullname)
+ (path,type,ispkg) = info
+ return path
+
diff --git a/fs/opener.py b/fs/opener.py
index 0bb7b5c..560c581 100644
--- a/fs/opener.py
+++ b/fs/opener.py
@@ -538,7 +538,8 @@ example:
@classmethod
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create_dir):
from fs.tempfs import TempFS
- fs = TempFS(identifier=fs_name_params)
+ from fs.wrapfs.lazyfs import LazyFS
+ fs = LazyFS((TempFS,(),{"identifier":fs_name_params}))
return fs, fs_path
class S3Opener(Opener):
diff --git a/fs/tests/test_importhook.py b/fs/tests/test_importhook.py
new file mode 100644
index 0000000..86659b2
--- /dev/null
+++ b/fs/tests/test_importhook.py
@@ -0,0 +1,141 @@
+
+import sys
+import unittest
+import marshal
+import imp
+import struct
+from textwrap import dedent
+
+from fs.expose.importhook import FSImportHook
+from fs.tempfs import TempFS
+from fs.zipfs import ZipFS
+
+
+class TestFSImportHook(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ for mph in list(sys.meta_path):
+ if isinstance(mph,FSImportHook):
+ sys.meta_path.remove(mph)
+ for ph in list(sys.path_hooks):
+ if issubclass(ph,FSImportHook):
+ sys.path_hooks.remove(mph)
+ for (k,v) in sys.modules.items():
+ if k.startswith("fsih_"):
+ del sys.modules[k]
+ elif hasattr(v,"__loader__"):
+ if isinstance(v.__loader__,FSImportHook):
+ del sys.modules[k]
+ sys.path_importer_cache.clear()
+
+ def _init_modules(self,fs):
+ fs.setcontents("fsih_hello.py",dedent("""
+ message = 'hello world!'
+ """))
+ fs.makedir("fsih_pkg")
+ fs.setcontents("fsih_pkg/__init__.py",dedent("""
+ a = 42
+ """))
+ fs.setcontents("fsih_pkg/sub1.py",dedent("""
+ import fsih_pkg
+ from fsih_hello import message
+ a = fsih_pkg.a
+ """))
+ fs.setcontents("fsih_pkg/sub2.pyc",self._getpyc(dedent("""
+ import fsih_pkg
+ from fsih_hello import message
+ a = fsih_pkg.a * 2
+ """)))
+
+ def _getpyc(self,src):
+ """Get the .pyc contents to match th given .py source code."""
+ code = imp.get_magic() + struct.pack("<i",0)
+ code += marshal.dumps(compile(src,__file__,"exec"))
+ return code
+
+ def test_loader_methods(self):
+ t = TempFS()
+ self._init_modules(t)
+ ih = FSImportHook(t)
+ sys.meta_path.append(ih)
+ try:
+ self.assertEquals(ih.find_module("fsih_hello"),ih)
+ self.assertEquals(ih.find_module("fsih_helo"),None)
+ self.assertEquals(ih.find_module("fsih_pkg"),ih)
+ self.assertEquals(ih.find_module("fsih_pkg.sub1"),ih)
+ self.assertEquals(ih.find_module("fsih_pkg.sub2"),ih)
+ self.assertEquals(ih.find_module("fsih_pkg.sub3"),None)
+ m = ih.load_module("fsih_hello")
+ self.assertEquals(m.message,"hello world!")
+ self.assertRaises(ImportError,ih.load_module,"fsih_helo")
+ m = ih.load_module("fsih_pkg.sub1")
+ self.assertEquals(m.message,"hello world!")
+ self.assertEquals(m.a,42)
+ m = ih.load_module("fsih_pkg.sub2")
+ self.assertEquals(m.message,"hello world!")
+ self.assertEquals(m.a,42 * 2)
+ self.assertRaises(ImportError,ih.load_module,"fsih_pkg.sub3")
+ finally:
+ sys.meta_path.remove(ih)
+ t.close()
+
+ def _check_imports_are_working(self):
+ try:
+ import fsih_hello
+ self.assertEquals(fsih_hello.message,"hello world!")
+ try:
+ import fsih_helo
+ except ImportError:
+ pass
+ else:
+ assert False, "ImportError not raised"
+ import fsih_pkg
+ import fsih_pkg.sub1
+ self.assertEquals(fsih_pkg.sub1.message,"hello world!")
+ self.assertEquals(fsih_pkg.sub1.a,42)
+ import fsih_pkg.sub2
+ self.assertEquals(fsih_pkg.sub2.message,"hello world!")
+ self.assertEquals(fsih_pkg.sub2.a,42 * 2)
+ try:
+ import fsih_pkg.sub3
+ except ImportError:
+ pass
+ else:
+ assert False, "ImportError not raised"
+ finally:
+ for k in sys.modules.keys():
+ if k.startswith("fsih_"):
+ del sys.modules[k]
+
+ def test_importer_on_meta_path(self):
+ t = TempFS()
+ self._init_modules(t)
+ ih = FSImportHook(t)
+ sys.meta_path.append(ih)
+ try:
+ self._check_imports_are_working()
+ finally:
+ sys.meta_path.remove(ih)
+ t.close()
+
+ def test_url_on_sys_path(self):
+ t = TempFS()
+ zpath = t.getsyspath("modules.zip")
+ z = ZipFS(zpath,"w")
+ self._init_modules(z)
+ z.close()
+ z = ZipFS(zpath,"r")
+ assert z.isfile("fsih_hello.py")
+ z.close()
+ sys.path.append("zip://" + zpath)
+ FSImportHook.install()
+ try:
+ self._check_imports_are_working()
+ finally:
+ sys.path_hooks.remove(FSImportHook)
+ sys.path.pop()
+ t.close()
+