Package jazzparser :: Package utils :: Module config
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.utils.config

  1  """Config file parsing 
  2   
  3  All of the scripts in this project use the optparse package to handle  
  4  command line options. 
  5   
  6  This module provides a very simple mechanism for taking config files  
  7  as input instead of specifying options on the command line. This is  
  8  convenient, for example, for running experiments repeatedly where a  
  9  whole load of options need to be given to set parameters. 
 10   
 11  The options in the file are stored in the following format:: 
 12   optname=value 
 13   
 14   - The files may contain comments beginning with a '#'. 
 15   - To specify arguments, just put the argument on a line of its own. 
 16   - To specify flags, put the flag name on a line of its own preceded by a +. 
 17   
 18  You can only use long option names currently. This is best practice  
 19  anyway, as it makes the file more readable. 
 20   
 21  The config options are simply transformed into a string of  
 22  command-line-like options and added to the actual command-line options. 
 23   
 24  Don't forget to put a comment in the file so you know what script it's  
 25  for! 
 26   
 27  Additionally, lines beginning with '%%' are treated as directives. 
 28   - C{%% INCLUDE filename}: includes another config file. 
 29   - C{%% ARG i value}: treats C{value} as the ith argument. If you  
 30      specify any arguments in this way, you should specify them all like 
 31      this. Allows the arguments not to be given in order. 
 32   - C{%% DEF name value}: defines or defines the value of the variable  
 33      C{name}. This value may subsequently be used with a %{name}  
 34      substitution. 
 35   - C{%% ABSTRACT}: declares the whole file to be abstract, i.e. it cannot  
 36      be used directly, but only as an include in another file. You should  
 37      put this in any file that relies on including files to supply required  
 38      options/arguments. 
 39   - C{%% REQUIRE option}: requires the user to specify the named option on  
 40      the command line when using this config file. 
 41   
 42  You may use certain substitutions in the options. %{X} will be replaced  
 43  by a value if one can be found. The following sources are consulted (in  
 44  this order): 
 45   - a variable X defined with a DEF directive; 
 46   - a constant X from the settings file. 
 47  One purpose of this is to allow you to specify paths relative to the  
 48  project root, etc, rather than where the script is run. 
 49   
 50  A linebreak preceded by a \ will be ignored. Whitespace at the start  
 51  of the subsequent line will be ignored (but not whitespace before the  
 52  \). 
 53   
 54  """ 
 55  """ 
 56  ============================== License ======================================== 
 57   Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding 
 58    
 59   This file is part of The Jazz Parser. 
 60    
 61   The Jazz Parser is free software: you can redistribute it and/or modify 
 62   it under the terms of the GNU General Public License as published by 
 63   the Free Software Foundation, either version 3 of the License, or 
 64   (at your option) any later version. 
 65    
 66   The Jazz Parser is distributed in the hope that it will be useful, 
 67   but WITHOUT ANY WARRANTY; without even the implied warranty of 
 68   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 69   GNU General Public License for more details. 
 70    
 71   You should have received a copy of the GNU General Public License 
 72   along with The Jazz Parser.  If not, see <http://www.gnu.org/licenses/>. 
 73   
 74  ============================ End license ====================================== 
 75   
 76  """ 
 77  __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"  
 78   
 79  import sys 
80 81 -class ConfigFile(object):
82 """ 83 A really simple interface to options stored in config files. 84 Can also process a string as if read from the contents of a file. 85 86 """
87 - def __init__(self, filename, string=False):
88 self.options = [] 89 self.flags = [] 90 self.arguments = [] 91 self.required_options = [] 92 93 if string: 94 # Take the config lines as a string directly 95 self.filename = None 96 self.lines = filename.split("\n") 97 else: 98 # Read config lines from a file 99 self.filename = filename 100 with open(self.filename, 'r') as cfile: 101 self.lines = cfile.readlines() 102 self.parse_lines()
103 104 @staticmethod
105 - def from_string(string):
106 return ConfigFile(string, string=True)
107
108 - def parse_lines(self):
109 """ 110 Parses the lines stored in self.lines. Called by initialization. 111 You can call this straight off if you've instantiated with a string. 112 113 """ 114 from jazzparser import settings 115 import os 116 117 conf_lines = self.lines 118 119 numbered_args = {} 120 defined_variables = {} 121 122 def _do_substitutions(value, full_line): 123 # Look for any placeholders in the value and perform the 124 # appropriate substitution 125 while "%{" in value: 126 opener = value.index("%{") 127 closer = value.find("}", opener) 128 if closer == -1: 129 # No matching close brace 130 raise ConfigFileReadError, "no matching close brace (}) found in %s" % full_line 131 const_name = value[opener+2:closer] 132 133 if const_name in defined_variables: 134 # First check whether a value is defined for this name 135 sub_value = defined_variables[const_name] 136 elif hasattr(settings, const_name): 137 # Try getting a constant from the settings file 138 sub_value = getattr(settings, const_name) 139 else: 140 raise ConfigFileReadError, "no setting or variable "\ 141 "'%s' found to make substitution in: %s" % \ 142 (const_name, full_line.strip()) 143 # Replace every occurrence of this placeholder with the value 144 value = value.replace("%{"+const_name+"}", sub_value) 145 return value
146 147 # Preprocess the lines 148 def _preprocess_lines(lines, included=False): 149 _proc_lines = [] 150 151 # Remove line breaks from the end 152 lines = [l.rstrip("\n") for l in lines] 153 # Concatenate lines where a \ precedes the line break 154 joined_lines = [] 155 to_join = [] 156 for line in lines: 157 if line.endswith("\\"): 158 to_join.append(line[:-1].lstrip()) 159 else: 160 if len(to_join) > 0: 161 to_join.append(line.lstrip()) 162 joined_lines.append("".join(to_join)) 163 to_join = [] 164 else: 165 joined_lines.append(line) 166 167 for line in joined_lines: 168 use_line = True 169 # Ignore anything after a # 170 if "#" in line: 171 line = line[:line.index("#")] 172 line = line.strip() 173 174 # Check for directives 175 if line.startswith("%%"): 176 # Eliminate this line from the preprocessed set 177 use_line = False 178 179 line = _do_substitutions(line[2:], line) 180 # Pull out the first word and make it case insensitive 181 directive = line.split()[0].strip().lower() 182 args = line.split()[1:] 183 # Check what the directive is a process the lines accordingly 184 if directive == "include": 185 if len(args) != 1: 186 raise ConfigFileReadError, "INCLUDE directive "\ 187 "requires a filename argument" 188 # Include another config file 189 if self.filename is None: 190 # Can't make paths relative to filename 191 filename = args[0] 192 else: 193 filename = os.path.join( 194 os.path.dirname(self.filename), 195 args[0]) 196 filename = os.path.abspath(filename) 197 try: 198 file = open(filename, 'r') 199 except IOError: 200 raise ConfigFileReadError, "could not open "\ 201 "included config file %s" % filename 202 203 try: 204 # Replace this line with the lines of the other file 205 file_lines = _preprocess_lines(file.readlines(), included=True) 206 _proc_lines.extend(file_lines) 207 finally: 208 file.close() 209 elif directive == "arg": 210 if len(args) != 2: 211 raise ConfigFileReadError, "ARG directive "\ 212 "requires an argument number and a value" 213 arg_num = int(args[0]) 214 numbered_args[arg_num] = args[1] 215 elif directive == "def": 216 if len(args) != 2: 217 raise ConfigFileReadError, "DEF directive "\ 218 "requires a variable name and a value" 219 defined_variables[args[0]] = args[1] 220 elif directive == "abstract": 221 if not included: 222 # The file isn't being read because it's included 223 # in another, but it's marked as abstract, so 224 # shouldn't be used directly 225 raise ConfigFileReadError, "encountered ABSTRACT "\ 226 "directive in a non-included file. You should "\ 227 "not use this file directly, but as an include "\ 228 "in another config file." 229 elif directive == "require": 230 if len(args) != 1: 231 raise ConfigFileReadError, "REQUIRE directive "\ 232 "needs an option name" 233 self.required_options.append(args[0]) 234 # Define any more directives here 235 else: 236 raise ConfigFileReadError, "unknown directive: %s" % directive 237 238 if use_line and len(line) > 0: 239 _proc_lines.append(line) 240 return _proc_lines
241 242 proc_lines = _preprocess_lines(conf_lines) 243 if len(numbered_args) > 0: 244 # Check that the numbered args make sense 245 num_args = max(numbered_args.keys()) 246 for i in range(num_args+1): 247 if i not in numbered_args: 248 raise ConfigFileReadError, "missing argument: "\ 249 "arg number %s was given but %s was not" % \ 250 (num_args, i) 251 # Transform these into ordinary ordered args 252 self.arguments = [numbered_args[i] for i in range(num_args+1)] 253 254 for line in proc_lines: 255 if line.startswith("+"): 256 # Take the + off and store this as a flag 257 self.flags.append(line[1:]) 258 elif "=" in line: 259 # Split the optname=val into optname and val 260 opt, __, val = line.partition("=") 261 self.options.append((opt.strip(), _do_substitutions(val.strip(), line))) 262 else: 263 # Just a plain argument 264 if len(numbered_args) > 0: 265 raise ConfigFileReadError, "cannot mix numbered args "\ 266 "and non-numbered args: %s" % line 267 self.arguments.append(_do_substitutions(line.strip(), line)) 268
269 - def get_strings(self):
270 """ 271 Get a list of strings containing all the config options in a form ready 272 to be passed to optparse as if they were command-line options. 273 274 """ 275 return sum([["--%s" % opt, "%s" % val] for (opt,val) in self.options], []) \ 276 + ["--%s" % flag for flag in self.flags] \ 277 + self.arguments
278
279 -def parse_args_with_config(parser, option_name="config"):
280 """ 281 An alternative to calling parser.parse_args() which adds a --config 282 option to the parser's options and uses it to read in a config 283 file if it's given. 284 285 The args will potentially get parsed twice: once to get the config 286 file and then again to incorporate options from the file. 287 288 @return: (options, arguments) tuple, as given by parser.parse_args(). 289 290 """ 291 import sys 292 # Add the config file option 293 parser.add_option("--%s" % option_name, dest="%s" % option_name, action="store", help="read options in from a config file.") 294 # Do the initial (standard) parse 295 options,arguments = parser.parse_args() 296 297 conf_file = getattr(options, option_name) 298 if conf_file is not None: 299 # Read in options from the config file 300 conf = ConfigFile(conf_file) 301 # Check that any options the file requires the user to give are there 302 for required in conf.required_options: 303 if not hasattr(options, required): 304 raise ConfigFileReadError, "config file requires non-existent "\ 305 "command line option '%s'" % required 306 elif getattr(options, required) is None: 307 print "Error: config file requires that you "\ 308 "give the option '%s' on the command line" % required 309 sys.exit(1) 310 # Produce arguments that incorporate the file and cmd-line opts 311 conf_strings = conf.get_strings() 312 if len(conf_strings) > 0: 313 # Parse the opts and args from the config file first 314 options, file_arguments = parser.parse_args(args=conf_strings) 315 # Reparse the command line options so that values given there 316 # override those in the file 317 options, cl_arguments = parser.parse_args(values=options) 318 # Include arguments from both sources, file first 319 arguments = file_arguments + cl_arguments 320 return options,arguments
321
322 -class ConfigFileReadError(Exception):
323 pass
324