diff --git a/direct/src/p3d/AppRunner.py b/direct/src/p3d/AppRunner.py index 7d5161f2e3..53f8379155 100644 --- a/direct/src/p3d/AppRunner.py +++ b/direct/src/p3d/AppRunner.py @@ -294,16 +294,12 @@ class AppRunner(DirectObject): if mainName: moduleName = mainName - root = self.multifileRoot - if '.' in moduleName: - root += '/' + '/'.join(moduleName.split('.')[:-1]) - v = VFSImporter.VFSImporter(root) - loader = v.find_module(moduleName) - if not loader: + try: + __import__(moduleName) + except ImportError: message = "No %s found in application." % (moduleName) raise StandardError, message - - main = loader.load_module(moduleName) + main = sys.modules[moduleName] if hasattr(main, 'main') and callable(main.main): main.main(self) @@ -418,6 +414,7 @@ class AppRunner(DirectObject): # Mount the Multifile under /mf, by convention. vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly) + VFSImporter.reloadSharedPackages() self.loadMultifilePrcFiles(mf, self.multifileRoot) self.gotP3DFilename = True diff --git a/direct/src/p3d/PackageInfo.py b/direct/src/p3d/PackageInfo.py index a8a7259e5d..a3e0aa152a 100644 --- a/direct/src/p3d/PackageInfo.py +++ b/direct/src/p3d/PackageInfo.py @@ -363,3 +363,19 @@ class PackageInfo: if not foundOnPath: # Not already here; add it. sys.path.append(root) + + # Also, find any toplevel Python packages, and add these as + # shared packages. This will allow different packages + # installed in different directories to share Python files as + # if they were all in the same directory. + for filename in mf.getSubfileNames(): + if filename.endswith('/__init__.pyc') or \ + filename.endswith('/__init__.pyo') or \ + filename.endswith('/__init__.py'): + components = filename.split('/')[:-1] + moduleName = '.'.join(components) + VFSImporter.sharedPackages[moduleName] = True + + # Fix up any shared directories so we can load packages from + # disparate locations. + VFSImporter.reloadSharedPackages() diff --git a/direct/src/p3d/panda3d.pdef b/direct/src/p3d/panda3d.pdef index 2563840445..6cc820796f 100755 --- a/direct/src/p3d/panda3d.pdef +++ b/direct/src/p3d/panda3d.pdef @@ -101,58 +101,58 @@ default-model-extension .bam """ + auxDisplays) -## class egg(package): -## # This package contains the code for reading and operating on egg -## # files. Since the Packager automatically converts egg files to bam -## # files, this is not needed for most Panda3D applications. +class egg(package): + # This package contains the code for reading and operating on egg + # files. Since the Packager automatically converts egg files to bam + # files, this is not needed for most Panda3D applications. -## config(display_name = "Panda3D egg loader") -## require('panda3d') + config(display_name = "Panda3D egg loader") + require('panda3d') -## file('libpandaegg.dll') + file('libpandaegg.dll') -## file('egg.prc', extract = True, text = """ -## plugin-path $EGG_ROOT -## load-file-type egg pandaegg -## """) + file('egg.prc', extract = True, text = """ +plugin-path $EGG_ROOT +load-file-type egg pandaegg +""") -## class wx(package): -## config(display_name = "wxPython GUI Toolkit") -## require('panda3d') +class wx(package): + config(display_name = "wxPython GUI Toolkit") + require('panda3d') -## module('direct.showbase.WxGlobal', 'wx', 'wx.*') + module('direct.showbase.WxGlobal', 'wx', 'wx.*') -## class tk(package): -## config(display_name = "Tk GUI Toolkit") -## require('panda3d') +class tk(package): + config(display_name = "Tk GUI Toolkit") + require('panda3d') -## module('Tkinter', -## 'direct.showbase.TkGlobal', -## 'direct.tkpanels', -## 'direct.tkwidgets') + module('Tkinter', + 'direct.showbase.TkGlobal', + 'direct.tkpanels', + 'direct.tkwidgets') -## class packp3d(p3d): -## # This application is a command-line convenience for building a p3d -## # application out of a directory hierarchy on disk. We build it here -## # into its own p3d application, to allow end-users to easily build p3d -## # applications using the appropriate version of Python and Panda for -## # the targeted runtime. +class packp3d(p3d): + # This application is a command-line convenience for building a p3d + # application out of a directory hierarchy on disk. We build it here + # into its own p3d application, to allow end-users to easily build p3d + # applications using the appropriate version of Python and Panda for + # the targeted runtime. -## config(display_name = "Panda3D Application Packer", -## hidden = True, platform_specific = False) -## require('panda3d', 'egg') + config(display_name = "Panda3D Application Packer", + hidden = True, platform_specific = False) + require('panda3d', 'egg') -## mainModule('direct.p3d.packp3d') + mainModule('direct.p3d.packp3d') -## class ppackage(p3d): -## # As above, a packaging utility. This is the fully-general ppackage -## # utility, which reads pdef files (like this one!) and creates one or -## # more packages or p3d applications. +class ppackage(p3d): + # As above, a packaging utility. This is the fully-general ppackage + # utility, which reads pdef files (like this one!) and creates one or + # more packages or p3d applications. -## config(display_name = "Panda3D General Package Utility", -## hidden = True, platform_specific = False) -## require('panda3d', 'egg') + config(display_name = "Panda3D General Package Utility", + hidden = True, platform_specific = False) + require('panda3d', 'egg') -## mainModule('direct.p3d.ppackage') + mainModule('direct.p3d.ppackage') diff --git a/direct/src/plugin/p3dSession.cxx b/direct/src/plugin/p3dSession.cxx index 9b5ae0dff1..f54ab6b787 100644 --- a/direct/src/plugin/p3dSession.cxx +++ b/direct/src/plugin/p3dSession.cxx @@ -664,13 +664,15 @@ start_p3dpython(P3DInstance *inst) { } // Build up a search path that includes all of the required packages - // that have already been installed. + // that have already been installed. We build this in reverse + // order, so that the higher-order packages come first in the list; + // that allows them to shadow settings in the lower-order packages. + assert(!inst->_packages.empty()); string search_path; - size_t pi = 0; - assert(pi < inst->_packages.size()); + size_t pi = inst->_packages.size() - 1; search_path = inst->_packages[pi]->get_package_dir(); - ++pi; - while (pi < inst->_packages.size()) { + while (pi > 0) { + --pi; #ifdef _WIN32 search_path += ';'; #else @@ -678,7 +680,6 @@ start_p3dpython(P3DInstance *inst) { #endif // _WIN32 search_path += inst->_packages[pi]->get_package_dir(); - ++pi; } nout << "Search path is " << search_path << "\n"; diff --git a/direct/src/showbase/VFSImporter.py b/direct/src/showbase/VFSImporter.py index 5cb0affb39..e432db9b12 100644 --- a/direct/src/showbase/VFSImporter.py +++ b/direct/src/showbase/VFSImporter.py @@ -5,9 +5,26 @@ import os import marshal import imp import struct +import types import __builtin__ -__all__ = ['register', 'freeze_new_modules'] +__all__ = ['register', 'sharedPackages', + 'reloadSharedPackage', 'reloadSharedPackages'] + +# 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() @@ -102,22 +119,36 @@ class VFSLoader: self.desc = desc self.packagePath = packagePath - def load_module(self, fullname): + 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.fileType == FTFrozenModule: return self._import_frozen_module(fullname) if self.fileType == FTExtensionModule: 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, new.module(fullname)) - mod.__file__ = self.filename.cStr() + mod.__file__ = self.filename.toOsSpecific() mod.__loader__ = self if self.packagePath: - mod.__path__ = [self.packagePath.cStr()] + mod.__path__ = [self.packagePath.toOsSpecific()] + #print >> sys.stderr, "loaded %s, path = %s" % (fullname, mod.__path__) exec code in mod.__dict__ return mod @@ -137,6 +168,9 @@ class VFSLoader: def get_source(self, fullname): return self._read_source() + + def get_filename(self, fullname): + return self.filename.toOsSpecific() def _read_source(self): """ Returns the Python source for this file, if it is @@ -190,7 +224,7 @@ class VFSLoader: module = imp.load_module(fullname, None, filename.toOsSpecific(), self.desc) - module.__file__ = self.filename.cStr() + module.__file__ = self.filename.toOsSpecific() return module def _import_frozen_module(self, fullname): @@ -199,7 +233,6 @@ class VFSLoader: #print >>sys.stderr, "importing frozen %s" % (fullname) module = imp.load_module(fullname, None, fullname, ('', '', imp.PY_FROZEN)) - #print >>sys.stderr, "got frozen %s" % (module) return module def _read_code(self): @@ -269,7 +302,7 @@ class VFSLoader: if source and source[-1] != '\n': source = source + '\n' - code = __builtin__.compile(source, filename.cStr(), 'exec') + code = __builtin__.compile(source, filename.toOsSpecific(), 'exec') # try to cache the compiled code pycFilename = Filename(filename) @@ -289,6 +322,138 @@ class VFSLoader: 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 + + try: + loader = importer.find_module(fullname) + if not loader: + continue + except ImportError: + continue + + loaders.append(loader) + + if not loaders: + return None + return VFSSharedLoader(loaders, reload = reload) + + def getLoadedDirname(self, mod): + """ Returns the directory name that the indicated + conventionally-loaded module must have been loaded from. """ + + fullname = mod.__name__ + dirname = Filename.fromOsSpecific(mod.__file__).getDirname() + + parentname = None + basename = fullname + if '.' in fullname: + parentname, basename = fullname.rsplit('.', 1) + + path = None + if parentname: + parent = sys.modules[parentname] + path = parent.__path__ + if path is None: + path = sys.path + + for dir in path: + pdir = Filename.fromOsSpecific(dir).cStr() + if pdir + '/' + basename == dirname: + # We found it! + return dir + + # Couldn't figure it out. + return None + +class VFSSharedLoader: + """ The second part of VFSSharedImporter, this imports a list of + packages and combines them. """ + + def __init__(self, loaders, reload): + self.loaders = loaders + self.reload = reload + + def load_module(self, fullname): + #print >>sys.stderr, "shared load_module(%s), loaders = %s" % (fullname, map(lambda l: l.dir_path, self.loaders)) + + mod = None + path = [] + vfs_shared_path = [] + if self.reload: + mod = sys.modules[fullname] + path = mod.__path__ or [] + vfs_shared_path = getattr(mod, '_vfs_shared_path', []) + + for loader in self.loaders: + try: + mod = loader.load_module(fullname, loadingShared = True) + except ImportError: + continue + for dir in getattr(mod, '__path__', []): + if dir not in path: + path.append(dir) + + if mod is None: + # If all of them failed to load, raise ImportError. + raise ImportError + + # If at least one of them loaded successfully, return the + # union of loaded modules. + mod.__path__ = path + + # 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 + map(lambda l: l.dir_path, self.loaders) + + return mod + _registered = False def register(): """ Register the VFSImporter on the path_hooks, if it has not @@ -300,8 +465,52 @@ def register(): if not _registered: _registered = True sys.path_hooks.insert(0, VFSImporter) - + sys.meta_path.insert(0, VFSSharedImporter()) + # Blow away the importer cache, so we'll come back through the # VFSImporter 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 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()) + + for fullname in sharedPackages.keys(): + mod = sys.modules.get(fullname, None) + if not mod: + continue + + reloadSharedPackage(mod) + + diff --git a/direct/src/showutil/FreezeTool.py b/direct/src/showutil/FreezeTool.py index 07566beee3..f37c2e9186 100644 --- a/direct/src/showutil/FreezeTool.py +++ b/direct/src/showutil/FreezeTool.py @@ -1096,12 +1096,12 @@ class Freezer: moduleList.append(self.makeForbiddenModuleListEntry(moduleName)) else: if origName in sourceTrees: - # This is one of our Python source trees. - # These are a special case: we don't compile - # the __init__.py files within them, since - # their only purpose is to munge the __path__ - # variable anyway. Instead, we pretend the - # __init__.py files are empty. + # This is one of Panda3D's own Python source + # trees. These are a special case: we don't + # compile the __init__.py files within them, + # since their only purpose is to munge the + # __path__ variable anyway. Instead, we + # pretend the __init__.py files are empty. code = compile('', moduleName, 'exec') if code: