"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "modules/pymol/parser.py" between
pymol-v1.8.6.0.tar.bz2 and pymol-v2.1.0.tar.bz2

About: PyMOL is a Python-enhanced molecular graphics tool. It excels at 3D visualization of proteins, small molecules, density, surfaces, and trajectories. It also includes molecular editing, ray tracing, and movies. Open Source version.

parser.py  (pymol-v1.8.6.0.tar.bz2):parser.py  (pymol-v2.1.0.tar.bz2)
skipping to change at line 23 skipping to change at line 23
#Z* ------------------------------------------------------------------- #Z* -------------------------------------------------------------------
# parser.py # parser.py
# Python parser module for PyMol # Python parser module for PyMol
# #
from __future__ import absolute_import from __future__ import absolute_import
# Don't import __future__.print_function # Don't import __future__.print_function
class SecurityException(Exception):
pass
SCRIPT_TOPLEVEL = 'toplevel'
if __name__=='pymol.parser': if __name__=='pymol.parser':
import pymol import pymol
import traceback import traceback
import re import re
import glob import glob
import sys import sys
import os import os
import __main__ import __main__
from . import parsing from . import parsing
from . import colorprinting
from .cmd import _feedback,fb_module,fb_mask,exp_path from .cmd import _feedback,fb_module,fb_mask,exp_path
QuietException = parsing.QuietException QuietException = parsing.QuietException
CmdException = pymol.CmdException CmdException = pymol.CmdException
py_delims = { '=' : 1, '+=' : 1, '-=' : 1, '*=' : 1, py_delims = { '=' : 1, '+=' : 1, '-=' : 1, '*=' : 1,
'/=' :1, '//=' : 1, '%=' : 1, '&=' : 1, '/=' :1, '//=' : 1, '%=' : 1, '&=' : 1,
'|=' :1, '^=' : 1, '>>=' : 1,'<<=' : 1, '|=' :1, '^=' : 1, '>>=' : 1,'<<=' : 1,
'**=':1 } '**=':1 }
skipping to change at line 55 skipping to change at line 61
def complete_sc(st,sc,type_name,postfix, mode=0): def complete_sc(st,sc,type_name,postfix, mode=0):
result = None result = None
try: try:
sc=sc() # invoke lambda functions (if any) sc=sc() # invoke lambda functions (if any)
except: except:
traceback.print_exc() traceback.print_exc()
amb = sc.interpret(st, mode) amb = sc.interpret(st, mode)
if amb==None: if amb==None:
print(" parser: no matching %s."%type_name) colorprinting.warning(" parser: no matching %s."%type_name)
elif isinstance(amb, str): elif isinstance(amb, str):
result = amb+postfix result = amb+postfix
else: else:
amb.sort() amb.sort()
print(" parser: matching %s:"%type_name) colorprinting.suggest(" parser: matching %s:"%type_name)
flist = [x for x in amb if x[0]!='_'] flist = [x for x in amb if x[0]!='_']
lst = parsing.list_to_str_list(flist) lst = parsing.list_to_str_list(flist)
for a in lst: for a in lst:
print(a) colorprinting.suggest(a)
# now append up to point of ambiguity # now append up to point of ambiguity
if not len(flist): if not len(flist):
css = [] css = []
else: else:
css = list(flist[0]) # common sub-string (css) css = list(flist[0]) # common sub-string (css)
for a in flist: for a in flist:
ac = list(a) ac = list(a)
tmp = css tmp = css
css = [] css = []
for c in range(len(tmp)): for c in range(len(tmp)):
skipping to change at line 90 skipping to change at line 96
if len(css)>len(st): if len(css)>len(st):
result = css result = css
return result return result
class NestLayer: class NestLayer:
def __init__(self): def __init__(self):
self.cont = "" self.cont = ""
self.com0 = "" self.com0 = ""
self.sc_path = "toplevel" self.sc_path = SCRIPT_TOPLEVEL
self.lineno = 0
self.literal_python_fallback = False
self.embed_sentinel = None self.embed_sentinel = None
self.embed_dict = {} self.embed_dict = {}
self.next = [] self.next = []
class Parser: class Parser:
def __init__(self,cmd): def __init__(self,cmd):
cmd = cmd._weakrefproxy
self.cmd = cmd self.cmd = cmd
self.nest = 0 self.nest = 0
self.layer = { 0: NestLayer() } self.layer = { 0: NestLayer() }
self.pymol_names = self.cmd._pymol.__dict__ self.pymol_names = self.cmd._pymol.__dict__
# parsing state implemented with dictionaries to enable safe recursi on # parsing state implemented with dictionaries to enable safe recursi on
# to arbitrary depths # to arbitrary depths
#com0 = {} # verbose line, as read in #com0 = {} # verbose line, as read in
skipping to change at line 133 skipping to change at line 142
# The resulting value from a pymol command (if any) is stored in the # The resulting value from a pymol command (if any) is stored in the
# parser.result global variable. However, script developers will # parser.result global variable. However, script developers will
# geerally want to switch to the Python API for any of this kind of # geerally want to switch to the Python API for any of this kind of
# stuff. # stuff.
self.result = None self.result = None
# initialize parser # initialize parser
self.cmd._pymol.__script__="toplevel" self.cmd._pymol.__script__ = SCRIPT_TOPLEVEL
def exec_python(self, s, secure=False, fallback=False):
if secure:
raise SecurityException('Python expressions disallowed in this f
ile')
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 # main parser routine
def parse(self,s,secure=0): def parse(self,s,secure=0):
layer = self.layer.get(self.nest,None) layer = self.layer.get(self.nest,None)
self.result = None self.result = None
# report any uncaught errors... # report any uncaught errors...
# WLD: this is problematic if parse is called inside an exception...removed. # WLD: this is problematic if parse is called inside an exception...removed.
# if sys.exc_info()!=(None,None,None): # if sys.exc_info()!=(None,None,None):
# traceback.print_exc() # traceback.print_exc()
# sys.exc_clear() # sys.exc_clear()
if layer.embed_sentinel!=None: def parse_embed():
if s.strip() == layer.embed_sentinel: if s.strip() == layer.embed_sentinel:
etn = layer.embed_type etn = layer.embed_type
if etn == 0: # embedded data if etn == 0: # embedded data
print(" Embed: read %d lines."%(len(layer.embed_list))) colorprinting.parrot(" Embed: read %d lines."%(len(layer .embed_list)))
layer.embed_sentinel=None layer.embed_sentinel=None
elif etn == 1: # python block elif etn == 1: # python block
print("PyMOL>"+s.rstrip()) colorprinting.parrot("PyMOL>"+s.rstrip())
py_block = ''.join(layer.embed_list) py_block = ''.join(layer.embed_list)
del layer.embed_list del layer.embed_list
layer.embed_sentinel=None layer.embed_sentinel=None
try: self.exec_python(py_block)
exec(py_block,self.pymol_names,self.pymol_names)
except:
traceback.print_exc()
elif etn == 2: # skip block elif etn == 2: # skip block
print(" Skip: skipped %d lines."%(layer.embed_line)) colorprinting.parrot(" Skip: skipped %d lines."%(layer.e mbed_line))
layer.embed_sentinel=None layer.embed_sentinel=None
else: else:
etn = layer.embed_type etn = layer.embed_type
if etn == 0: # normal embedded data if etn == 0: # normal embedded data
layer.embed_list.append(s.rstrip()+"\n") layer.embed_list.append(s.rstrip()+"\n")
elif etn == 1: # python block elif etn == 1: # python block
el = layer.embed_line + 1 el = layer.embed_line + 1
print("%5d:%s"%(el,s.rstrip())) colorprinting.parrot("%5d:%s"%(el,s.rstrip()))
layer.embed_line = el layer.embed_line = el
layer.embed_list.append(s.rstrip()+"\n") layer.embed_list.append(s.rstrip()+"\n")
elif etn == 2: elif etn == 2:
layer.embed_line = layer.embed_line + 1 layer.embed_line = layer.embed_line + 1
return 1
p_result = 1 p_result = 1
layer.com0 = s layer.com0 = s
try: try:
if layer.embed_sentinel is not None:
parse_embed()
return 1
layer.com1 = layer.com0.rstrip() # strips trailing whitespace layer.com1 = layer.com0.rstrip() # strips trailing whitespace
if len(layer.com1) > 0: if len(layer.com1) > 0:
if str(layer.com1[-1]) == "\\": if str(layer.com1[-1]) == "\\":
# prepend leftovers # prepend leftovers
if layer.cont != '': if layer.cont != '':
layer.cont = layer.cont + "\n" + layer.com1[:-1] layer.cont = layer.cont + "\n" + layer.com1[:-1]
else: else:
layer.cont = layer.com1[:-1] layer.cont = layer.com1[:-1]
else: else:
# prepend leftovers # prepend leftovers
skipping to change at line 206 skipping to change at line 230
layer.com2 = layer.next[0] layer.com2 = layer.next[0]
layer.input = layer.com2.split(' ',1) layer.input = layer.com2.split(' ',1)
lin = len(layer.input) lin = len(layer.input)
if lin: if lin:
layer.input[0] = layer.input[0].strip() layer.input[0] = layer.input[0].strip()
com = layer.input[0] com = layer.input[0]
if (com[0:1]=='/'): if (com[0:1]=='/'):
# explicit literal python # explicit literal python
layer.com2 = layer.com2[1:].strip() layer.com2 = layer.com2[1:].strip()
if len(layer.com2)>0: if len(layer.com2)>0:
if not secure: self.exec_python(layer.com2, secure)
exec(layer.com2+"\n",self.pymol_names,se
lf.pymol_names)
else:
print('Error: Python expressions disallo
wed in this file.')
return None
elif lin>1 and layer.input[-1:][0].split(' ',1)[0] i n py_delims: elif lin>1 and layer.input[-1:][0].split(' ',1)[0] i n py_delims:
if not secure: self.exec_python(layer.com2, secure)
exec(layer.com2+"\n",self.pymol_names,self.p
ymol_names)
else:
print('Error: Python expressions disallowed
in this file.')
return None
else: else:
# try to find a keyword which matches # try to find a keyword which matches
if com in self.cmd.kwhash: if com in self.cmd.kwhash:
amb = self.cmd.kwhash.interpret(com) amb = self.cmd.kwhash.interpret(com)
if amb == None: if amb == None:
com = self.cmd.kwhash[com] com = self.cmd.kwhash[com]
elif not isinstance(amb, str): elif not isinstance(amb, str):
print('Error: ambiguous command: ') colorprinting.warning('Error: ambiguous command: ')
amb.sort() amb.sort()
amb = parsing.list_to_str_list(amb) amb = parsing.list_to_str_list(amb)
for a in amb: for a in amb:
print(a) colorprinting.warning(a)
raise QuietException raise QuietException
com = amb com = amb
if com in self.cmd.keyword: if com in self.cmd.keyword:
# here is the command and argument handling section # here is the command and argument handling section
layer.kw = self.cmd.keyword[com] layer.kw = self.cmd.keyword[com]
if layer.kw[4]>=parsing.NO_CHECK: if layer.kw[4]>=parsing.NO_CHECK:
# stricter, Python-based argument parsin g # stricter, Python-based argument parsin g
# remove line breaks (only important for Python expressions) # remove line breaks (only important for Python expressions)
layer.com2=layer.com2.replace('\n','') layer.com2=layer.com2.replace('\n','')
if layer.kw[4]>=parsing.LITERAL: # treat literally if layer.kw[4]>=parsing.LITERAL: # treat literally
layer.next = [] layer.next = []
if not secure: if not secure:
layer.com2=layer.com1 layer.com2=layer.com1
else: else:
print('Error: Python expressions raise SecurityException('Python
disallowed in this file. ') expressions disallowed in this file')
return 0
if secure and (layer.kw[4]==parsing.SECU RE): if secure and (layer.kw[4]==parsing.SECU RE):
layer.next = [] layer.next = []
print('Error: Command disallowed in raise SecurityException('Command dis
this file.') allowed in this file')
return None
else: else:
(layer.args, layer.kw_args) = \ (layer.args, layer.kw_args) = \
parsing.prepare_call( parsing.prepare_call(
layer.kw[0], layer.kw[0],
parsing.parse_arg(layer.com2,mode=l ayer.kw[4],_self=self.cmd), parsing.parse_arg(layer.com2,mode=l ayer.kw[4],_self=self.cmd),
layer.kw[4], _self=self.cmd) # will raise exception on failure layer.kw[4], _self=self.cmd) # will raise exception on failure
self.result=layer.kw[0](*layer.args, **l ayer.kw_args) self.result=layer.kw[0](*layer.args, **l ayer.kw_args)
elif layer.kw[4]==parsing.PYTHON: elif layer.kw[4]==parsing.PYTHON:
# handle python keyword # handle python keyword
layer.com2 = layer.com2.strip() layer.com2 = layer.com2.strip()
if len(layer.com2)>0: if len(layer.com2)>0:
if not secure: self.exec_python(layer.com2, sec
exec(layer.com2+"\n",self.py ure)
mol_names,self.pymol_names)
else:
layer.next = []
print('Error: Python express
ions disallowed in this file.')
return None
else: else:
# remove line breaks (only important for Python expressions) # remove line breaks (only important for Python expressions)
layer.com2=layer.com2.replace('\n','') layer.com2=layer.com2.replace('\n','')
# old parsing style, being phased out # old parsing style, being phased out
if layer.kw[4]==parsing.ABORT: if layer.kw[4]==parsing.ABORT:
return None # SCRIPT ABORT EXIT POIN T return None # SCRIPT ABORT EXIT POIN T
if layer.kw[4]==parsing.MOVIE: # copy li teral single line, no breaks if layer.kw[4]==parsing.MOVIE: # copy li teral single line, no breaks
layer.next = [] layer.next = []
if not secure: if not secure:
layer.input = layer.com1.split(' ',1) layer.input = layer.com1.split(' ',1)
else: else:
print('Error: Movie commands dis raise SecurityException('Movie c
allowed in this file. ') ommands disallowed in this file')
return None
if len(layer.input)>1: if len(layer.input)>1:
layer.args = parsing.split(layer.inp ut[1],layer.kw[3]) layer.args = parsing.split(layer.inp ut[1],layer.kw[3])
while 1: while 1:
nArg = len(layer.args) - 1 nArg = len(layer.args) - 1
c = 0 c = 0
while c < nArg: while c < nArg:
if ( layer.args[c].count('(' )!= if ( layer.args[c].count('(' )!=
layer.args[c].count(')' )): layer.args[c].count(')' )):
tmp=layer.args[c+1] tmp=layer.args[c+1]
layer.args.remove(tmp) layer.args.remove(tmp)
skipping to change at line 316 skipping to change at line 324
# #
self.result=layer.kw[0](*layer.a rgs) self.result=layer.kw[0](*layer.a rgs)
# #
elif (layer.kw[4]==parsing.EMBED): elif (layer.kw[4]==parsing.EMBED):
layer.next = [] layer.next = []
if secure or self.nest==0: # onl y legal on top level and p1m files if secure or self.nest==0: # onl y legal on top level and p1m files
l = len(layer.args) l = len(layer.args)
if l>0: if l>0:
key = layer.args[0] key = layer.args[0]
else: else:
key = os.path.splitext(o s.path.basename(layer.sc_path))[0] key = self.get_default_k ey()
if l>1: if l>1:
format = layer.args[1] format = layer.args[1]
else: else:
format = 'pdb' format = 'pdb'
if l>2: if l>2:
layer.embed_sentinel = l ayer.args[2] layer.embed_sentinel = l ayer.args[2]
else: else:
layer.embed_sentinel = " embed end" layer.embed_sentinel = " embed end"
list = [] list = []
layer.embed_dict[key] = ( fo rmat, list ) layer.embed_dict[key] = ( fo rmat, list )
skipping to change at line 382 skipping to change at line 390
elif len(layer.input[0]): elif len(layer.input[0]):
if layer.input[0][0]=='@': if layer.input[0][0]=='@':
path = exp_path(layer.com2[1:].strip()) path = exp_path(layer.com2[1:].strip())
if path[-3:].lower()=='p1m': if path[-3:].lower()=='p1m':
nest_securely = 1 nest_securely = 1
else: else:
nest_securely = secure nest_securely = secure
if re.search("\.py$|\.pym$",path) != Non e: if re.search("\.py$|\.pym$",path) != Non e:
if self.cmd._feedback(fb_module.pars er,fb_mask.warnings): if self.cmd._feedback(fb_module.pars er,fb_mask.warnings):
print("Warning: use 'run' instea d of '@' with Python files?") print("Warning: use 'run' instea d of '@' with Python files?")
layer.script = open(path,'r') layer.script = open(path,'rU')
self.cmd._pymol.__script__ = path self.cmd._pymol.__script__ = path
self.nest=self.nest+1 self.nest=self.nest+1
self.layer[self.nest] = NestLayer() self.layer[self.nest] = NestLayer()
layer = self.layer[self.nest] layer = self.layer[self.nest]
layer.cont='' layer.cont=''
layer.sc_path=path layer.sc_path=path
layer.embed_sentinel=None layer.embed_sentinel=None
while 1: while 1:
layer.com0 = self.layer[self.nest-1 ].script.readline() layer.com0 = self.layer[self.nest-1 ].script.readline()
self.layer[self.nest].lineno += 1
if not layer.com0: break if not layer.com0: break
inp_cmd = layer.com0 inp_cmd = layer.com0
tmp_cmd = inp_cmd.strip() tmp_cmd = inp_cmd.strip()
if len(tmp_cmd): if len(tmp_cmd):
if tmp_cmd[0] not in ['#','_','/ ']: # suppress comments, internals, python if tmp_cmd[0] not in ['#','_','/ ']: # suppress comments, internals, python
if layer.embed_sentinel==Non e: if layer.embed_sentinel==Non e:
print("PyMOL>"+tmp_cmd) colorprinting.parrot("Py MOL>"+tmp_cmd)
elif tmp_cmd[0]=='_' and \ elif tmp_cmd[0]=='_' and \
tmp_cmd[1:2] in [' ','']: # "_ " remove echo suppression signal tmp_cmd[1:2] in [' ','']: # "_ " remove echo suppression signal
inp_cmd=inp_cmd[2:] inp_cmd=inp_cmd[2:]
pp_result = self.parse(inp_cmd,nest_ securely) pp_result = self.parse(inp_cmd,nest_ securely)
if pp_result==None: # RECURSION if pp_result==None: # RECURSION
break # abort command gets us ou t break # abort command gets us ou t
elif pp_result==0: # QuietException elif pp_result==0: # QuietException
if self.cmd.get_setting_boolean( "stop_on_exceptions"): if self.cmd.get_setting_boolean( "stop_on_exceptions"):
p_result = 0 # signal an err or occurred p_result = 0 # signal an err or occurred
print("PyMOL: stopped on exc eption.") colorprinting.error("PyMOL: stopped on exception.")
break; break;
self.nest=self.nest-1 self.nest=self.nest-1
layer=self.layer[self.nest] layer=self.layer[self.nest]
layer.script.close() layer.script.close()
self.cmd.__script__ = layer.sc_path self.cmd._pymol.__script__ = layer.sc_pa th
else: # nothing found, try literal python else: # nothing found, try literal python
layer.com2 = layer.com2.strip() layer.com2 = layer.com2.strip()
if len(layer.com2)>0: if len(layer.com2)>0:
if not secure: if not secure:
exec(layer.com2+"\n",self.pymol_ names,self.pymol_names) self.exec_python(layer.com2, fal lback=True)
elif layer.input[0][0:1]!='#': elif layer.input[0][0:1]!='#':
print('Error: unrecognized keywo rd: '+layer.input[0]) colorprinting.error('Error: unre cognized keyword: '+layer.input[0])
if (len(layer.next)>1) and p_result: if (len(layer.next)>1) and p_result:
# continue parsing if no error or break has occurred # continue parsing if no error or break has occurred
self.nest=self.nest+1 self.nest=self.nest+1
self.layer[self.nest] = NestLayer() self.layer[self.nest] = NestLayer()
layer=self.layer[self.nest] layer=self.layer[self.nest]
layer.com0 = self.layer[self.nest-1].next[1] layer.com0 = self.layer[self.nest-1].next[1]
self.layer[self.nest-1].next=[] self.layer[self.nest-1].next=[]
layer.cont='' layer.cont=''
layer.embed_sentinel=None layer.embed_sentinel=None
p_result = self.parse(layer.com0,secure) # RECURSION p_result = self.parse(layer.com0,secure) # RECURSION
self.nest=self.nest-1 self.nest=self.nest-1
layer=self.layer[self.nest] layer=self.layer[self.nest]
except QuietException: except (QuietException, CmdException) as e:
if self.cmd._feedback(fb_module.parser,fb_mask.blather): if e.args:
print("Parser: QuietException caught") colorprinting.error(e)
p_result = 0 # notify caller that an error was encountered
except CmdException as e:
if e.message:
print(e)
if self.cmd._feedback(fb_module.parser,fb_mask.blather): if self.cmd._feedback(fb_module.parser,fb_mask.blather):
print("Parser: CmdException caught.") print("Parser: caught " + type(e).__name__)
p_result = 0 p_result = 0
except SecurityException as e:
colorprinting.error('Error: %s' % (e,))
p_result = None
except: except:
traceback.print_exc() exc_type, exc_value, tb = colorprinting.print_exc(
if self.cmd._feedback(fb_module.parser,fb_mask.blather): [__file__, SCRIPT_TOPLEVEL])
print("PyMOL: Caught an unknown exception.")
p_result = 0 # notify caller that an error was encountered p_result = 0 # notify caller that an error was encountered
if not p_result and self.cmd._pymol.invocation.options.exit_on_error : if not p_result and self.cmd._pymol.invocation.options.exit_on_error :
self.cmd.quit(1) self.cmd.quit(1)
return p_result # 0 = Exception, None = abort, 1 = ok return p_result # 0 = Exception, None = abort, 1 = ok
def get_embedded(self,key=None): def get_embedded(self,key=None):
layer = self.layer[self.nest] layer = self.layer[self.nest]
dict = layer.embed_dict dict = layer.embed_dict
if key==None: if key==None:
key = self.get_default_key() key = self.get_default_key()
skipping to change at line 526 skipping to change at line 534
pre = re.sub("[^ ]*$","",pre,count=1) # trim 1 arg pre = re.sub("[^ ]*$","",pre,count=1) # trim 1 arg
pre = re.sub(r"^ *",'',pre) pre = re.sub(r"^ *",'',pre)
pre = full+' '+pre pre = full+' '+pre
pat = re.sub(r".*[\, ]",'',st) pat = re.sub(r".*[\, ]",'',st)
# print ":"+pre+":"+pat+":" # print ":"+pre+":"+pat+":"
# print tuple([pat] + self.cmd.auto_arg[count][fu ll]) # print tuple([pat] + self.cmd.auto_arg[count][fu ll])
result = complete_sc(*tuple([pat] + self.cmd.aut o_arg[count][full]), **{}) result = complete_sc(*tuple([pat] + self.cmd.aut o_arg[count][full]), **{})
except: except:
traceback.print_exc() traceback.print_exc()
if not flag: # otherwise fallback onto filename completion if not flag: # otherwise fallback onto filename completion
st = self.cmd.as_pathstr(st)
loc = 1 + max(map(st.rfind, ',@')) loc = 1 + max(map(st.rfind, ',@'))
if not loc: if not loc:
loc = 1 + st.find(' ') loc = 1 + st.find(' ')
pre = st[:loc] pre = st[:loc]
st3 = st[loc:].lstrip() st3 = st[loc:].lstrip()
flist = glob.glob(exp_path(st3)+"*") 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) lf = len(flist)
if lf == 0: if lf == 0:
print(" parser: no matching files.") print(" parser: no matching files.")
elif lf==1: elif lf==1:
result = flist[0] result = flist[0]
if os.path.isdir(flist[0]): if os.path.isdir(flist[0]):
result += '/' # do not use os.path.sep here result += '/' # do not use os.path.sep here
else: else:
flist.sort() flist.sort()
print(" parser: matching files:") print(" parser: matching files:")
 End of changes. 38 change blocks. 
66 lines changed or deleted 77 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)