test
CMS 3D CMS Logo

 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
TreeCrawler.py
Go to the documentation of this file.
1 # This CMS code is based on previous work done by Toby Dickenson, as indiciated below
2 #
3 # for questions: Benedikt.Hegner@cern.ch
4 
5 # Copyright 2004 Toby Dickenson
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining
8 # a copy of this software and associated documentation files (the
9 # "Software"), to deal in the Software without restriction, including
10 # without limitation the rights to use, copy, modify, merge, publish,
11 # distribute, sublicense, and/or sell copies of the Software, and to
12 # permit persons to whom the Software is furnished to do so, subject
13 # to the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be included
16 # in all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26 import sys, os, inspect, copy, struct, dis, imp
27 import modulefinder
28 
30  return ".".join(name.replace("python/","").replace(".py","").split("/")[-3:])
31 
32 
33 class Color:
34  """ANSI escape display sequences"""
35  info = "\033[1;34m"
36  hilight = "\033[31m"
37  alternate = "\033[32m"
38  extra = "\033[33m"
39  backlight = "\033[43m"
40  underline = "\033[4m"
41  lessemphasis = "\033[30m"
42  deemphasis = "\033[1;30m"
43  none = "\033[0m"
44 
45 _stack = []
46 
47 class SearchHit:
48  pass
49 
50 class Package(object):
51  def __init__(self,name,top=False):
52  self.name = name
53  self.dependencies = []
54  self.searched = False
55  self.stack = []
56  if top:
57  self.module = None
58  else:
59  self.module = __import__(name,[],[],"*")
60  def dump(self,level):
61  indent = " " * level
62  print indent, "+", Color.info, self.name, Color.none
63  # sort dependencies alphabetically
64  self.dependencies.sort(key = lambda x: x.name)
65  for package in self.dependencies:
66  package.dump(level+1)
67  def search(self,pattern,result):
68  """ recursive search for pattern in source files"""
69  # first start searching in the package itself / do this only once
70  if self.module:
71  for number, line in enumerate(inspect.getsource(self.module).splitlines()):
72  if pattern in line:
73  filename = packageNameFromFilename(inspect.getsourcefile(self.module))
74  if not self.searched:
75  # save the hit, so we can add later stacks to it
76  self.hit = SearchHit()
77  self.hit.number = number
78  self.hit.filename = filename
79  self.hit.line = line
80  self.hit.stacks = list()
81  result.append(self.hit)
82  self.hit.stacks.append(copy.copy(_stack))
83  # then go on with dependencies
84  _stack.append(self.name)
85  for package in self.dependencies:
86  package.search(pattern,result)
87  _stack.pop()
88  self.searched = True
89 
90 
91 class mymf(modulefinder.ModuleFinder):
92  def __init__(self,*args,**kwargs):
93  self._depgraph = {}
94  self._types = {}
95  self._last_caller = None
96  #TODO - replace by environment variables CMSSW_BASE and CMSSW_RELEASE_BASE (*and* do it only if the global one is not empty like for IB areas)
97  self._localarea = os.path.expandvars('$CMSSW_BASE')
98  self._globalarea = os.path.expandvars('$CMSSW_RELEASE_BASE')
99  modulefinder.ModuleFinder.__init__(self,*args,**kwargs)
100  def import_hook(self, name, caller=None, fromlist=None, level=-1):
101  old_last_caller = self._last_caller
102  try:
103  self._last_caller = caller
104  return modulefinder.ModuleFinder.import_hook(self,name,caller,fromlist, level=level)
105  finally:
106  self._last_caller = old_last_caller
107 
108  def import_module(self,partnam,fqname,parent):
109 
110  if partnam in ("os","unittest"):
111  r = None
112  else:
113  r = modulefinder.ModuleFinder.import_module(self,partnam,fqname,parent)
114  # since the modulefinder is not able to look into the global area when coming from the local area, we force a second try
115  if parent and not r and self._localarea != '' and self._globalarea != '':
116  parent.__file__ = parent.__file__.replace(self._localarea,self._globalarea)
117  parent.__path__[0] = parent.__path__[0].replace(self._localarea,self._globalarea)
118  r = modulefinder.ModuleFinder.import_module(self,partnam,fqname,parent)
119 
120  if r is not None:
121  self._depgraph.setdefault(self._last_caller.__name__,{})[r.__name__] = 1
122  return r
123  def load_module(self, fqname, fp, pathname, (suffix, mode, type)):
124  r = modulefinder.ModuleFinder.load_module(self, fqname, fp, pathname, (suffix, mode, type))
125  if r is not None:
126  self._types[r.__name__] = type
127  return r
128 
129  def scan_opcodes_25(self, co, unpack = struct.unpack):
130  """
131  This is basically just the default opcode scanner from ModuleFinder, but extended to also
132  look for "process.load(<module>)' commands. Since the Process object might not necassarily
133  be called "process", it scans for a call to a "load" method with a single parameter on
134  *any* object. If one is found it checks if the parameter is a string that refers to a valid
135  python module in the local or global area. If it does, the scanner assumes this was a call
136  to a Process object and yields the module name.
137  It's not possible to scan first for Process object declarations to get the name of the
138  objects since often (e.g. for customisation functions) the object is passed to a function
139  in a different file.
140 
141  The ModuleFinder.scan_opcodes_25 implementation this is based was taken from
142  https://hg.python.org/cpython/file/2.7/Lib/modulefinder.py#l364
143  """
144  # Scan the code, and yield 'interesting' opcode combinations
145  # Python 2.5 version (has absolute and relative imports)
146  code = co.co_code
147  names = co.co_names
148  consts = co.co_consts
149  LOAD_CONST = modulefinder.LOAD_CONST
150  IMPORT_NAME = modulefinder.IMPORT_NAME
151  STORE_OPS = modulefinder.STORE_OPS
152  HAVE_ARGUMENT = modulefinder.HAVE_ARGUMENT
153  LOAD_ATTR = chr(dis.opname.index('LOAD_ATTR'))
154  LOAD_NAME = chr(dis.opname.index('LOAD_NAME'))
155  CALL_FUNCTION = chr(dis.opname.index('CALL_FUNCTION'))
156  LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME
157 
158  try :
159  indexOfLoadConst = names.index("load") # This might throw a ValueError
160  # These are the opcodes required to access the "load" attribute. This might
161  # not even be a function, but I check for that later.
162  loadMethodOpcodes = LOAD_ATTR+struct.pack('<H',indexOfLoadConst)
163  except ValueError :
164  # doesn't look like "load" is used anywhere in this file
165  loadMethodOpcodes=None
166 
167  while code:
168  c = code[0]
169 
170  # Check to see if this is a call to a "load" method
171  if loadMethodOpcodes!=None and len(code)>=9 : # Need at least 9 codes for the full call
172  if code[:3]==loadMethodOpcodes :
173  # The attribute "load" is being accessed, need to make sure this is a function call.
174  # I'll look ahead and see if the CALL_FUNCTION code is used - this could be in a different
175  # place depending on the number of arguments, but I'm only interested in methods with a
176  # single argument so I know exactly where CALL_FUNCTION should be.
177  if code[6]==CALL_FUNCTION :
178  # I know this is calling a method called "load" with one argument. I need
179  # to find out what the argument is. Note that I still don't know if this is
180  # on a cms.Process object.
181  indexInTable=unpack('<H',code[4:6])[0]
182  if code[3]==LOAD_CONST :
183  # The argument is a constant, so retrieve that from the table
184  loadMethodArgument=consts[indexInTable]
185  # I know a load method with one argument has been called on *something*, but I don't
186  # know if it was a cms.Process object. All I can do is check to see if the argument is
187  # a string, and if so if it refers to a python file in the user or global areas.
188  try :
189  loadMethodArgument = loadMethodArgument.replace("/",".")
190  # I can only use imp.find_module on submodules (i.e. each bit between a "."), so try
191  # that on each submodule in turn using the previously found filename. Note that I have
192  # to try this twice, because if the first pass traverses into a package in the local
193  # area but the subpackage has not been checked out it will report that the subpackage
194  # doesn't exist, even though it is available in the global area.
195  try :
196  parentFilename=[self._localarea+"/python"]
197  for subModule in loadMethodArgument.split(".") :
198  moduleInfo=imp.find_module( subModule, parentFilename )
199  parentFilename=[moduleInfo[1]]
200  # If control got this far without raising an exception, then it must be a valid python module
201  yield "import", (None, loadMethodArgument)
202  except ImportError :
203  # Didn't work in the local area, try in the global area.
204  parentFilename=[self._globalarea+"/python"]
205  for subModule in loadMethodArgument.split(".") :
206  moduleInfo=imp.find_module( subModule, parentFilename )
207  parentFilename=[moduleInfo[1]]
208  # If control got this far without raising an exception, then it must be a valid python module
209  yield "import", (None, loadMethodArgument)
210  except Exception as error:
211  # Either there was an import error (not a python module) or there was a string
212  # manipulaton error (argument not a string). Assume this wasn't a call on a
213  # cms.Process object and move on silently.
214  pass
215 
216  elif code[3]==LOAD_NAME :
217  # The argument is a variable. I can get the name of the variable quite easily but
218  # not the value, unless I execute all of the opcodes. Not sure what to do here,
219  # guess I'll just print a warning so that the user knows?
220  print "Unable to determine the value of variable '"+names[indexInTable]+"' to see if it is a proces.load(...) statement in file "+co.co_filename
221 
222  code=code[9:]
223  continue
224 
225  if c in STORE_OPS:
226  oparg, = unpack('<H', code[1:3])
227  yield "store", (names[oparg],)
228  code = code[3:]
229  continue
230  if code[:9:3] == LOAD_LOAD_AND_IMPORT:
231  oparg_1, oparg_2, oparg_3 = unpack('<xHxHxH', code[:9])
232  level = consts[oparg_1]
233  if level == -1: # normal import
234  yield "import", (consts[oparg_2], names[oparg_3])
235  elif level == 0: # absolute import
236  yield "absolute_import", (consts[oparg_2], names[oparg_3])
237  else: # relative import
238  yield "relative_import", (level, consts[oparg_2], names[oparg_3])
239  code = code[9:]
240  continue
241  if c >= HAVE_ARGUMENT:
242  code = code[3:]
243  else:
244  code = code[1:]
245 
246 def removeRecursiveLoops( node, verbose=False, currentStack=None ) :
247  if currentStack is None : currentStack=[]
248  try :
249  duplicateIndex=currentStack.index( node ) # If there isn't a recursive loop this will raise a ValueError
250  if verbose :
251  print "Removing recursive loop in:"
252  for index in xrange(duplicateIndex,len(currentStack)) :
253  print " ",currentStack[index].name,"-->"
254  print " ",node.name
255  currentStack[-1].dependencies.remove(node)
256  except ValueError:
257  # No recursive loop found, so continue traversing the tree
258  currentStack.append( node )
259  for subnode in node.dependencies :
260  removeRecursiveLoops( subnode, verbose, currentStack[:] )
261 
262 def transformIntoGraph(depgraph,toplevel):
263  packageDict = {}
264  # create the top level config
265  packageDict[toplevel] = Package(toplevel, top = True)
266 
267  # create package objects
268  for key, value in depgraph.iteritems():
269  if key.count(".") == 2 and key != toplevel:
270  packageDict[key] = Package(key)
271  for name in value.keys():
272  if name.count(".") == 2: packageDict[name] = Package(name)
273  # now create dependencies
274  for key, value in depgraph.iteritems():
275  if key.count(".") == 2 or key == toplevel:
276  package = packageDict[key]
277  package.dependencies = [packageDict[name] for name in value.keys() if name.count(".") == 2]
278 
279  removeRecursiveLoops( packageDict[toplevel] )
280  # find and return the top level config
281  return packageDict[toplevel]
282 
283 
284 def getDependenciesFromPythonFile(filename,toplevelname,path):
285  modulefinder = mymf(path)
286  modulefinder.run_script(filename)
287  globalDependencyDict = modulefinder._depgraph
288  globalDependencyDict[toplevelname] = globalDependencyDict["__main__"]
289  return globalDependencyDict
290 
291 
292 def getImportTree(filename,path):
293  toplevelname = packageNameFromFilename(filename)
294  # get dependencies from given file
295  globalDependencyDict = getDependenciesFromPythonFile(filename,toplevelname,path)
296 
297  # transform this flat structure in a dependency tree
298  dependencyGraph = transformIntoGraph(globalDependencyDict,toplevelname)
299  return dependencyGraph
def transformIntoGraph
Definition: TreeCrawler.py:262
def getDependenciesFromPythonFile
Definition: TreeCrawler.py:284
def removeRecursiveLoops
Definition: TreeCrawler.py:246
def packageNameFromFilename
Definition: TreeCrawler.py:29
static std::string join(char **cmd)
Definition: RemoteFile.cc:18
list object
Definition: dbtoconf.py:77
def getImportTree
Definition: TreeCrawler.py:292
double split
Definition: MVATrainer.cc:139
How EventSelector::AcceptEvent() decides whether to accept an event for output otherwise it is excluding the probing of A single or multiple positive and the trigger will pass if any such matching triggers are PASS or EXCEPTION[A criterion thatmatches no triggers at all is detected and causes a throw.] A single negative with an expectation of appropriate bit checking in the decision and the trigger will pass if any such matching triggers are FAIL or EXCEPTION A wildcarded negative criterion that matches more than one trigger in the trigger list("!*","!HLTx*"if it matches 2 triggers or more) will accept the event if all the matching triggers are FAIL.It will reject the event if any of the triggers are PASS or EXCEPTION(this matches the behavior of"!*"before the partial wildcard feature was incorporated).Triggers which are in the READY state are completely ignored.(READY should never be returned since the trigger paths have been run