The Gaudi Framework  master (37c0b60a)
ProcessJobOptions.py
Go to the documentation of this file.
1 
11 import logging
12 import os
13 import re
14 import sys
15 import time
16 
17 _log = logging.getLogger(__name__)
18 
19 
20 class LogFormatter(logging.Formatter):
21  def __init__(self, fmt=None, datefmt=None, prefix="# ", with_time=False):
22  logging.Formatter.__init__(self, fmt, datefmt)
23  self.prefix = prefix
24  self.with_time = with_time
25 
26  def format(self, record):
27  fmsg = logging.Formatter.format(self, record)
28  prefix = self.prefix
29  if self.with_time:
30  prefix += "%f " % time.time()
31  if record.levelno >= logging.WARNING:
32  prefix += record.levelname + ": "
33  s = "\n".join([prefix + line for line in fmsg.splitlines()])
34  return s
35 
36 
37 class LogFilter(logging.Filter):
38  def __init__(self, name=""):
39  logging.Filter.__init__(self, name)
40  self.printing_level = 0
41  self.enabled = True
42  self.threshold = logging.WARNING
43 
44  def filter(self, record):
45  return record.levelno >= self.threshold or (
46  self.enabled and self.printing_level <= 0
47  )
48 
49  def printOn(self, step=1, force=False):
50  """
51  Decrease the printing_level of 'step' units. ( >0 means no print)
52  The level cannot go below 0, unless the force flag is set to True.
53  A negative value of the threshold disables subsequent "PrintOff"s.
54  """
55  if force:
56  self.printing_level -= step
57  else:
58  if self.printing_level > step:
59  self.printing_level -= step
60  else:
61  self.printing_level = 0
62 
63  def printOff(self, step=1):
64  """
65  Increase the printing_level of 'step' units. ( >0 means no print)
66  """
67  self.printing_level += step
68 
69  def disable(self, allowed=logging.WARNING):
70  self.enabled = False
71  self.threshold = allowed
72 
73  def enable(self, allowed=logging.WARNING):
74  self.enabled = True
75  self.threshold = allowed
76 
77 
78 class ConsoleHandler(logging.StreamHandler):
79  def __init__(self, stream=None, prefix=None, with_time=False):
80  if stream is None:
81  stream = sys.stdout
82  logging.StreamHandler.__init__(self, stream)
83  if prefix is None:
84  prefix = "# "
85  self._filter = LogFilter(_log.name)
86  self._formatter = LogFormatter(prefix=prefix, with_time=with_time)
87  self.setFormatter(self._formatter)
88  self.addFilter(self._filter)
89 
90  def setPrefix(self, prefix):
91  self._formatter.prefix = prefix
92 
93  def printOn(self, step=1, force=False):
94  """
95  Decrease the printing_level of 'step' units. ( >0 means no print)
96  The level cannot go below 0, unless the force flag is set to True.
97  A negative value of the threshold disables subsequent "PrintOff"s.
98  """
99  self._filter.printOn(step, force)
100 
101  def printOff(self, step=1):
102  """
103  Increase the printing_level of 'step' units. ( >0 means no print)
104  """
105  self._filter.printOff(step)
106 
107  def disable(self, allowed=logging.WARNING):
108  self._filter.disable(allowed)
109 
110  def enable(self, allowed=logging.WARNING):
111  self._filter.enable(allowed)
112 
113 
114 _consoleHandler = None
115 
116 
117 def GetConsoleHandler(prefix=None, stream=None, with_time=False):
118  global _consoleHandler
119  if _consoleHandler is None:
120  _consoleHandler = ConsoleHandler(
121  prefix=prefix, stream=stream, with_time=with_time
122  )
123  elif prefix is not None:
124  _consoleHandler.setPrefix(prefix)
125  return _consoleHandler
126 
127 
128 def InstallRootLoggingHandler(prefix=None, level=None, stream=None, with_time=False):
129  root_logger = logging.getLogger()
130  if not root_logger.handlers:
131  root_logger.addHandler(GetConsoleHandler(prefix, stream, with_time))
132  root_logger.setLevel(logging.WARNING)
133  if level is not None:
134  root_logger.setLevel(level)
135 
136 
137 def PrintOn(step=1, force=False):
138  GetConsoleHandler().printOn(step, force)
139 
140 
141 def PrintOff(step=1):
142  GetConsoleHandler().printOff(step)
143 
144 
145 class ParserError(RuntimeError):
146  pass
147 
148 
149 def _find_file(f):
150  # expand environment variables in the filename
151  f = os.path.expandvars(f)
152  if os.path.isfile(f):
153  return os.path.realpath(f)
154 
155  path = os.environ.get("JOBOPTSEARCHPATH", "").split(os.pathsep)
156  # find the full path to the option file
157  candidates = [d for d in path if os.path.isfile(os.path.join(d, f))]
158  if not candidates:
159  raise ParserError("Cannot find '%s' in %s" % (f, path))
160  return os.path.realpath(os.path.join(candidates[0], f))
161 
162 
163 _included_files = set()
164 
165 
167  if f in _included_files:
168  _log.warning("file '%s' already included, ignored.", f)
169  return False
170  _included_files.add(f)
171  return True
172 
173 
175  comment = re.compile(r"(//.*)$")
176  # non-perfect R-E to check if '//' is inside a string
177  # (a tokenizer would be better)
178  comment_in_string = re.compile(r'(["\']).*//.*\1')
179  directive = re.compile(r"^\s*#\s*([\w!]+)\s*(.*)\s*$")
180  comment_ml = (re.compile(r"/\*"), re.compile(r"\*/"))
181  statement_sep = ";"
182  reference = re.compile(r"^@([\w.]*)$")
183 
184  def __init__(self):
185  # parser level states
186  self.units = {}
187  self.defines = {}
188  if sys.platform != "win32":
189  self.defines["WIN32"] = True
190 
191  def _include(self, file, function):
192  file = _find_file(file)
193  if _to_be_included(file):
194  _log.info("--> Including file '%s'", file)
195  function(file)
196  _log.info("<-- End of file '%s'", file)
197 
198  def parse(self, file):
199  # states for the "translation unit"
200  statement = ""
201 
202  ifdef_level = 0
203  ifdef_skipping = False
204  ifdef_skipping_level = 0
205 
206  in_string = False
207 
208  f = open(_find_file(file))
209  l = f.readline()
210  if l.startswith("#!"):
211  # Skip the first line if it starts with "#!".
212  # It allows to use options files as scripts.
213  l = f.readline()
214 
215  while l:
216  l = (
217  l.rstrip() + "\n"
218  ) # normalize EOL chars (to avoid problems with DOS new-line on Unix)
219 
220  # single line comment
221  m = self.comment.search(l)
222  if m:
223  # check if the '//' is part of a string
224  m2 = self.comment_in_string.search(l)
225  # the '//' is part of a string if we find the quotes around it
226  # and they are not part of the comment itself
227  if not (m2 and m2.start() < m.start()):
228  # if it is not the case, we can remove the comment from the
229  # statement
230  l = l[: m.start()] + l[m.end() :]
231  # process directives
232  m = self.directive.search(l)
233  if m:
234  directive_name = m.group(1)
235  directive_arg = m.group(2).strip()
236  if directive_name == "include":
237  included_file = directive_arg.strip("'\"")
238  importOptions(included_file)
239  elif directive_name == "units":
240  units_file = directive_arg.strip("'\"")
241  self._include(units_file, self._parse_units)
242  elif directive_name in ["ifdef", "ifndef"]:
243  ifdef_skipping_level = ifdef_level
244  ifdef_level += 1
245  if directive_arg in self.defines:
246  ifdef_skipping = directive_name == "ifndef"
247  else:
248  ifdef_skipping = directive_name == "ifdef"
249  elif directive_name == "else":
250  ifdef_skipping = not ifdef_skipping
251  elif directive_name == "endif":
252  ifdef_level -= 1
253  if ifdef_skipping and ifdef_skipping_level == ifdef_level:
254  ifdef_skipping = False
255  elif directive_name == "pragma":
256  if not directive_arg:
257  l = f.readline()
258  continue
259  pragma = directive_arg.split()
260  if pragma[0] == "print":
261  if len(pragma) > 1:
262  if pragma[1].upper() in ["ON", "TRUE", "1"]:
263  PrintOn()
264  else:
265  PrintOff()
266  else:
267  _log.warning("unknown directive '%s'", directive_name)
268  l = f.readline()
269  continue
270 
271  if ifdef_skipping:
272  l = f.readline()
273  continue
274 
275  # multi-line comment
276  m = self.comment_ml[0].search(l)
277  if m:
278  l, l1 = l[: m.start()], l[m.end() :]
279  m = self.comment_ml[1].search(l1)
280  while not m:
281  l1 = f.readline()
282  if not l1:
283  break # EOF
284  m = self.comment_ml[1].search(l1)
285  if not l1 and not m:
286  raise ParserError(
287  "End Of File reached before end of multi-line comment"
288  )
289  l += l1[m.end() :]
290 
291  # if we are in a multiline string, we add to the statement
292  # everything until the next '"'
293  if in_string:
294  string_end = l.find('"')
295  if string_end >= 0:
296  statement += l[: string_end + 1]
297  l = l[string_end + 1 :]
298  in_string = False # the string ends here
299  else:
300  statement += l
301  l = ""
302  else: # check if we have a string
303  string_start = l.find('"')
304  if string_start >= 0:
305  string_end = l.find('"', string_start + 1)
306  if string_end >= 0:
307  # the string is opened and closed
308  statement += l[: string_end + 1]
309  l = l[string_end + 1 :]
310  else:
311  # the string is only opened
312  statement += l
313  in_string = True
314  l = f.readline()
315  continue
316 
317  if self.statement_sep in l:
318  i = l.index(self.statement_sep)
319  statement += l[:i]
320  self._eval_statement(statement.strip().replace("\n", "\\n"))
321  statement = l[i + 1 :]
322  # it may happen (bug #37479) that the rest of the statement
323  # contains a comment.
324  if statement.lstrip().startswith("//"):
325  statement = ""
326  else:
327  statement += l
328 
329  l = f.readline()
330 
331  def _parse_units(self, file):
332  for line in open(file):
333  if "//" in line:
334  line = line[: line.index("//")]
335  line = line.strip()
336  if not line:
337  continue
338  nunit, value = line.split("=")
339  factor, unit = nunit.split()
340  value = eval(value) / eval(factor)
341  self.units[unit] = value
342 
343  def _eval_statement(self, statement):
344  from GaudiKernel.Proxy.Configurable import (
345  Configurable,
346  ConfigurableGeneric,
347  PropertyReference,
348  )
349 
350  # statement = statement.replace("\n","").strip()
351  _log.info("%s%s", statement, self.statement_sep)
352 
353  property, value = statement.split("=", 1)
354 
355  inc = None
356  if property[-1] in ["+", "-"]:
357  inc = property[-1]
358  property = property[:-1]
359 
360  property = property.strip()
361  value = value.strip()
362 
363  # find the configurable to apply the property to
364  # parent_cfg = None
365  # while '.' in property:
366  # component, property = property.split('.',1)
367  # if parent_cfg:
368  # if hasattr(parent_cfg,component):
369  # cfg = getattr(parent_cfg,component)
370  # else:
371  # cfg = ConfigurableGeneric(component)
372  # setattr(parent_cfg,component,cfg)
373  # else:
374  # cfg = ConfigurableGeneric(component)
375  # parent_cfg = cfg
376 
377  # remove spaces around dots
378  property = ".".join([w.strip() for w in property.split(".")])
379  component, property = property.rsplit(".", 1)
380  if component in Configurable.allConfigurables:
381  cfg = Configurable.allConfigurables[component]
382  else:
383  cfg = ConfigurableGeneric(component)
384 
385  # value = os.path.expandvars(value)
386  value = value.replace("true", "True").replace("false", "False")
387  if value[0] == "{":
388  # Try to guess if the values looks like a dictionary
389  if ":" in value and not (
390  value[: value.index(":")].count('"') % 2
391  or value[: value.index(":")].count("'") % 2
392  ):
393  # for dictionaries, keep the surrounding {}
394  value = "{" + value[1:-1].replace("{", "[").replace("}", "]") + "}"
395  else: # otherwise replace all {} with []
396  value = value.replace("{", "[").replace("}", "]")
397 
398  # We must escape '\' because eval tends to interpret them
399  value = value.replace("\\", "\\\\")
400  # Restore special cases ('\n', '\t' and '\"') (see GAUDI-1001)
401  value = (
402  value.replace(r"\\n", r"\n").replace(r"\\t", r"\t").replace(r'\\"', r"\"")
403  )
404  # replace r'\n' and r'\t' that are outside double quoted strings
405  value = '"'.join(
406  [
407  (v if i % 2 else re.sub(r"\\[nt]", " ", v))
408  for i, v in enumerate(value.split('"'))
409  ]
410  )
411 
412  # interprete the @ operator
413  m = self.reference.match(value)
414  if m:
415  # this allows late binding of references
416  value = PropertyReference(m.group(1))
417  else:
418  value = eval(value, self.units)
419 
420  # if type(value) is str : value = os.path.expandvars(value)
421  # elif type(value) is list : value = [ type(item) is str and os.path.expandvars(item) or item for item in value ]
422 
423  if property not in cfg.__slots__ and not hasattr(cfg, property):
424  # check if the case of the property is wrong (old options are case insensitive)
425  lprop = property.lower()
426  for p in cfg.__slots__:
427  if lprop == p.lower():
428  _log.warning(
429  "property '%s' was requested for %s, but the correct spelling is '%s'",
430  property,
431  cfg.name(),
432  p,
433  )
434  property = p
435  break
436 
437  # consider the += and -=
438  if inc == "+":
439  if hasattr(cfg, property):
440  prop = getattr(cfg, property)
441  if isinstance(prop, dict):
442  for k in value:
443  prop[k] = value[k]
444  else:
445  prop += value
446  else:
447  setattr(cfg, property, value)
448  elif inc == "-":
449  if hasattr(cfg, property):
450  prop = getattr(cfg, property)
451  if isinstance(prop, dict):
452  for k in value:
453  if k in prop:
454  del prop[k]
455  else:
456  _log.warning(
457  "key '%s' not in %s.%s", k, cfg.name(), property
458  )
459  else:
460  for k in value:
461  if k in prop:
462  prop.remove(k)
463  else:
464  _log.warning(
465  "value '%s' not in %s.%s", k, cfg.name(), property
466  )
467  else:
468  setattr(cfg, property, value)
469 
470 
472  def __init__(self, new_path):
473  self.old_path = sys.path
474  sys.path = new_path
475 
476  def __del__(self):
477  sys.path = self.old_path
478 
479 
480 _parser = JobOptsParser()
481 
482 
483 def _import_python(file):
484  with open(file) as f:
485  code = compile(f.read(), file, "exec")
486  exec(code, {"__file__": file})
487 
488 
489 def _import_pickle(file):
490  import pickle
491 
492  input = open(file, "rb")
493  catalog = pickle.load(input)
494  _log.info("Unpickled %d configurables", len(catalog))
495 
496 
497 def _import_opts(file):
498  _parser.parse(file)
499 
500 
501 def _import_dict(data):
502  from GaudiKernel.Proxy.Configurable import Configurable, ConfigurableGeneric
503 
504  for property, value_repr in data.items():
505  component, property = property.rsplit(".", 1)
506  if component in Configurable.allConfigurables:
507  cfg = Configurable.allConfigurables[component]
508  else:
509  cfg = ConfigurableGeneric(component)
510  value = eval(value_repr)
511  setattr(cfg, property, value)
512 
513 
514 def _import_json(filename):
515  import json
516 
517  with open(filename) as f:
518  _import_dict(json.load(f))
519 
520 
521 _import_function_mapping = {
522  ".py": _import_python,
523  ".pkl": _import_pickle,
524  ".opts": _import_opts,
525  ".json": _import_json,
526 }
527 
528 try:
529  import yaml
530 
531  def _import_yaml(filename):
532  with open(filename) as f:
533  _import_dict(yaml.safe_load(f))
534 
535  _import_function_mapping[".yaml"] = _import_yaml
536  _import_function_mapping[".yml"] = _import_function_mapping[".yaml"]
537 except ImportError:
538  pass # yaml support is optional
539 
540 
541 def importOptions(optsfile):
542  # expand environment variables before checking the extension
543  optsfile = os.path.expandvars(optsfile)
544  # check the file type (extension)
545  dummy, ext = os.path.splitext(optsfile)
546  if ext in _import_function_mapping:
547  # check if the file has been already included
548  optsfile = _find_file(optsfile)
549  if _to_be_included(optsfile):
550  _log.info("--> Including file '%s'", optsfile)
551  # include the file
552  _import_function_mapping[ext](optsfile)
553  _log.info("<-- End of file '%s'", optsfile)
554  else:
555  raise ParserError("Unknown file type '%s' ('%s')" % (ext, optsfile))
556 
557 
558 # Import a file containing declaration of units.
559 # It is equivalent to:
560 #
561 # #units "unitsfile.opts"
562 #
563 
564 
565 def importUnits(unitsfile):
566  # expand environment variables
567  unitsfile = os.path.expandvars(unitsfile)
568  # we do not need to check the file type (extension) because it must be a
569  # units file
570  _parser._include(unitsfile, _parser._parse_units)
GaudiKernel.ProcessJobOptions.GetConsoleHandler
def GetConsoleHandler(prefix=None, stream=None, with_time=False)
Definition: ProcessJobOptions.py:117
GaudiKernel.ProcessJobOptions.JobOptsParser.parse
def parse(self, file)
Definition: ProcessJobOptions.py:198
GaudiKernel.ProcessJobOptions._import_dict
def _import_dict(data)
Definition: ProcessJobOptions.py:501
GaudiKernel.ProcessJobOptions._import_opts
def _import_opts(file)
Definition: ProcessJobOptions.py:497
GaudiKernel.ProcessJobOptions.JobOptsParser._parse_units
def _parse_units(self, file)
Definition: ProcessJobOptions.py:331
GaudiKernel.ProcessJobOptions.importUnits
def importUnits(unitsfile)
Definition: ProcessJobOptions.py:565
GaudiKernel.ProcessJobOptions.LogFormatter
Definition: ProcessJobOptions.py:20
GaudiKernel.ProcessJobOptions.ConsoleHandler.disable
def disable(self, allowed=logging.WARNING)
Definition: ProcessJobOptions.py:107
GaudiKernel.ProcessJobOptions.JobOptsParser.comment
comment
Definition: ProcessJobOptions.py:175
GaudiKernel.ProcessJobOptions._find_file
def _find_file(f)
Definition: ProcessJobOptions.py:149
GaudiKernel.ProcessJobOptions.LogFilter.disable
def disable(self, allowed=logging.WARNING)
Definition: ProcessJobOptions.py:69
GaudiKernel.ProcessJobOptions.LogFilter.printOff
def printOff(self, step=1)
Definition: ProcessJobOptions.py:63
GaudiKernel.ProcessJobOptions.ConsoleHandler._formatter
_formatter
Definition: ProcessJobOptions.py:86
GaudiKernel.ProcessJobOptions._TempSysPath.old_path
old_path
Definition: ProcessJobOptions.py:473
GaudiKernel.ProcessJobOptions.LogFilter.printing_level
printing_level
Definition: ProcessJobOptions.py:40
GaudiKernel.ProcessJobOptions._to_be_included
def _to_be_included(f)
Definition: ProcessJobOptions.py:166
GaudiKernel.ProcessJobOptions.JobOptsParser._include
def _include(self, file, function)
Definition: ProcessJobOptions.py:191
GaudiKernel.ProcessJobOptions.LogFilter.enabled
enabled
Definition: ProcessJobOptions.py:41
GaudiKernel.ProcessJobOptions.JobOptsParser.comment_ml
comment_ml
Definition: ProcessJobOptions.py:180
GaudiKernel.ProcessJobOptions._TempSysPath.__init__
def __init__(self, new_path)
Definition: ProcessJobOptions.py:472
GaudiKernel.ProcessJobOptions.LogFilter
Definition: ProcessJobOptions.py:37
GaudiKernel.ProcessJobOptions.JobOptsParser.__init__
def __init__(self)
Definition: ProcessJobOptions.py:184
GaudiKernel.ProcessJobOptions.ConsoleHandler._filter
_filter
Definition: ProcessJobOptions.py:85
GaudiKernel.ProcessJobOptions.ConsoleHandler.printOff
def printOff(self, step=1)
Definition: ProcessJobOptions.py:101
GaudiKernel.ProcessJobOptions._TempSysPath.__del__
def __del__(self)
Definition: ProcessJobOptions.py:476
GaudiKernel.ProcessJobOptions.LogFormatter.with_time
with_time
Definition: ProcessJobOptions.py:24
GaudiKernel.ProcessJobOptions._import_yaml
def _import_yaml(filename)
Definition: ProcessJobOptions.py:531
GaudiKernel.ProcessJobOptions.JobOptsParser.defines
defines
Definition: ProcessJobOptions.py:187
GaudiKernel.ProcessJobOptions.ConsoleHandler.setPrefix
def setPrefix(self, prefix)
Definition: ProcessJobOptions.py:90
GaudiKernel.ProcessJobOptions.JobOptsParser.units
units
Definition: ProcessJobOptions.py:186
GaudiKernel.ProcessJobOptions.LogFormatter.format
def format(self, record)
Definition: ProcessJobOptions.py:26
GaudiKernel.ProcessJobOptions.JobOptsParser.directive
directive
Definition: ProcessJobOptions.py:179
GaudiKernel.ProcessJobOptions.JobOptsParser
Definition: ProcessJobOptions.py:174
GaudiKernel.ProcessJobOptions._import_json
def _import_json(filename)
Definition: ProcessJobOptions.py:514
GaudiKernel.ProcessJobOptions.ConsoleHandler.printOn
def printOn(self, step=1, force=False)
Definition: ProcessJobOptions.py:93
GaudiKernel.ProcessJobOptions.InstallRootLoggingHandler
def InstallRootLoggingHandler(prefix=None, level=None, stream=None, with_time=False)
Definition: ProcessJobOptions.py:128
GaudiKernel.ProcessJobOptions.ConsoleHandler.enable
def enable(self, allowed=logging.WARNING)
Definition: ProcessJobOptions.py:110
GaudiKernel.ProcessJobOptions.LogFormatter.__init__
def __init__(self, fmt=None, datefmt=None, prefix="# ", with_time=False)
Definition: ProcessJobOptions.py:21
GaudiKernel.ProcessJobOptions.importOptions
def importOptions(optsfile)
Definition: ProcessJobOptions.py:541
GaudiKernel.ProcessJobOptions.LogFilter.enable
def enable(self, allowed=logging.WARNING)
Definition: ProcessJobOptions.py:73
GaudiKernel.ProcessJobOptions.JobOptsParser.statement_sep
statement_sep
Definition: ProcessJobOptions.py:181
GaudiKernel.ProcessJobOptions.ConsoleHandler
Definition: ProcessJobOptions.py:78
GaudiKernel.ProcessJobOptions._import_pickle
def _import_pickle(file)
Definition: ProcessJobOptions.py:489
GaudiKernel.ProcessJobOptions.LogFormatter.prefix
prefix
Definition: ProcessJobOptions.py:23
GaudiKernel.ProcessJobOptions.JobOptsParser._eval_statement
def _eval_statement(self, statement)
Definition: ProcessJobOptions.py:343
GaudiKernel.ProcessJobOptions._import_python
def _import_python(file)
Definition: ProcessJobOptions.py:483
GaudiKernel.ProcessJobOptions.LogFilter.__init__
def __init__(self, name="")
Definition: ProcessJobOptions.py:38
GaudiKernel.ProcessJobOptions._TempSysPath
Definition: ProcessJobOptions.py:471
GaudiKernel.ProcessJobOptions.JobOptsParser.comment_in_string
comment_in_string
Definition: ProcessJobOptions.py:178
GaudiKernel.ProcessJobOptions.PrintOff
def PrintOff(step=1)
Definition: ProcessJobOptions.py:141
GaudiKernel.ProcessJobOptions.JobOptsParser.reference
reference
Definition: ProcessJobOptions.py:182
GaudiKernel.ProcessJobOptions.LogFilter.printOn
def printOn(self, step=1, force=False)
Definition: ProcessJobOptions.py:49
GaudiKernel.ProcessJobOptions.ParserError
Definition: ProcessJobOptions.py:145
GaudiKernel.ProcessJobOptions.LogFilter.filter
def filter(self, record)
Definition: ProcessJobOptions.py:44
GaudiKernel.ProcessJobOptions.PrintOn
def PrintOn(step=1, force=False)
Definition: ProcessJobOptions.py:137
GaudiKernel.ProcessJobOptions.LogFilter.threshold
threshold
Definition: ProcessJobOptions.py:42
GaudiKernel.ProcessJobOptions.ConsoleHandler.__init__
def __init__(self, stream=None, prefix=None, with_time=False)
Definition: ProcessJobOptions.py:79