modules/pymol/parser.py (462 lines of code) (raw):
#A* -------------------------------------------------------------------
#B* This file contains source code for the PyMOL computer program
#C* Copyright (c) Schrodinger, LLC.
#D* -------------------------------------------------------------------
#E* It is unlawful to modify or remove this copyright notice.
#F* -------------------------------------------------------------------
#G* Please see the accompanying LICENSE file for further information.
#H* -------------------------------------------------------------------
#I* Additional authors of this source file include:
#-*
#-*
#-*
#Z* -------------------------------------------------------------------
# parser.py
# Python parser module for PyMol
#
from __future__ import absolute_import
# Don't import __future__.print_function
class SecurityException(Exception):
pass
SCRIPT_TOPLEVEL = 'toplevel'
if __name__=='pymol.parser':
import pymol
import traceback
import collections
import re
import glob
import sys
import os
import __main__
from . import parsing
from . import colorprinting
from .cmd import _feedback,fb_module,fb_mask,exp_path
QuietException = parsing.QuietException
CmdException = pymol.CmdException
py_delims = { '=' : 1, '+=' : 1, '-=' : 1, '*=' : 1,
'/=' :1, '//=' : 1, '%=' : 1, '&=' : 1,
'|=' :1, '^=' : 1, '>>=' : 1,'<<=' : 1,
'**=':1 }
remove_lists_re = re.compile("\[[^\]]*\]")
def complete_sc(st,sc,type_name,postfix, mode=0):
result = None
try:
sc=sc() # invoke lambda functions (if any)
except:
traceback.print_exc()
amb = sc.interpret(st, mode)
if amb==None:
colorprinting.warning(" parser: no matching %s."%type_name)
elif isinstance(amb, str):
result = amb+postfix
else:
amb.sort()
colorprinting.suggest(" parser: matching %s:"%type_name)
flist = [x for x in amb if x[0]!='_']
lst = parsing.list_to_str_list(flist)
for a in lst:
colorprinting.suggest(a)
# now append up to point of ambiguity
if not len(flist):
css = []
else:
css = list(flist[0]) # common sub-string (css)
for a in flist:
ac = list(a)
tmp = css
css = []
for c in range(len(tmp)):
if tmp[c]!=ac[c]:
break
css.append(tmp[c])
css = [_f for _f in css if _f]
css = ''.join(css)
if len(css)>len(st):
result = css
return result
class NestLayer:
def __init__(self):
self.cont = ""
self.com0 = ""
self.sc_path = SCRIPT_TOPLEVEL
self.lineno = 0
self.literal_python_fallback = False
self.embed_sentinel = None
self.embed_dict = {}
self.next = []
class Parser:
def __init__(self,cmd):
cmd = cmd._weakrefproxy
self.cmd = cmd
self.nest = 0
self.layer = collections.defaultdict(NestLayer)
self.pymol_names = self.cmd._pymol.__dict__
# parsing state implemented with dictionaries to enable safe recursion
# to arbitrary depths
#com0 = {} # verbose line, as read in
#com1 = {} # line w/o trailing whitespace
#com2 = {} # non-compound command
#cont = {} # continued characters from previous lines (i.e., before \ )
#script = {} # file handles
#sc_path = {} # file paths
#kw = {} # row in the keyword table for the current command
#input = {} # list of length two - command and unparsed arguments string
#next = {} # characters for next command (i.e., after ; )
#args = {} # parsed non-keyword argument string
#kw_args = {} # parser keyword argument string
#embed_dict = {}
#embed_list = {}
#embed_sentinel = {}
#embed_type = {}
#embed_line = {}
# The resulting value from a pymol command (if any) is stored in the
# parser.result global variable. However, script developers will
# geerally want to switch to the Python API for any of this kind of
# stuff.
self.result = None
# initialize parser
self.cmd._pymol.__script__ = SCRIPT_TOPLEVEL
def exec_python(self, s, secure=False, fallback=False):
if secure:
raise SecurityException('Python expressions disallowed in this file')
layer = self.layer[self.nest]
layer.literal_python_fallback = fallback
# for meaningful line number in error messages
blanklines = layer.lineno - 1 - s.count('\n')
s = '\n' * blanklines + s + '\n'
s = compile(s, layer.sc_path, 'exec')
exec(s, self.pymol_names, self.pymol_names)
# main parser routine
def parse(self,s,secure=0):
try:
self.nest += 1
return self._parse(s, secure)
finally:
self.nest -= 1
def _parse(self, s, secure):
layer = self.layer[self.nest]
self.result = None
# report any uncaught errors...
# WLD: this is problematic if parse is called inside an exception...removed.
# if sys.exc_info()!=(None,None,None):
# traceback.print_exc()
# sys.exc_clear()
def parse_embed():
if s.strip() == layer.embed_sentinel:
etn = layer.embed_type
if etn == 0: # embedded data
colorprinting.parrot(" Embed: read %d lines."%(len(layer.embed_list)))
layer.embed_sentinel=None
elif etn == 1: # python block
colorprinting.parrot("PyMOL>"+s.rstrip())
py_block = ''.join(layer.embed_list)
del layer.embed_list
layer.embed_sentinel=None
self.exec_python(py_block)
elif etn == 2: # skip block
colorprinting.parrot(" Skip: skipped %d lines."%(layer.embed_line))
layer.embed_sentinel=None
else:
etn = layer.embed_type
if etn == 0: # normal embedded data
layer.embed_list.append(s.rstrip()+"\n")
elif etn == 1: # python block
el = layer.embed_line + 1
colorprinting.parrot("%5d:%s"%(el,s.rstrip()))
layer.embed_line = el
layer.embed_list.append(s.rstrip()+"\n")
elif etn == 2:
layer.embed_line = layer.embed_line + 1
p_result = 1
layer.com0 = s
try:
if layer.embed_sentinel is not None:
parse_embed()
return 1
layer.com1 = layer.com0.rstrip() # strips trailing whitespace
if len(layer.com1) > 0:
if str(layer.com1[-1]) == "\\":
# prepend leftovers
if layer.cont != '':
layer.cont = layer.cont + "\n" + layer.com1[:-1]
else:
layer.cont = layer.com1[:-1]
else:
# prepend leftovers
if layer.cont != '':
layer.com1 = layer.cont + "\n" + layer.com1
layer.cont = ''
# this routine splits up the line first based on semicolon
layer.next = parsing.split(layer.com1,';',1) + layer.next[1:]
# layer.com2 now a full non-compound command
layer.com2 = layer.next[0]
layer.input = layer.com2.split(' ',1)
lin = len(layer.input)
if lin:
layer.input[0] = layer.input[0].strip()
com = layer.input[0]
if (com[0:1]=='/'):
# explicit literal python
layer.com2 = layer.com2[1:].strip()
if len(layer.com2)>0:
self.exec_python(layer.com2, secure)
elif lin>1 and layer.input[-1:][0].split(' ',1)[0] in py_delims:
self.exec_python(layer.com2, secure)
else:
# try to find a keyword which matches
if com in self.cmd.kwhash:
amb = self.cmd.kwhash.interpret(com)
if amb == None:
com = self.cmd.kwhash[com]
elif not isinstance(amb, str):
colorprinting.warning('Error: ambiguous command: ')
amb.sort()
amb = parsing.list_to_str_list(amb)
for a in amb:
colorprinting.warning(a)
raise QuietException
com = amb
if com in self.cmd.keyword:
# here is the command and argument handling section
layer.kw = self.cmd.keyword[com]
if layer.kw[4]>=parsing.NO_CHECK:
# stricter, Python-based argument parsing
# remove line breaks (only important for Python expressions)
layer.com2=layer.com2.replace('\n','')
if layer.kw[4]>=parsing.LITERAL: # treat literally
layer.next = []
if not secure:
layer.com2=layer.com1
else:
raise SecurityException('Python expressions disallowed in this file')
if secure and (layer.kw[4]==parsing.SECURE):
layer.next = []
raise SecurityException('Command disallowed in this file')
else:
(layer.args, layer.kw_args) = \
parsing.prepare_call(
layer.kw[0],
parsing.parse_arg(layer.com2,mode=layer.kw[4],_self=self.cmd),
layer.kw[4], _self=self.cmd) # will raise exception on failure
self.result=layer.kw[0](*layer.args, **layer.kw_args)
elif layer.kw[4]==parsing.PYTHON:
# handle python keyword
layer.com2 = layer.com2.strip()
if len(layer.com2)>0:
self.exec_python(layer.com2, secure)
else:
# remove line breaks (only important for Python expressions)
layer.com2=layer.com2.replace('\n','')
# old parsing style, being phased out
if layer.kw[4]==parsing.ABORT:
return None # SCRIPT ABORT EXIT POINT
if layer.kw[4]==parsing.MOVIE: # copy literal single line, no breaks
layer.next = []
if not secure:
layer.input = layer.com1.split(' ',1)
else:
raise SecurityException('Movie commands disallowed in this file')
if len(layer.input)>1:
layer.args = parsing.split(layer.input[1],layer.kw[3])
while 1:
nArg = len(layer.args) - 1
c = 0
while c < nArg:
if ( layer.args[c].count('(')!=
layer.args[c].count(')')):
tmp=layer.args[c+1]
layer.args.remove(tmp)
layer.args[c]=layer.args[c].strip()+\
','+tmp.strip()
nArg = nArg-1
break;
c = c + 1
if c == nArg:
break;
if len(layer.args)==1 and len(layer.args[0])==0:
layer.args = []
else:
layer.args = []
if layer.kw[1]<= len(layer.args) <= layer.kw[2]:
layer.args = [a.strip() for a in layer.args]
if layer.kw[4]<parsing.RUN:
#
# this is where old-style commands are invoked
#
self.result=layer.kw[0](*layer.args)
#
elif (layer.kw[4]==parsing.EMBED):
layer.next = []
if secure or self.nest==0: # only legal on top level and p1m files
l = len(layer.args)
if l>0:
key = layer.args[0]
else:
key = self.get_default_key()
if l>1:
format = layer.args[1]
else:
format = 'pdb'
if l>2:
layer.embed_sentinel = layer.args[2]
else:
layer.embed_sentinel = "embed end"
list = []
layer.embed_dict[key] = ( format, list )
layer.embed_list = list
layer.embed_type = 0 # not a python block
else:
print('Error: embed only legal in special files (e.g. p1m)')
raise None
elif (layer.kw[4]==parsing.SKIP):
layer.next = []
arg = parsing.apply_arg(
parsing.parse_arg(layer.com2,_self=self.cmd),
('sentinel',),
{'sentinel':'skip end'})
print(arg) # ???
if len(layer.args):
if layer.args[0]=='end': # probable 'skip end' to ignore
arg = []
if len(arg):
layer.embed_sentinel = arg[0]
layer.embed_type = 2 # skip block
layer.embed_line = 0
elif (layer.kw[4]==parsing.PYTHON_BLOCK):
layer.next = []
if not secure:
arg = parsing.apply_arg(
parsing.parse_arg(layer.com2,_self=self.cmd),
('sentinel','skip'),
{'sentinel':'python end','skip':0})
layer.embed_sentinel = arg[0]
list = []
layer.embed_list = list
if arg[1]:
layer.embed_type = 2 # skip block
else:
layer.embed_type = 1 # python block
layer.embed_line = 0
else:
print('Error: Python blocks disallowed in this file.')
raise None
else:
print('Error: unknown keyword mode: '+str(layer.kw[4]))
raise QuietException
else:
print('Error: invalid arguments for %s command.' % com)
#
# non-keyword command handling
#
elif len(layer.input[0]):
if layer.input[0][0]=='@':
path = exp_path(layer.com2[1:].strip())
if path[-3:].lower()=='p1m':
nest_securely = 1
else:
nest_securely = secure
if re.search("\.py$|\.pym$",path) != None:
if self.cmd._feedback(fb_module.parser,fb_mask.warnings):
print("Warning: use 'run' instead of '@' with Python files?")
layer.script = open(path,'rU')
self.cmd._pymol.__script__ = path
self.nest=self.nest+1
self.layer[self.nest] = NestLayer()
layer = self.layer[self.nest]
layer.cont=''
layer.sc_path=path
layer.embed_sentinel=None
while 1:
layer.com0 = self.layer[self.nest-1].script.readline()
self.layer[self.nest].lineno += 1
if not layer.com0: break
inp_cmd = layer.com0
tmp_cmd = inp_cmd.strip()
if len(tmp_cmd):
if tmp_cmd[0] not in ['#','_','/']: # suppress comments, internals, python
if layer.embed_sentinel==None:
colorprinting.parrot("PyMOL>"+tmp_cmd)
elif tmp_cmd[0]=='_' and \
tmp_cmd[1:2] in [' ','']: # "_ " remove echo suppression signal
inp_cmd=inp_cmd[2:]
pp_result = self.parse(inp_cmd,nest_securely)
if pp_result==None: # RECURSION
break # abort command gets us out
elif pp_result==0: # QuietException
if self.cmd.get_setting_boolean("stop_on_exceptions"):
p_result = 0 # signal an error occurred
colorprinting.error("PyMOL: stopped on exception.")
break;
self.nest=self.nest-1
layer=self.layer[self.nest]
layer.script.close()
self.cmd._pymol.__script__ = layer.sc_path
else: # nothing found, try literal python
layer.com2 = layer.com2.strip()
if len(layer.com2)>0:
if not secure:
self.exec_python(layer.com2, fallback=True)
elif layer.input[0][0:1]!='#':
colorprinting.error('Error: unrecognized keyword: '+layer.input[0])
if (len(layer.next)>1) and p_result:
# continue parsing if no error or break has occurred
self.nest=self.nest+1
self.layer[self.nest] = NestLayer()
layer=self.layer[self.nest]
layer.com0 = self.layer[self.nest-1].next[1]
self.layer[self.nest-1].next=[]
layer.cont=''
layer.embed_sentinel=None
p_result = self.parse(layer.com0,secure) # RECURSION
self.nest=self.nest-1
layer=self.layer[self.nest]
except (QuietException, CmdException) as e:
if e.args:
colorprinting.error(e)
if self.cmd._feedback(fb_module.parser,fb_mask.blather):
print("Parser: caught " + type(e).__name__)
p_result = 0
except SecurityException as e:
colorprinting.error('Error: %s' % (e,))
p_result = None
except:
exc_type, exc_value, tb = colorprinting.print_exc(
[__file__, SCRIPT_TOPLEVEL])
p_result = 0 # notify caller that an error was encountered
if not p_result and self.cmd._pymol.invocation.options.exit_on_error:
self.cmd.quit(1)
return p_result # 0 = Exception, None = abort, 1 = ok
def get_embedded(self,key=None):
layer = self.layer[self.nest]
dict = layer.embed_dict
if key==None:
key = self.get_default_key()
return dict.get(key,None)
def get_default_key(self):
layer = self.layer[self.nest]
return os.path.splitext(os.path.basename(layer.sc_path))[0]
def stdin_reader(self): # dedicated thread for reading standard input
import sys
while 1:
try:
l = sys.stdin.readline()
except IOError:
continue
if l!="":
if self.nest==0:
# if we're reading embedded input on stdin
# then bypass PyMOL C code altogether
if self.layer[0].embed_sentinel!=None:
self.parse(l)
else:
self.cmd.do(l, flush=True)
else:
self.cmd.do(l, flush=True)
elif not self.cmd._pymol.invocation.options.keep_thread_alive:
self.cmd.quit()
else:
import time
time.sleep(.1)
self.cmd._pymol._stdin_reader_thread = None
def complete(self,st):
with self.cmd.lockcm:
return self._complete(st)
def _complete(self,st):
result = None
pre = ''
flag = 0
if not (' ' in st or '@' in st):
try:
result = complete_sc(st, self.cmd.kwhash, 'commands',' ', 1)
except:
traceback.print_exc()
else:
full = self.cmd.kwhash.interpret(re.sub(r" .*","",st))
st_no_lists = remove_lists_re.sub("",st)
count = st_no_lists.count(',') # which argument are we on
if self.cmd.is_string(full):
try:
if count<len(self.cmd.auto_arg):
if full in self.cmd.auto_arg[count]: # autocomplete arguments
flag = 1
pre = re.sub(r"^[^ ]* ",' ',st,count=1) # trim command
if re.search(r",",pre)!=None:
pre = re.sub(r"[^\, ]*$","",pre,count=1)
pre = re.sub(r",\s*[^\, ]*$",", ",pre,count=1) # trim 1 arg
else:
pre = re.sub("[^ ]*$","",pre,count=1) # trim 1 arg
pre = re.sub(r"^ *",'',pre)
pre = full+' '+pre
pat = re.sub(r".*[\, ]",'',st)
# print ":"+pre+":"+pat+":"
# print tuple([pat] + self.cmd.auto_arg[count][full])
result = complete_sc(*tuple([pat] + self.cmd.auto_arg[count][full]), **{})
except:
traceback.print_exc()
if not flag: # otherwise fallback onto filename completion
st = self.cmd.as_pathstr(st)
loc = 1 + max(map(st.rfind, ',@'))
if not loc:
loc = 1 + st.find(' ')
pre = st[:loc]
st3 = st[loc:].lstrip()
flist = glob.glob(exp_path(st3)+"*")
# environment variable completion
if not flist and st3.startswith('$'):
flist = ['$' + var for var in os.environ
if var.startswith(st3[1:])]
lf = len(flist)
if lf == 0:
print(" parser: no matching files.")
elif lf==1:
result = flist[0]
if os.path.isdir(flist[0]):
result += '/' # do not use os.path.sep here
else:
flist.sort()
print(" parser: matching files:")
lst = parsing.list_to_str_list(flist)
for a in lst:
print(a)
# now append as much up to point of ambiguity
css = os.path.commonprefix(flist)
if len(css)>len(st3):
result = css
if result!=None:
result = pre+result
return result
def new_parse_closure(self_cmd): # create parser and return an instance-specific parse function closure
try:
p = Parser(self_cmd)
except:
traceback.print_exc()
self_cmd._parser = p
return lambda s,secure,p=p:p.parse(s,secure)
def new_complete_closure(self_cmd): # return an instance-specific complete function closure
return lambda st,p=self_cmd._parser:p.complete(st)
# unused code?
#
# def _same_(a,b):
# if a==b:
# return a
# else:
# return None