showbase: Rewrite VFSImporter for newer Python import system
Fixes #1718
This commit is contained in:
parent
e57c71d8e4
commit
bd4b3dd1a7
|
|
@ -5,477 +5,262 @@ Calling the :func:`register()` function to register the import hooks should be
|
|||
sufficient to enable this functionality.
|
||||
"""
|
||||
|
||||
__all__ = ['register', 'sharedPackages',
|
||||
'reloadSharedPackage', 'reloadSharedPackages']
|
||||
__all__ = ['register']
|
||||
|
||||
from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem, OFileStream, copyStream
|
||||
from direct.stdpy.file import open
|
||||
from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem
|
||||
from panda3d.core import OFileStream, copy_stream
|
||||
import sys
|
||||
import marshal
|
||||
import imp
|
||||
import types
|
||||
import _imp
|
||||
import atexit
|
||||
from importlib.abc import Loader, SourceLoader
|
||||
from importlib.util import MAGIC_NUMBER, decode_source
|
||||
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, BYTECODE_SUFFIXES
|
||||
|
||||
#: The sharedPackages dictionary lists all of the "shared packages",
|
||||
#: special Python packages that automatically span multiple directories
|
||||
#: via magic in the VFSImporter. You can make a package "shared"
|
||||
#: simply by adding its name into this dictionary (and then calling
|
||||
#: reloadSharedPackages() if it's already been imported).
|
||||
#:
|
||||
#: When a package name is in this dictionary at import time, *all*
|
||||
#: instances of the package are located along sys.path, and merged into
|
||||
#: a single Python module with a __path__ setting that represents the
|
||||
#: union. Thus, you can have a direct.showbase.foo in your own
|
||||
#: application, and loading it won't shadow the system
|
||||
#: direct.showbase.ShowBase which is in a different directory on disk.
|
||||
sharedPackages = {}
|
||||
|
||||
vfs = VirtualFileSystem.getGlobalPtr()
|
||||
|
||||
compiledExtensions = ['pyc', 'pyo']
|
||||
if not __debug__:
|
||||
# In optimized mode, we prefer loading .pyo files over .pyc files.
|
||||
# We implement that by reversing the extension names.
|
||||
compiledExtensions = ['pyo', 'pyc']
|
||||
vfs = VirtualFileSystem.get_global_ptr()
|
||||
|
||||
|
||||
class VFSImporter:
|
||||
def _make_spec(fullname, loader, *, is_package):
|
||||
filename = loader._vfile.get_filename()
|
||||
spec = ModuleSpec(fullname, loader, origin=filename.to_os_specific(), is_package=is_package)
|
||||
if is_package:
|
||||
spec.submodule_search_locations.append(Filename(filename.get_dirname()).to_os_specific())
|
||||
spec.has_location = True
|
||||
return spec
|
||||
|
||||
|
||||
class VFSFinder:
|
||||
""" This class serves as a Python importer to support loading
|
||||
Python .py and .pyc/.pyo files from Panda's Virtual File System,
|
||||
which allows loading Python source files from mounted .mf files
|
||||
(among other places). """
|
||||
|
||||
def __init__(self, path):
|
||||
if isinstance(path, Filename):
|
||||
self.dir_path = Filename(path)
|
||||
else:
|
||||
self.dir_path = Filename.fromOsSpecific(path)
|
||||
self.path = path
|
||||
|
||||
def find_module(self, fullname, path = None):
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if path is None:
|
||||
dir_path = self.dir_path
|
||||
else:
|
||||
dir_path = path
|
||||
#print >>sys.stderr, "find_module(%s), dir_path = %s" % (fullname, dir_path)
|
||||
path = self.path
|
||||
|
||||
#print(f"find_spec({fullname}), dir_path = {dir_path}", file=sys.stderr)
|
||||
basename = fullname.split('.')[-1]
|
||||
path = Filename(dir_path, basename)
|
||||
filename = Filename(Filename.from_os_specific(path), basename)
|
||||
|
||||
# First, look for Python files.
|
||||
filename = Filename(path)
|
||||
filename.setExtension('py')
|
||||
vfile = vfs.getFile(filename, True)
|
||||
vfile = vfs.get_file(filename + '.py', True)
|
||||
if vfile:
|
||||
return VFSLoader(dir_path, vfile, filename,
|
||||
desc=('.py', 'r', imp.PY_SOURCE))
|
||||
loader = VFSSourceLoader(fullname, vfile)
|
||||
return _make_spec(fullname, loader, is_package=False)
|
||||
|
||||
# If there's no .py file, but there's a .pyc file, load that
|
||||
# anyway.
|
||||
for ext in compiledExtensions:
|
||||
filename = Filename(path)
|
||||
filename.setExtension(ext)
|
||||
vfile = vfs.getFile(filename, True)
|
||||
for suffix in BYTECODE_SUFFIXES:
|
||||
vfile = vfs.get_file(filename + suffix, True)
|
||||
if vfile:
|
||||
return VFSLoader(dir_path, vfile, filename,
|
||||
desc=('.'+ext, 'rb', imp.PY_COMPILED))
|
||||
loader = VFSCompiledLoader(fullname, vfile)
|
||||
return _make_spec(fullname, loader, is_package=False)
|
||||
|
||||
# Look for a C/C++ extension module.
|
||||
for desc in imp.get_suffixes():
|
||||
if desc[2] != imp.C_EXTENSION:
|
||||
continue
|
||||
|
||||
filename = Filename(path + desc[0])
|
||||
vfile = vfs.getFile(filename, True)
|
||||
for suffix in EXTENSION_SUFFIXES:
|
||||
vfile = vfs.get_file(filename + suffix, True)
|
||||
if vfile:
|
||||
return VFSLoader(dir_path, vfile, filename, desc=desc)
|
||||
loader = VFSExtensionLoader(fullname, vfile)
|
||||
return _make_spec(fullname, loader, is_package=False)
|
||||
|
||||
# Finally, consider a package, i.e. a directory containing
|
||||
# __init__.py.
|
||||
filename = Filename(path, '__init__.py')
|
||||
vfile = vfs.getFile(filename, True)
|
||||
# Consider a package, i.e. a directory containing __init__.py.
|
||||
init_filename = Filename(filename, '__init__.py')
|
||||
vfile = vfs.get_file(init_filename, True)
|
||||
if vfile:
|
||||
return VFSLoader(dir_path, vfile, filename, packagePath=path,
|
||||
desc=('.py', 'r', imp.PY_SOURCE))
|
||||
for ext in compiledExtensions:
|
||||
filename = Filename(path, '__init__.' + ext)
|
||||
vfile = vfs.getFile(filename, True)
|
||||
if vfile:
|
||||
return VFSLoader(dir_path, vfile, filename, packagePath=path,
|
||||
desc=('.'+ext, 'rb', imp.PY_COMPILED))
|
||||
loader = VFSSourceLoader(fullname, vfile)
|
||||
return _make_spec(fullname, loader, is_package=True)
|
||||
|
||||
#print >>sys.stderr, "not found."
|
||||
for suffix in BYTECODE_SUFFIXES:
|
||||
init_filename = Filename(filename, '__init__' + suffix)
|
||||
vfile = vfs.get_file(init_filename, True)
|
||||
if vfile:
|
||||
loader = VFSCompiledLoader(fullname, vfile)
|
||||
return _make_spec(fullname, loader, is_package=True)
|
||||
|
||||
# Consider a namespace package.
|
||||
if vfs.is_directory(filename):
|
||||
spec = ModuleSpec(fullname, VFSNamespaceLoader(), is_package=True)
|
||||
spec.submodule_search_locations.append(filename.to_os_specific())
|
||||
return spec
|
||||
|
||||
#print("not found.", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
class VFSLoader:
|
||||
""" The second part of VFSImporter, this is created for a
|
||||
particular .py file or directory. """
|
||||
|
||||
def __init__(self, dir_path, vfile, filename, desc, packagePath=None):
|
||||
self.dir_path = dir_path
|
||||
self.timestamp = None
|
||||
if vfile:
|
||||
self.timestamp = vfile.getTimestamp()
|
||||
self.filename = filename
|
||||
self.desc = desc
|
||||
self.packagePath = packagePath
|
||||
|
||||
def load_module(self, fullname, loadingShared = False):
|
||||
#print >>sys.stderr, "load_module(%s), dir_path = %s, filename = %s" % (fullname, self.dir_path, self.filename)
|
||||
if self.desc[2] == imp.PY_FROZEN:
|
||||
return self._import_frozen_module(fullname)
|
||||
if self.desc[2] == imp.C_EXTENSION:
|
||||
return self._import_extension_module(fullname)
|
||||
|
||||
# Check if this is a child of a shared package.
|
||||
if not loadingShared and self.packagePath and '.' in fullname:
|
||||
parentname = fullname.rsplit('.', 1)[0]
|
||||
if parentname in sharedPackages:
|
||||
# It is. That means it's a shared package too.
|
||||
parent = sys.modules[parentname]
|
||||
path = getattr(parent, '__path__', None)
|
||||
importer = VFSSharedImporter()
|
||||
sharedPackages[fullname] = True
|
||||
loader = importer.find_module(fullname, path = path)
|
||||
assert loader
|
||||
return loader.load_module(fullname)
|
||||
|
||||
code = self._read_code()
|
||||
if not code:
|
||||
raise ImportError('No Python code in %s' % (fullname))
|
||||
|
||||
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
|
||||
mod.__file__ = self.filename.toOsSpecific()
|
||||
mod.__loader__ = self
|
||||
if self.packagePath:
|
||||
mod.__path__ = [self.packagePath.toOsSpecific()]
|
||||
#print >> sys.stderr, "loaded %s, path = %s" % (fullname, mod.__path__)
|
||||
|
||||
exec(code, mod.__dict__)
|
||||
return sys.modules[fullname]
|
||||
|
||||
def getdata(self, path):
|
||||
path = Filename(self.dir_path, Filename.fromOsSpecific(path))
|
||||
vfile = vfs.getFile(path)
|
||||
if not vfile:
|
||||
raise IOError("Could not find '%s'" % (path))
|
||||
return vfile.readFile(True)
|
||||
class VFSLoader(Loader):
|
||||
def __init__(self, fullname, vfile):
|
||||
self.name = fullname
|
||||
self._vfile = vfile
|
||||
|
||||
def is_package(self, fullname):
|
||||
return bool(self.packagePath)
|
||||
if fullname is not None and self.name != fullname:
|
||||
raise ImportError
|
||||
|
||||
def get_code(self, fullname):
|
||||
return self._read_code()
|
||||
filename = self._vfile.get_filename().get_basename()
|
||||
filename_base = filename.rsplit('.', 1)[0]
|
||||
tail_name = fullname.rpartition('.')[2]
|
||||
return filename_base == '__init__' and tail_name != '__init__'
|
||||
|
||||
def get_source(self, fullname):
|
||||
return self._read_source()
|
||||
def create_module(self, spec):
|
||||
"""Use default semantics for module creation."""
|
||||
|
||||
def exec_module(self, module):
|
||||
"""Execute the module."""
|
||||
code = self.get_code(module.__name__)
|
||||
exec(code, module.__dict__)
|
||||
|
||||
def get_filename(self, fullname):
|
||||
return self.filename.toOsSpecific()
|
||||
if fullname is not None and self.name != fullname:
|
||||
raise ImportError
|
||||
|
||||
def _read_source(self):
|
||||
""" Returns the Python source for this file, if it is
|
||||
available, or None if it is not. May raise IOError. """
|
||||
return self._vfile.get_filename().to_os_specific()
|
||||
|
||||
if self.desc[2] == imp.PY_COMPILED or \
|
||||
self.desc[2] == imp.C_EXTENSION:
|
||||
return None
|
||||
@staticmethod
|
||||
def get_data(path):
|
||||
vfile = vfs.get_file(Filename.from_os_specific(path))
|
||||
if vfile:
|
||||
return vfile.read_file(True)
|
||||
else:
|
||||
raise OSError
|
||||
|
||||
filename = Filename(self.filename)
|
||||
filename.setExtension('py')
|
||||
filename.setText()
|
||||
@staticmethod
|
||||
def path_stats(path):
|
||||
vfile = vfs.get_file(Filename.from_os_specific(path))
|
||||
if vfile:
|
||||
return {'mtime': vfile.get_timestamp(), 'size': vfile.get_file_size()}
|
||||
else:
|
||||
raise OSError
|
||||
|
||||
# Use the tokenize module to detect the encoding.
|
||||
import tokenize
|
||||
fh = open(self.filename, 'rb')
|
||||
encoding, lines = tokenize.detect_encoding(fh.readline)
|
||||
return (b''.join(lines) + fh.read()).decode(encoding)
|
||||
@staticmethod
|
||||
def path_mtime(path):
|
||||
vfile = vfs.get_file(Filename.from_os_specific(path))
|
||||
if vfile:
|
||||
return vfile.get_timestamp()
|
||||
else:
|
||||
raise OSError
|
||||
|
||||
def _import_extension_module(self, fullname):
|
||||
""" Loads the binary shared object as a Python module, and
|
||||
returns it. """
|
||||
|
||||
vfile = vfs.getFile(self.filename, False)
|
||||
class VFSSourceLoader(VFSLoader, SourceLoader):
|
||||
def get_source(self, fullname):
|
||||
if fullname is not None and self.name != fullname:
|
||||
raise ImportError
|
||||
|
||||
return decode_source(self._vfile.read_file(True))
|
||||
|
||||
|
||||
class VFSCompiledLoader(VFSLoader):
|
||||
def get_code(self, fullname):
|
||||
if fullname is not None and self.name != fullname:
|
||||
raise ImportError
|
||||
|
||||
vfile = self._vfile
|
||||
data = vfile.read_file(True)
|
||||
if data[:4] != MAGIC_NUMBER:
|
||||
raise ImportError("Bad magic number in %s" % (vfile))
|
||||
|
||||
return marshal.loads(data[16:])
|
||||
|
||||
def get_source(self, fullname):
|
||||
return None
|
||||
|
||||
|
||||
class VFSExtensionLoader(VFSLoader):
|
||||
def create_module(self, spec):
|
||||
vfile = self._vfile
|
||||
filename = vfile.get_filename()
|
||||
|
||||
# We can only import an extension module if it already exists on
|
||||
# disk. This means if it's a truly virtual file that has no
|
||||
# on-disk equivalent, we have to write it to a temporary file
|
||||
# first.
|
||||
if hasattr(vfile, 'getMount') and \
|
||||
isinstance(vfile.getMount(), VirtualFileMountSystem):
|
||||
if isinstance(vfile.get_mount(), VirtualFileMountSystem):
|
||||
# It's a real file.
|
||||
filename = self.filename
|
||||
elif self.filename.exists():
|
||||
pass
|
||||
elif filename.exists():
|
||||
# It's a virtual file, but it's shadowing a real file in
|
||||
# the same directory. Assume they're the same, and load
|
||||
# the real one.
|
||||
filename = self.filename
|
||||
else:
|
||||
# It's a virtual file with no real-world existence. Dump
|
||||
# it to disk. TODO: clean up this filename.
|
||||
filename = Filename.temporary('', self.filename.getBasenameWoExtension(),
|
||||
'.' + self.filename.getExtension(),
|
||||
type = Filename.TDso)
|
||||
filename.setExtension(self.filename.getExtension())
|
||||
filename.setBinary()
|
||||
sin = vfile.openReadFile(True)
|
||||
sout = OFileStream()
|
||||
if not filename.openWrite(sout):
|
||||
raise IOError
|
||||
if not copyStream(sin, sout):
|
||||
raise IOError
|
||||
vfile.closeReadFile(sin)
|
||||
del sout
|
||||
|
||||
module = imp.load_module(fullname, None, filename.toOsSpecific(),
|
||||
self.desc)
|
||||
module.__file__ = self.filename.toOsSpecific()
|
||||
return module
|
||||
|
||||
def _import_frozen_module(self, fullname):
|
||||
""" Imports the frozen module without messing around with
|
||||
searching any more. """
|
||||
#print >>sys.stderr, "importing frozen %s" % (fullname)
|
||||
module = imp.load_module(fullname, None, fullname,
|
||||
('', '', imp.PY_FROZEN))
|
||||
module.__path__ = []
|
||||
return module
|
||||
|
||||
def _read_code(self):
|
||||
""" Returns the Python compiled code object for this file, if
|
||||
it is available, or None if it is not. May raise IOError,
|
||||
ValueError, SyntaxError, or a number of other errors generated
|
||||
by the low-level system. """
|
||||
|
||||
if self.desc[2] == imp.PY_COMPILED:
|
||||
# It's a pyc file; just read it directly.
|
||||
pycVfile = vfs.getFile(self.filename, False)
|
||||
if pycVfile:
|
||||
return self._loadPyc(pycVfile, None)
|
||||
raise IOError('Could not read %s' % (self.filename))
|
||||
|
||||
elif self.desc[2] == imp.C_EXTENSION:
|
||||
return None
|
||||
|
||||
# It's a .py file (or an __init__.py file; same thing). Read
|
||||
# the .pyc file if it is available and current; otherwise read
|
||||
# the .py file and compile it.
|
||||
t_pyc = None
|
||||
for ext in compiledExtensions:
|
||||
pycFilename = Filename(self.filename)
|
||||
pycFilename.setExtension(ext)
|
||||
pycVfile = vfs.getFile(pycFilename, False)
|
||||
if pycVfile:
|
||||
t_pyc = pycVfile.getTimestamp()
|
||||
break
|
||||
|
||||
code = None
|
||||
if t_pyc and t_pyc >= self.timestamp:
|
||||
try:
|
||||
code = self._loadPyc(pycVfile, self.timestamp)
|
||||
except ValueError:
|
||||
code = None
|
||||
|
||||
if not code:
|
||||
source = self._read_source()
|
||||
filename = Filename(self.filename)
|
||||
filename.setExtension('py')
|
||||
code = self._compile(filename, source)
|
||||
|
||||
return code
|
||||
|
||||
def _loadPyc(self, vfile, timestamp):
|
||||
""" Reads and returns the marshal data from a .pyc file.
|
||||
Raises ValueError if there is a problem. """
|
||||
|
||||
code = None
|
||||
data = vfile.readFile(True)
|
||||
if data[:4] != imp.get_magic():
|
||||
raise ValueError("Bad magic number in %s" % (vfile))
|
||||
|
||||
t = int.from_bytes(data[4:8], 'little')
|
||||
data = data[12:]
|
||||
|
||||
if not timestamp or t == timestamp:
|
||||
return marshal.loads(data)
|
||||
else:
|
||||
raise ValueError("Timestamp wrong on %s" % (vfile))
|
||||
|
||||
def _compile(self, filename, source):
|
||||
""" Compiles the Python source code to a code object and
|
||||
attempts to write it to an appropriate .pyc file. May raise
|
||||
SyntaxError or other errors generated by the compiler. """
|
||||
|
||||
if source and source[-1] != '\n':
|
||||
source = source + '\n'
|
||||
code = compile(source, filename.toOsSpecific(), 'exec')
|
||||
|
||||
# try to cache the compiled code
|
||||
pycFilename = Filename(filename)
|
||||
pycFilename.setExtension(compiledExtensions[0])
|
||||
try:
|
||||
f = open(pycFilename.toOsSpecific(), 'wb')
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
f.write(imp.get_magic())
|
||||
f.write((self.timestamp & 0xffffffff).to_bytes(4, 'little'))
|
||||
f.write(b'\0\0\0\0')
|
||||
f.write(marshal.dumps(code))
|
||||
f.close()
|
||||
|
||||
return code
|
||||
|
||||
|
||||
class VFSSharedImporter:
|
||||
""" This is a special importer that is added onto the meta_path
|
||||
list, so that it is called before sys.path is traversed. It uses
|
||||
special logic to load one of the "shared" packages, by searching
|
||||
the entire sys.path for all instances of this shared package, and
|
||||
merging them. """
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def find_module(self, fullname, path = None, reload = False):
|
||||
#print >>sys.stderr, "shared find_module(%s), path = %s" % (fullname, path)
|
||||
|
||||
if fullname not in sharedPackages:
|
||||
# Not a shared package; fall back to normal import.
|
||||
return None
|
||||
|
||||
if path is None:
|
||||
path = sys.path
|
||||
|
||||
excludePaths = []
|
||||
if reload:
|
||||
# If reload is true, we are simply reloading the module,
|
||||
# looking for new paths to add.
|
||||
mod = sys.modules[fullname]
|
||||
excludePaths = getattr(mod, '_vfs_shared_path', None)
|
||||
if excludePaths is None:
|
||||
# If there isn't a _vfs_shared_path symbol already,
|
||||
# the module must have been loaded through
|
||||
# conventional means. Try to guess which path it was
|
||||
# found on.
|
||||
d = self.getLoadedDirname(mod)
|
||||
excludePaths = [d]
|
||||
|
||||
loaders = []
|
||||
for dir in path:
|
||||
if dir in excludePaths:
|
||||
continue
|
||||
|
||||
importer = sys.path_importer_cache.get(dir, None)
|
||||
if importer is None:
|
||||
try:
|
||||
importer = VFSImporter(dir)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
sys.path_importer_cache[dir] = importer
|
||||
|
||||
# It's a virtual file with no real-world existence. Dump
|
||||
# it to disk.
|
||||
ext = filename.get_extension()
|
||||
tmp_filename = Filename.temporary('', filename.get_basename_wo_extension(),
|
||||
'.' + ext,
|
||||
type = Filename.T_dso)
|
||||
tmp_filename.set_extension(ext)
|
||||
tmp_filename.set_binary()
|
||||
sin = vfile.open_read_file(True)
|
||||
try:
|
||||
loader = importer.find_module(fullname)
|
||||
if not loader:
|
||||
continue
|
||||
except ImportError:
|
||||
continue
|
||||
sout = OFileStream()
|
||||
if not tmp_filename.open_write(sout):
|
||||
raise IOError
|
||||
if not copy_stream(sin, sout):
|
||||
raise IOError
|
||||
finally:
|
||||
vfile.close_read_file(sin)
|
||||
del sout
|
||||
|
||||
loaders.append(loader)
|
||||
# Delete when the process ends.
|
||||
atexit.register(tmp_filename.unlink)
|
||||
|
||||
if not loaders:
|
||||
return None
|
||||
return VFSSharedLoader(loaders, reload = reload)
|
||||
# Make a dummy spec to pass to create_dynamic with the path to
|
||||
# our temporary file.
|
||||
spec = ModuleSpec(spec.name, spec.loader,
|
||||
origin=tmp_filename.to_os_specific(),
|
||||
is_package=False)
|
||||
|
||||
def getLoadedDirname(self, mod):
|
||||
""" Returns the directory name that the indicated
|
||||
conventionally-loaded module must have been loaded from. """
|
||||
module = _imp.create_dynamic(spec)
|
||||
module.__file__ = filename.to_os_specific()
|
||||
return module
|
||||
|
||||
if not getattr(mod, '__file__', None):
|
||||
return None
|
||||
def exec_module(self, module):
|
||||
_imp.exec_dynamic(module)
|
||||
|
||||
fullname = mod.__name__
|
||||
dirname = Filename.fromOsSpecific(mod.__file__).getDirname()
|
||||
def is_package(self, fullname):
|
||||
return False
|
||||
|
||||
parentname = None
|
||||
basename = fullname
|
||||
if '.' in fullname:
|
||||
parentname, basename = fullname.rsplit('.', 1)
|
||||
def get_code(self, fullname):
|
||||
return None
|
||||
|
||||
path = None
|
||||
if parentname:
|
||||
parent = sys.modules[parentname]
|
||||
path = parent.__path__
|
||||
if path is None:
|
||||
path = sys.path
|
||||
|
||||
for dir in path:
|
||||
pdir = str(Filename.fromOsSpecific(dir))
|
||||
if pdir + '/' + basename == dirname:
|
||||
# We found it!
|
||||
return dir
|
||||
|
||||
# Couldn't figure it out.
|
||||
def get_source(self, fullname):
|
||||
return None
|
||||
|
||||
|
||||
class VFSSharedLoader:
|
||||
""" The second part of VFSSharedImporter, this imports a list of
|
||||
packages and combines them. """
|
||||
class VFSNamespaceLoader:
|
||||
def create_module(self, spec):
|
||||
"""Use default semantics for module creation."""
|
||||
|
||||
def __init__(self, loaders, reload):
|
||||
self.loaders = loaders
|
||||
self.reload = reload
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
def load_module(self, fullname):
|
||||
#print >>sys.stderr, "shared load_module(%s), loaders = %s" % (fullname, map(lambda l: l.dir_path, self.loaders))
|
||||
def is_package(self, fullname):
|
||||
return True
|
||||
|
||||
mod = None
|
||||
message = None
|
||||
path = []
|
||||
vfs_shared_path = []
|
||||
if self.reload:
|
||||
mod = sys.modules[fullname]
|
||||
path = mod.__path__ or []
|
||||
if path == fullname:
|
||||
# Work around Python bug setting __path__ of frozen modules.
|
||||
path = []
|
||||
vfs_shared_path = getattr(mod, '_vfs_shared_path', [])
|
||||
def get_source(self, fullname):
|
||||
return ''
|
||||
|
||||
for loader in self.loaders:
|
||||
try:
|
||||
mod = loader.load_module(fullname, loadingShared = True)
|
||||
except ImportError:
|
||||
etype, evalue, etraceback = sys.exc_info()
|
||||
print("%s on %s: %s" % (etype.__name__, fullname, evalue))
|
||||
if not message:
|
||||
message = '%s: %s' % (fullname, evalue)
|
||||
continue
|
||||
for dir in getattr(mod, '__path__', []):
|
||||
if dir not in path:
|
||||
path.append(dir)
|
||||
def get_code(self, fullname):
|
||||
return compile('', '<string>', 'exec', dont_inherit=True)
|
||||
|
||||
if mod is None:
|
||||
# If all of them failed to load, raise ImportError.
|
||||
raise ImportError(message)
|
||||
|
||||
# If at least one of them loaded successfully, return the
|
||||
# union of loaded modules.
|
||||
mod.__path__ = path
|
||||
mod.__package__ = fullname
|
||||
|
||||
# Also set this special symbol, which records that this is a
|
||||
# shared package, and also lists the paths we have already
|
||||
# loaded.
|
||||
mod._vfs_shared_path = vfs_shared_path + [l.dir_path for l in self.loaders]
|
||||
|
||||
return mod
|
||||
def _path_hook(entry):
|
||||
# If this is a directory in the VFS, create a VFSFinder for this entry.
|
||||
vfile = vfs.get_file(Filename.from_os_specific(entry), False)
|
||||
if vfile and vfile.is_directory() and not isinstance(vfile.get_mount(), VirtualFileMountSystem):
|
||||
return VFSFinder(entry)
|
||||
else:
|
||||
raise ImportError
|
||||
|
||||
|
||||
_registered = False
|
||||
|
||||
|
||||
def register():
|
||||
""" Register the VFSImporter on the path_hooks, if it has not
|
||||
""" Register the VFSFinder on the path_hooks, if it has not
|
||||
already been registered, so that future Python import statements
|
||||
will vector through here (and therefore will take advantage of
|
||||
Panda's virtual file system). """
|
||||
|
|
@ -483,55 +268,9 @@ def register():
|
|||
global _registered
|
||||
if not _registered:
|
||||
_registered = True
|
||||
sys.path_hooks.insert(0, VFSImporter)
|
||||
sys.meta_path.insert(0, VFSSharedImporter())
|
||||
sys.path_hooks.insert(0, _path_hook)
|
||||
|
||||
# Blow away the importer cache, so we'll come back through the
|
||||
# VFSImporter for every folder in the future, even those
|
||||
# VFSFinder for every folder in the future, even those
|
||||
# folders that previously were loaded directly.
|
||||
sys.path_importer_cache = {}
|
||||
|
||||
|
||||
def reloadSharedPackage(mod):
|
||||
""" Reloads the specific module as a shared package, adding any
|
||||
new directories that might have appeared on the search path. """
|
||||
|
||||
fullname = mod.__name__
|
||||
path = None
|
||||
if '.' in fullname:
|
||||
parentname = fullname.rsplit('.', 1)[0]
|
||||
parent = sys.modules[parentname]
|
||||
path = parent.__path__
|
||||
|
||||
importer = VFSSharedImporter()
|
||||
loader = importer.find_module(fullname, path = path, reload = True)
|
||||
if loader:
|
||||
loader.load_module(fullname)
|
||||
|
||||
# Also force any child packages to become shared packages, if
|
||||
# they aren't already.
|
||||
for basename, child in list(mod.__dict__.items()):
|
||||
if isinstance(child, types.ModuleType):
|
||||
childname = child.__name__
|
||||
if childname == fullname + '.' + basename and \
|
||||
hasattr(child, '__path__') and \
|
||||
childname not in sharedPackages:
|
||||
sharedPackages[childname] = True
|
||||
reloadSharedPackage(child)
|
||||
|
||||
|
||||
def reloadSharedPackages():
|
||||
""" Walks through the sharedPackages list, and forces a reload of
|
||||
any modules on that list that have already been loaded. This
|
||||
allows new directories to be added to the search path. """
|
||||
|
||||
#print >> sys.stderr, "reloadSharedPackages, path = %s, sharedPackages = %s" % (sys.path, sharedPackages.keys())
|
||||
|
||||
# Sort the list, just to make sure parent packages are reloaded
|
||||
# before child packages are.
|
||||
for fullname in sorted(sharedPackages.keys()):
|
||||
mod = sys.modules.get(fullname, None)
|
||||
if not mod:
|
||||
continue
|
||||
|
||||
reloadSharedPackage(mod)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
from panda3d.core import VirtualFileSystem, VirtualFileMountRamdisk
|
||||
import sys
|
||||
|
||||
|
||||
def test_VFSImporter():
|
||||
from direct.showbase import VFSImporter
|
||||
|
||||
VFSImporter.register()
|
||||
|
||||
vfs = VirtualFileSystem.get_global_ptr()
|
||||
mount = VirtualFileMountRamdisk()
|
||||
success = vfs.mount(mount, "/ram", 0)
|
||||
assert success
|
||||
try:
|
||||
sys.path.insert(0, "/ram")
|
||||
vfs.write_file("/ram/testmod.py", b"var = 1\n", False)
|
||||
|
||||
vfs.make_directory("/ram/testpkg")
|
||||
vfs.write_file("/ram/testpkg/__init__.py", b"var = 2\n", False)
|
||||
vfs.write_file("/ram/testpkg/test.py", b"var = 3\n", False)
|
||||
|
||||
vfs.make_directory("/ram/testnspkg")
|
||||
vfs.write_file("/ram/testnspkg/test.py", b"var = 4\n", False)
|
||||
|
||||
import testmod
|
||||
assert testmod.var == 1
|
||||
assert testmod.__spec__.name == 'testmod'
|
||||
assert testmod.__spec__.origin == '/ram/testmod.py'
|
||||
assert testmod.__file__ == '/ram/testmod.py'
|
||||
|
||||
import testpkg
|
||||
assert testpkg.var == 2
|
||||
assert testpkg.__package__ == 'testpkg'
|
||||
assert testpkg.__path__ == ['/ram/testpkg']
|
||||
assert testpkg.__spec__.name == 'testpkg'
|
||||
assert testpkg.__spec__.origin == '/ram/testpkg/__init__.py'
|
||||
assert testpkg.__file__ == '/ram/testpkg/__init__.py'
|
||||
|
||||
from testpkg import test
|
||||
assert test.var == 3
|
||||
assert test.__spec__.name == 'testpkg.test'
|
||||
assert test.__spec__.origin == '/ram/testpkg/test.py'
|
||||
assert test.__file__ == '/ram/testpkg/test.py'
|
||||
|
||||
from testnspkg import test
|
||||
assert test.var == 4
|
||||
assert test.__spec__.name == 'testnspkg.test'
|
||||
assert test.__spec__.origin == '/ram/testnspkg/test.py'
|
||||
assert test.__file__ == '/ram/testnspkg/test.py'
|
||||
|
||||
finally:
|
||||
vfs.unmount(mount)
|
||||
try:
|
||||
del sys.path[sys.path.index("/ram")]
|
||||
except ValueError:
|
||||
pass
|
||||
Loading…
Reference in New Issue