ensembl-hive-python3  2.4
Params.py
Go to the documentation of this file.
1 
2 import sys
3 import numbers
4 import collections
5 
6 __doc__ = """
7 This module is an implementation of eHive's Param module.
8 It defines ParamContainer which is an attribute of BaseRunnable
9 and not its base class as in eHive's class hierarchy.
10 All the specific warnings and exceptions inherit from ParamWarning
11 and ParamException.
12 """
13 
14 
15 class ParamWarning(Warning):
16  """Used by Process.BaseRunnable"""
17  pass
18 
19 
20 class ParamException(Exception):
21  """Base class for parameters-related exceptions"""
22  pass
24  """Raised when the parameter name is not a string"""
25  def __str__(self):
26  return '"{0}" (type {1}) is not a valid parameter name'.format(self.args[0], type(self.args[0]).__name__)
28  """Raised when ParamContainer tried to substitute an unexpected structure (only dictionaries and lists are accepted)"""
29  def __str__(self):
30  return 'Cannot substitute elements in objects of type "{0}"'.format(str(type(self.args[0])))
32  """Raised when parameters depend on each other, forming a loop"""
33  def __str__(self):
34  return "Substitution loop has been detected on {0}. Parameter-substitution stack: {1}".format(self.args[0], list(self.args[1].keys()))
36  """Raised when a parameter cannot be required because it is null (None)"""
37  def __str__(self):
38  return "{0} is None".format(self.args[0])
39 
40 
41 class ParamContainer(object):
42  """Equivalent of eHive's Param module"""
43 
44  def __init__(self, unsubstituted_params, debug=False):
45  """Constructor. "unsubstituted_params" is a dictionary"""
46  self.unsubstituted_param_hash = unsubstituted_params.copy()
47  self.param_hash = {}
48  self.debug = debug
49 
50 
51  # Public methods
52 
53 
54  def set_param(self, param_name, value):
55  """Setter. Returns the new value"""
56  if not self.validate_parameter_name(param_name):
57  raise ParamNameException(param_name)
58  self.param_hash[param_name] = value
59  return value
60 
61  def get_param(self, param_name):
62  """Getter. Performs the parameter substitution"""
63  if not self.validate_parameter_name(param_name):
64  raise ParamNameException(param_name)
65  self.substitution_in_progress = collections.OrderedDict()
66  try:
67  return self.internal_get_param(param_name)
68  except (KeyError, SyntaxError, ParamException) as e:
69  # To hide the part of the stack that is in ParamContainer
70  raise type(e)(*e.args) from None
71 
72  def has_param(self, param_name):
73  """Returns a boolean. It checks both substituted and unsubstituted parameters"""
74  if not self.validate_parameter_name(param_name):
75  raise ParamNameException(param_name)
76  return (param_name in self.param_hash) or (param_name in self.unsubstituted_param_hash)
77 
78 
79  # Private methods
80 
81  def validate_parameter_name(self, param_name):
82  """Tells whether "param_name" is a non-empty string"""
83  return isinstance(param_name, str) and (param_name != '')
84 
85  def debug_print(self, *args, **kwargs):
86  """Print debug information if the debug flag is turned on (cf constructor)"""
87  if self.debug:
88  print(*args, **kwargs)
89 
90  def internal_get_param(self, param_name):
91  """Equivalent of get_param() that assumes "param_name" is a valid parameter name and hence, doesn't have to raise ParamNameException.
92  It is only used internally"""
93  self.debug_print("internal_get_param", param_name)
94  if param_name not in self.param_hash:
95  x = self.unsubstituted_param_hash[param_name]
96  self.param_hash[param_name] = self.param_substitute(x)
97  return self.param_hash[param_name]
98 
99 
100  def param_substitute(self, structure):
101  """
102  Take any structure and replace the pairs of hashes with the values of the parameters / expression they represent
103  Compatible types: numbers, strings, lists, dictionaries (otherwise, ParamSubstitutionException is raised)
104  """
105  self.debug_print("param_substitute", structure)
107  if structure is None:
108  return None
109 
110  elif isinstance(structure, list):
111  return [self.param_substitute(_) for _ in structure]
112 
113  elif isinstance(structure, dict):
114  # NB: In Python, not everything can be hashed and used as a dictionary key.
115  # Perhaps we should check for such errors ?
116  return {self.param_substitute(key): self.param_substitute(value) for (key,value) in structure.items()}
117 
118  elif isinstance(structure, numbers.Number):
119  return structure
120 
121  elif isinstance(structure, str):
122 
123  # We handle the substitution differently if there is a single reference as we can avoid forcing the result to be a string
124 
125  if structure[:6] == '#expr(' and structure[-6:] == ')expr#' and structure.count('#expr(', 6, -6) == 0 and structure.count(')expr#', 6, -6) == 0:
126  return self.subst_one_hashpair(structure[1:-1], True)
127 
128  if structure[0] == '#' and structure[-1] == '#' and structure.count('#', 1, -1) == 0:
129  if len(structure) <= 2:
130  return structure
131  return self.subst_one_hashpair(structure[1:-1], False)
132 
133  # Fallback to the default parser: all pairs of hashes are substituted
134  return self.subst_all_hashpairs(structure, lambda middle_param: self.subst_one_hashpair(middle_param, False) )
135 
136  else:
137  raise ParamSubstitutionException(structure)
138 
139 
140  def subst_all_hashpairs(self, structure, callback):
141  """
142  Parse "structure" and replace all the pairs of hashes by the result of calling callback() on the pair content
143  #expr()expr# are treated differently by calling subst_one_hashpair()
144  The result is a string (like structure)
145  """
146  self.debug_print("subst_all_hashpairs", structure)
147  result = []
148  while True:
149  (head,_,tmp) = structure.partition('#')
150  result.append(head)
151  if _ != '#':
152  return ''.join(result)
153  if tmp.startswith('expr('):
154  i = tmp.find(')expr#')
155  if i == -1:
156  raise SyntaxError("Unmatched '#expr(' token")
157  val = self.subst_one_hashpair(tmp[:i+5], True)
158  tail = tmp[i+6:]
159  else:
160  (middle_param,_,tail) = tmp.partition('#')
161  if _ != '#':
162  raise SyntaxError("Unmatched '#' token")
163  if middle_param == '':
164  val = '##'
165  else:
166  val = callback(middle_param)
167  result.append(str(val))
168  structure = tail
169 
170 
171  def subst_one_hashpair(self, inside_hashes, is_expr):
172  """
173  Run the parameter substitution for a single pair of hashes.
174  Here, we only need to handle #expr()expr#, #func:params# and #param_name#
175  as each condition has been parsed in the other methods
176  """
177  self.debug_print("subst_one_hashpair", inside_hashes, is_expr)
178 
179  # Keep track of the substitutions we've made to detect loops
180  if inside_hashes in self.substitution_in_progress:
181  raise ParamInfiniteLoopException(inside_hashes, self.substitution_in_progress)
182  self.substitution_in_progress[inside_hashes] = 1
183 
184  # We ask the caller to provide the is_expr tag to avoid checking the string again for the presence of the "expr" tokens
185  if is_expr:
186  s = self.subst_all_hashpairs(inside_hashes[5:-5].strip(), lambda middle_param: 'self.internal_get_param("{0}")'.format(middle_param))
187  val = eval(s)
188 
189  elif ':' in inside_hashes:
190  (func_name,_,parameters) = inside_hashes.partition(':')
191  try:
192  f = eval(func_name)
193  except:
194  raise SyntaxError("Unknown method: " + func_name)
195  if callable(f):
196  if parameters:
197  val = f(self.internal_get_param(parameters))
198  else:
199  val = f()
200  else:
201  raise SyntaxError(func_name + " is not callable")
202 
203  else:
204  val = self.internal_get_param(inside_hashes)
205 
206  del self.substitution_in_progress[inside_hashes]
207  return val
208 
209 
210 
211 def __main():
212  seed_params = [
213  ('alpha' , 2),
214  ('beta' , 5),
215  ('delta' , '#expr( #alpha#*#beta# )expr#'),
216 
217  ('gamma' , [10,20,33,15]),
218  ('gamma_prime' , '#expr( #gamma# )expr#'),
219  ('gamma_second' , '#expr( list(#gamma#) )expr#'),
220 
221  ('age' , { 'Alice' : 17, 'Bob' : 20, 'Chloe' : 21}),
222  ('age_prime' , '#expr( #age# )expr#'),
223  ('age_second' , '#expr( dict(#age#) )expr#'),
224 
225  ('csv' , '[123,456,789]'),
226  ('csv_prime' , '#expr( #csv# )expr#'),
227  ('listref' , '#expr( eval(#csv#) )expr#'),
229  ('null' , None),
230  ('ref_null' , '#null#'),
231  ('ref2_null' , '#expr( #null# )expr#'),
232  ('ref3_null' , '#alpha##null##beta#'),
233  ]
234 
235  p = ParamContainer(collections.OrderedDict(seed_params), False)
236 
237  def print_title(title):
238  print();
239  print("*" + title + "*")
240 
241  def print_substitution(title, param_string):
242  print(title)
243  print("\t>", param_string)
244  x = p.param_substitute(param_string)
245  print_param_value(x)
246 
247  def print_param_value(x):
248  print("\t=", x, type(x), "id=0x{0:012x}".format(id(x)))
249 
250  print_title("Exceptions")
251  try:
252  p.get_param('ppppppp')
253  except KeyError as e:
254  print("KeyError raised")
255  else:
256  print("KeyError NOT raised")
257  print()
258 
259  try:
260  p.get_param(0) # should raise ParamNameException
261  except ParamNameException as e:
262  print("ParamNameException raised")
263  else:
264  print("ParamNameException NOT raised")
265  print()
266 
267  try:
268  ParamContainer({'a': '#b#', 'b': '#a#'}, True).get_param('a')
269  except ParamInfiniteLoopException as e:
270  print("ParamInfiniteLoopException raised")
271  else:
272  print("ParamInfiniteLoopException NOT raised")
273  print()
274 
275  print_title('All the parameters')
276  for (key,value) in seed_params:
277  print("\t>", key, "is seeded as:", value, type(value))
278  x = p.get_param(key)
279  print_param_value(x)
280  print()
281 
282  print_title("Numbers")
283  print_substitution( "Scalar substitutions", "#alpha# and another: #beta# and again one: #alpha# and the other: #beta# . Their product: #delta#" )
284 
285  print_title("Lists")
286  print_substitution( "default stringification of gamma", "#gamma#" )
287  print_substitution( "expr-stringification of gamma", "#expr( #gamma# )expr#" )
288  print_substitution( "complex join of gamma", "#expr( '~'.join([str(_) for _ in sorted(#gamma#)]) )expr#" )
289  print_substitution( "complex join of gamma_prime", "#expr( '~'.join([str(_) for _ in sorted(#gamma_prime#)]) )expr#" )
290 
291  print_title("Global methods")
292  print_substitution( "sum(gamma)", "#expr( sum(#gamma#) )expr#" )
293  print_substitution( "min(gamma)", "#expr( min(#gamma#) )expr#" )
294  print_substitution( "max(gamma)", "#expr( max(#gamma#) )expr#" )
295 
296  print_title("Dictionaries")
297  print_substitution( "default stringification of age", "#age#" )
298  print_substitution( "expr-stringification of age", "#expr( #age# )expr#" )
299  print_substitution( "complex fold of age", '#expr( "\t".join(["{0} is {1} years old".format(p,a) for (p,a) in #age#.items()]) )expr#' )
300  print_substitution( "complex fold of age_prime", '#expr( "\t".join(["{0} is {1} years old".format(p,a) for (p,a) in #age_prime#.items()]) )expr#' )
301 
302  print_title("With indexes")
303  print_substitution( "adding indexed values", '#expr( #age#["Alice"]+max(#gamma#)+#listref#[0] )expr#' )
304 
305  print_title("Modifications of gamma")
306  p.get_param('gamma').append("val0")
307  print("\tgamma", p.get_param('gamma'))
308  print("\tgamma_prime", p.get_param('gamma_prime'))
309  print("\tgamma_second", p.get_param('gamma_second'))
310 
311 
312 if __name__ == '__main__':
313  __main()
314 
Base class for parameters-related exceptions.
Definition: Params.py:23
Raised when ParamContainer tried to substitute an unexpected structure (only dictionaries and lists a...
Definition: Params.py:32
def validate_parameter_name(self, param_name)
Tells whether "param_name" is a non-empty string.
Definition: Params.py:94
def subst_all_hashpairs(self, structure, callback)
Parse "structure" and replace all the pairs of hashes by the result of calling callback() on the pair...
Definition: Params.py:161
def get_param(self, param_name)
Getter.
Definition: Params.py:72
def debug_print(self, args, kwargs)
Print debug information if the debug flag is turned on (cf constructor)
Definition: Params.py:99
def internal_get_param(self, param_name)
Equivalent of get_param() that assumes "param_name" is a valid parameter name and hence...
Definition: Params.py:106
Equivalent of eHive&#39;s Param module.
Definition: Params.py:49
def has_param(self, param_name)
Returns a boolean.
Definition: Params.py:84
def subst_one_hashpair(self, inside_hashes, is_expr)
Run the parameter substitution for a single pair of hashes.
Definition: Params.py:193
Raised when a parameter cannot be required because it is null (None)
Definition: Params.py:42
def set_param(self, param_name, value)
Setter.
Definition: Params.py:64
Used by Process.BaseRunnable.
Definition: Params.py:17
Raised when the parameter name is not a string.
Definition: Params.py:27
def param_substitute(self, structure)
Take any structure and replace the pairs of hashes with the values of the parameters / expression the...
Definition: Params.py:119
def __main()
Definition: Params.py:228
Raised when parameters depend on each other, forming a loop.
Definition: Params.py:37