#! /usr/bin/env python3 ##@namespace produtil.testing.parse # This module contains the parser for the produtil.testing test suite. # # The main interface to the produtil.testing parser is # produtil.testing.parse.Parser.parse(). It constructs an internal # tree representation of a file, provided by the # produtil.testing.tokenizer.Tokenizer. The tree can then be # converted to a workflow by produtil.testing.rocoto or # produtil.testing.script. import sys, re, io, collections, os, datetime, logging, math import produtil.run, produtil.log, produtil.setup # This module really does use everything public from utilities, # parsetree and tokenize, hence the "import *" from produtil.testing.utilities import * from produtil.testing.parsetree import * from produtil.testing.tokenize import * from produtil.testing.setarith import arithparse __all__=[ 'Parser' ] ######################################################################## class RunConPair(object): """!A tuple-like object of a runnable object and a produtil.testing.parsetree.Context. This is used to store something that can be run, such as a compilation command or batch job, and the context from which its execution was requested.""" def __init__(self,runnable,context): """!Constructor for RunConPair @param runnable The runnable object for self.runnable @param context The produtil.testing.parsetree.Context for self.context""" self.runnable=runnable self.context=context self.__requested_platform=None def set_requested_platform_name(self,platform): self.__requested_platform_name=str(platform) def get_requested_platform_name(self): return self.__requested_platform_name def del_requested_platform_name(self): self.__requested_platform=None requested_platform_name=property(get_requested_platform_name, set_requested_platform_name, del_requested_platform_name) @property def as_tuple(self): """!A tuple self.runnable,self.context""" return self.runnable,self.context def __iter__(self): yield self.runnable yield self.context def __hash__(self): return hash(self.runnable) def __eq__(self,other): if not isinstance(other,RunConPair): return self.runnable==other return self.runnable==other.runnable # Replacements for __cmp__: def __lt__(self,other): if not isinstance(other,RunConPair): return self.runnableother return self.runnable>other.runnable def __ne__(self,other): return not ( self == other ) def __ge__(self,other): return self>other or self==other def __le__(self,other): return self1: raise PTPlatformError( 'This machine can submit to multiple platforms: '+( ' '.join([ s.resolve('PLATFORM_NAME') \ .string_context( produtil.testing.parsetree.fileless_context( verbose=self.verbose)) \ for s in matches ]))) return matches[0] def action_dependency(self,task,scopes,dep): """!Adds the specified dependency to the specified task. @param task The task for whom the dependency is to be added. @param dep The dependency @param scopes The surrounding scopes (presently unused)""" task.add_dependency(dep) def action_call(self,varname,token,scopes,parameters): """!Creates a scope that represents a function call. @param varname The function to call. @param parameters The arguments to the function. @param token the context from which the function call is requested. @param scopes a list of nested scopes (outermost first) surrounding the call @returns a scope representing the given function call""" # yell('%-7s %s in parameter scope %s\n'%( # 'CALL',repr(varname),repr(parameters))) callme=scopes[0].resolve(varname) # yell('CALL APPLY PARAMETERS\n') return callme.apply_parameters(parameters,self.con(token,scopes)) def action_use(self,scopes,key_token,only_scalars=False): """!Performs the action of a "use" block within a subscope. Adds variables to scope[0] from another scope. The scope is defined by key_token, a name that is resolved using scopes surrounding scope[0] @param scopes The relevant scopes: scope[0] is the scope that is "using," while the other scopes are those surrounding scope[0]. @param key_token The token containing the name of the scope that is to be used. @param only_scalars If True, then it is an error to "use" a scope that contains variables with non-scalar values. @returns None""" assert(isinstance(key_token,Token)) assert(isinstance(scopes,list)) assert(len(scopes)>=2) assert(isinstance(scopes[0],Scope)) key=key_token.token_value got=scopes[1].resolve(key) found_non_scalars=scopes[0].use_from(got,only_scalars) if only_scalars and found_non_scalars: self.error('use',key_token,'found non-scalars when ' 'using %s'%(key,)) # for k,v in got.iterlocal(): # if only_scalars and not isinstance(v,String): # self.error('use',key_token,'found non-scalars when ' # 'using %s'%(key,)) # scopes[0].setlocal(k,v) def action_operator(self,scopes,token): """!Creates an object that represents the operator ".oper." in the expression "a .oper. b" @param scopes a list of nested scopes (outermost first) surrounding the expression @param token the token containing the name of the operator @returns an object that represents the operator""" assert(isinstance(token,Token)) if token.token_value=='.copy.': return Copy(scopes) elif token.token_value=='.copydir.' or token.token_value=='.copyfrom.': return CopyDir(scopes) elif token.token_value=='.bitcmp.': return BitCmp(scopes) elif token.token_value=='.nccmp_vars.': return NccmpVars(scopes) elif token.token_value=='.md5cmp.': return Md5Cmp(scopes) elif token.token_value=='.link.': return Link(scopes) elif token.token_value=='.atparse.': return AtParse(scopes) else: self.error('operator name',token,'unknown operator '+ token.token_value) def action_numeric(self,scopes,token): """!Returns a Numeric object for the given token. @param scopes a list of nested scopes (outermost first) surrounding the number @param token The token containing the number @returns a Numeric object""" value=float(token.token_value) return Numeric(value) def action_string(self,scopes,token): """!Returns a String object for the given token. @param scopes a list of nested scopes (outermost first) surrounding the string @param token The token containing the string @returns a String object""" assert(isinstance(token,Token)) if token.token_type=='qstring': s=String(scopes,token.token_value,False) elif token.token_type=='dqstring': s=String(scopes,dqstring2bracestring(token.token_value),True) elif token.token_type=='bracestring': s=String(scopes,token.token_value,True) else: raise ValueError('Invalid token for a string: '+repr(token)) # yell('%-7s %s = %s\n'%('STRING',repr(token.token_value),repr(s))) return s def action_reference(self,varname_token,scopes): return Reference(varname_token.token_value,scopes) def action_resolve(self,varname_token,scopes): """!Resolves a variable reference within a scope. @param varname_token The token containing the name of the variable reference. @param scopes a list of nested scopes (outermost first) surrounding the reference @returns the value of the referenced variable @raise PTKeyError if no such variable exists""" varname=varname_token.token_value for scope in scopes: try: return scope.resolve(varname) except PTKeyError as ke: pass raise PTKeyError(varname) def action_null_param(self,varname,scope): """!Defines a variable with no value in the given scope. @param varname the name of the variable, a string @param scope the scope in which to define the variable @returns None""" if '%' in varname: raise ValueError('%s: cannot have "%" in a parameter name.'%( varname,)) scope.check_define(varname,null_value) def action_assign_var(self,toscope,tovar,fromvar,fromscopes, allow_overwrite): """!Assigns a value to a variable within a scope, from another variable in another scope. @param toscope The scope in which a variable is being defined. @param tovar The target variable name @param fromvar The variable reference to the source variable @param fromscopes The scopes, innermost first, in which to resolve the fromvar variable reference @param allow_overwrite If True, redefining tovar within toscope is allowed.""" if fromscopes: value=fromscopes[0].resolve(fromvar) else: # Global scope assignment value=toscope.resolve(fromvar) self.action_assign(toscope,tovar,value,allow_overwrite) def action_assign(self,scope,varname,value,allow_overwrite): """!Assigns a value to a variable within a scope @param scope The scope in which to assign. @param varname The name of the variable within the scope, a string. @param value The value of the variable, a BaseObject @param allow_overwrite If True, replacing an existing variable's value is allowed. @return None""" assert(isinstance(scope,Scope)) assert(isinstance(varname,str)) assert(isinstance(value,BaseObject)) if '%' in varname: raise ValueError('Cannot assign to %s; subscope definitions must be of syntax "var1 = { var2= { ...."'%( varname,)) # yell('%-7s %s = %s IN %s\n'%( # 'ASSIGN', varname, repr(value),repr(scope) )) if allow_overwrite: scope.force_define(varname,value) else: scope.check_define(varname,value) def action_run_in_set(self,setname,obj,con): """!Requests that a specified object be available for running, within a specific runset. This is used to implement the part list of runsets in the "run" statement. @param setname The name of the set in which to run. @param obj The object to run. @param con The produtil.testing.parsetree.Context from which the run statement is made.""" # yell('%-7s %s\n'%( # 'RUN', repr(obj))) return self.add_run(setname,obj,con) def action_run_by_name(self,obj,con): """!Requests that a specified object be available for running, without putting it in any runset. This is used to implement the part of the "run" statement before set lists. @param obj The object to run. @param con The produtil.testing.parsetree.Context from which the run statement is made.""" return self.add_run(None,obj,con) def error(self,mode,token,reason=None): """!Convenience function for error messages. Raises an exception explaining that an input file contains an error. This is used for most syntax or semantic errors. @param mode The run mode, which can be anything that can be cast to a string via str() @param token The token at which the error occurred, or None if the end of the file was reached. @param reason @returns Never. Always raises an exception.""" if token is None: raise PTParserError('Unexpected end of file.') elif reason: raise PTParserError('%s:%s: %s (%s token with value %s)'%( token.filename,token.lineno,str(reason), repr(token.token_type), repr(elipses(str(token.token_value))))) else: raise PTParserError( '%s:%s: unexpected %s in %s (token value %s)'%( token.filename, token.lineno, repr(token.token_type), str(mode), repr(elipses(str(token.token_value)))))