''' PyMOL Plugins Engine (c) 2011-2012 Thomas Holder, PyMOL OS Fellow License: BSD-2-Clause ''' import os import sys import pymol from pymol import cmd from pymol import colorprinting from .legacysupport import * # variables PYMOLPLUGINSRC = os.path.expanduser('~/.pymolpluginsrc.py') preferences = { 'verbose': False, 'instantsave': True, } autoload = {} plugins = {} HAVE_QT = False # exception types class QtNotAvailableError(Exception): pass # plugins from PYMOL_DATA startup.__path__.append(cmd.exp_path('$PYMOL_DATA/startup')) N_NON_USER_PATHS = len(startup.__path__) # API functions def is_verbose(debug=0): verbose = pref_get('verbose') if debug and verbose < 0: return True return verbose and pymol.invocation.options.show_splash def get_startup_path(useronly=False): if useronly: # assume last item is always from installation directory return startup.__path__[:-N_NON_USER_PATHS] return startup.__path__ def set_startup_path(p, autosave=True): if isinstance(p, list): startup.__path__[:-N_NON_USER_PATHS] = p if autosave: set_pref_changed() else: print(' Error: set_startup_path failed') def pref_set(k, v): preferences[k] = v set_pref_changed() def pref_get(k, d=None): return preferences.get(k, d) def pref_save(filename=PYMOLPLUGINSRC, quiet=1): import pprint repr = pprint.pformat try: f = open(cmd.exp_path(filename), 'w') except IOError: print(' Plugin-Error: Cannot write Plugins resource file to', filename) return print('# AUTOGENERATED FILE', file=f) print('try:', file=f) print(' import', __name__, file=f) print(' ' + __name__ + '.autoload =', repr(autoload), file=f) print(' ' + __name__ + '.preferences =', repr(preferences), file=f) print(' ' + __name__ + '.set_startup_path(', repr(get_startup_path(True)), ', False)', file=f) print('except:', file=f) print(' import os', file=f) print(' print("Error while loading " + os.path.abspath(__script__))', file=f) f.close() if not int(quiet): print(' Plugin settings saved!') def set_pref_changed(): if pref_get('instantsave', True): verbose = pref_get('verbose', False) pref_save(quiet=not verbose) def addmenuitemqt(label, command=None, menuName='PluginQt'): ''' Adds plugin menu item to main 'Plugin' menu. Intended for plugins which open a PyQt window. ''' if not HAVE_QT: raise QtNotAvailableError() addmenuitem(label, command, menuName) def addmenuitem(label, command=None, menuName='Plugin'): ''' Generic replacement for MegaWidgets menu item adding ''' labels1 = [menuName] + label.split('|') labels2 = ['|'.join(labels1[0:i]) for i in range(1, len(labels1))] pmgapp = get_pmgapp() if pmgapp is not None: for i in range(1, len(labels2)): try: pmgapp.menuBar.addcascademenu(labels2[i-1], labels2[i], label=labels1[i]) except ValueError: pass if labels1[-1] == '-': pmgapp.menuBar.addmenuitem(labels2[-1], 'separator') else: pmgapp.menuBar.addmenuitem(labels2[-1], 'command', label=labels1[-1], command=command) def plugin_load(name, quiet=1): ''' DESCRIPTION Load plugin from command line. ''' if len(plugins) == 0: initialize(-2) if name not in plugins: raise pymol.CmdException('no such plugin') info = plugins[name] if info.loaded: if not int(quiet): print(' info: plugin already loaded') return info.load() # helper functions and classes class PluginInfo(object): ''' Hold all information about a plugin. A instance with mod_name=None is considered a temporary instance which cannot be loaded (during installation, for extraction of metadata, ...) ''' def __init__(self, name, filename, mod_name=None): self.name = name self.mod_name = mod_name self.filename = filename # set on loading self.loadtime = None self.commands = [] # register if not self.is_temporary: plugins[name] = self def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.name) @property def autoload(self): return autoload.get(self.name, True) @autoload.setter def autoload(self, value): autoload[self.name] = bool(value) set_pref_changed() @property def module(self): return sys.modules.get(self.mod_name) @property def loaded(self): return self.loadtime is not None @property def is_temporary(self): return self.mod_name is None def get_metadata(self): ''' Parse plugin file for metadata (hash-commented block at beginning of file). ''' metadata = dict() f = open(self.filename, 'rb') for line in f: line = line.decode('utf-8', errors='replace') if line.strip() == '': continue if not line.startswith('#'): break if ':' in line: key, value = line[1:].split(':', 1) metadata[key.strip()] = value.strip() f.close() self.get_metadata = lambda: metadata return metadata def get_version(self): ''' Get version as string. If no version available, return empty string. Preferred way to specify version is metadata "Version" field. ''' v = self.get_metadata().get('Version', '') if not v and self.loaded: v = getattr(self.module, '__version__', '') return str(v) def get_citation_required(self): ''' Return True if Citation-Required: Yes ''' v = self.get_metadata().get('Citation-Required', 'No') return v.lower() == 'yes' def get_docstring(self): ''' Get docstring either from loaded module, or try to parse first python statement from file, without executing any code. ''' if self.loaded: return self.module.__doc__ try: c = compile(b''.join(open(self.filename, 'rb')), 'x', 'exec', dont_inherit=True) s = c.co_consts[0] if cmd.is_string(s): return s except SyntaxError as e: return 'WARNING: Plugin not Python 3.x compatible: ' + str(e) except: pass def load(self, pmgapp=None, force=0): ''' Load and initialize plugin. If pmgapp == -1, do not initialize. ''' import time assert not self.is_temporary starttime = time.time() if pmgapp is None: pmgapp = get_pmgapp() verbose = pref_get('verbose', False) try: # overload cmd.extend to register commands extend_orig = cmd.extend def extend_overload(a, b=None): self.commands.append(a if b else a.__name__) return extend_orig(a, b) cmd.extend = extend_overload # do not use self.loaded here if force and self.module is not None: from importlib import reload reload(self.module) else: __import__(self.mod_name, level=0) if pmgapp != -1: self.legacyinit(pmgapp) cmd.extend = extend_orig self.loadtime = time.time() - starttime if verbose and pymol.invocation.options.show_splash: print(' Plugin "%s" loaded in %.2f seconds' % (self.name, self.loadtime)) except QtNotAvailableError: colorprinting.warning("Plugin '%s' only available with PyQt GUI." % (self.name,)) except: e = sys.exc_info()[1] if verbose: colorprinting.print_exc([__file__]) elif 'libX11' in str(e) and sys.platform == 'darwin': colorprinting.error('Please install XQuartz (https://www.xquartz.org/)') else: colorprinting.error(e) colorprinting.warning("Unable to initialize plugin '%s' (%s)." % (self.name, self.mod_name)) return False return True def legacyinit(self, pmgapp): ''' Call the __init__ or __init_plugin__ function which takes the PMGApp instance as argument (usually adds menu items). This must be called after loading and after launching of the external GUI (PMGApp). Ignore the build-in that takes a string as first argument. Only call __init__ if it's of type 'function'. ''' import types mod = self.module if mod is None: raise RuntimeError('not loaded') if hasattr(mod, '__init_plugin__'): mod.__init_plugin__(pmgapp) elif hasattr(mod, '__init__'): if isinstance(mod.__init__, types.FunctionType): mod.__init__(pmgapp) def uninstall(self, parent=None): ''' Remove a plugin Removes the complete directory tree in case of a package. ''' from .legacysupport import tkMessageBox if parent is None: parent = get_tk_focused() ok = tkMessageBox.askyesno('Confirm', 'Do you really want to uninstall plugin "%s"' % (self.name), parent=parent) if not ok: return False showinfo = tkMessageBox.showinfo dirname, basename = os.path.split(self.filename) try: if basename == '__init__.py': import shutil shutil.rmtree(dirname) else: for suffix in ['', 'o', 'c']: filename = self.filename + suffix if os.path.exists(filename): os.remove(filename) except OSError: showinfo('Error', 'Could not delete files for plugin "%s".' % (self.name), parent=parent) return False plugins.pop(self.name, None) autoload.pop(self.name, None) set_pref_changed() showinfo('Info', 'Plugin "%s" successfully removed. Please restart PyMOL.' % (self.name), parent=parent) return True def findPlugins(paths): ''' Find all python modules (extension .py and directories with __init__.py) inside a list of directories. Returns a dictionary with names to filenames mapping. ''' import time start = time.time() verbose = pref_get('verbose', False) modules = dict() for path in paths: if not os.path.isdir(path): continue for filename in os.listdir(path): # ignore names that start with dot or underscore if filename[0] in ['.', '_']: continue if '.' in filename: name, _, ext = filename.partition('.') if ext == 'py': if name not in modules: modules[name] = os.path.join(path, filename) elif verbose: print(' warning: multiple plugins named', name) else: name, filename = filename, os.path.join(path, filename, '__init__.py') if os.path.exists(filename): if name not in modules: modules[name] = filename elif verbose: print(' warning: multiple plugins named', name) if verbose: print(' Scanning for modules took %.4f seconds' % (time.time() - start)) return modules def initialize(pmgapp=-1): ''' Searches for plugins and registers them. pmgapp == -2: No Autoloading pmgapp == -1: Autoloading but no legacyinit else: Autoloading and legacyinit ''' if os.path.exists(PYMOLPLUGINSRC): from pymol import parsing try: parsing.run_file(PYMOLPLUGINSRC, {'__script__': PYMOLPLUGINSRC}, {}) except SyntaxError as e: colorprinting.warning(str(e)) autoload = (pmgapp != -2) for parent in [startup]: modules = findPlugins(parent.__path__) for name, filename in modules.items(): mod_name = parent.__name__ + '.' + name info = PluginInfo(name, filename, mod_name) if autoload and info.autoload: info.load(pmgapp) # pymol commands cmd.extend('plugin_load', plugin_load) cmd.extend('plugin_pref_save', pref_save) # autocompletion cmd.auto_arg[0]['plugin_load'] = [ lambda: cmd.Shortcut(plugins), 'plugin', '' ] # vi:expandtab:smarttab:sw=4