Package Products :: Package ZenUtils :: Module config
[hide private]
[frames] | no frames]

Source Code for Module Products.ZenUtils.config

  1  ########################################################################### 
  2  # 
  3  # This program is part of Zenoss Core, an open source monitoring platform. 
  4  # Copyright (C) 2010, Zenoss Inc. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify it 
  7  # under the terms of the GNU General Public License version 2 or (at your 
  8  # option) any later version as published by the Free Software Foundation. 
  9  # 
 10  # For complete information please visit: http://www.zenoss.com/oss/ 
 11  # 
 12  ########################################################################### 
 13   
 14  __doc__ = """ 
 15  Zenoss config parsers. 
 16   
 17  There are mutiple stages to config parsing. Parsing is split into stages so that 
 18  we can validate a whole config file and possibly rebuild it to correct errors. 
 19   
 20  The stages are: 
 21   
 22  * Parse - Split the config file in to ConfigLine types while maintaining line order, comments, and new lines 
 23  * Validate - Check that all lines are valid 
 24  * Report - Investigate why a line might be invalid (ex: invalid key format) 
 25  * Load - Get a config object back 
 26  * Write - An optional stage to write the config back to a file 
 27  """ 
 28   
 29  import re 
30 31 -class ConfigError(Exception):
32 """ 33 Error for problems parsing config files. 34 """ 35 pass
36
37 -class ConfigLineError(ConfigError):
38 """ 39 Error for problems parsing config files with line context. 40 """
41 - def __init__(self, message, lineno):
42 super(ConfigLineError, self).__init__(message) 43 self.lineno = lineno
44
45 - def __str__(self):
46 return '%s on line %d' % (self.message, self.lineno)
47
48 -class ConfigErrors(ConfigError):
49 """ 50 A group of errors while parsing config. 51 """
52 - def __init__(self, message, errors):
53 super(ConfigErrors, self).__init__(message) 54 self.errors = errors
55
56 - def __str__(self):
57 output = [self.message] 58 for error in self.errors: 59 output.append(str(error)) 60 61 return '\n - '.join(output)
62
63 -class InvalidKey(ConfigError):
64 pass
65
66 -class ConfigLineKeyError(ConfigLineError):
67 pass
68
69 -class Config(dict):
70 """ 71 A bunch of configuration settings. Uses dictionary access, 72 or object attribute access. 73 74 Provides some Convenience functions for different types. 75 """
76 - def __getattr__(self, attr):
77 return self[attr]
78
79 - def getbool(self, key, default=None):
80 """ 81 Convenience function to convert the value to a bool. 82 Valid values are and case of true, yes, y, 1 anything 83 else is considered False. 84 85 If key doesn't exist, returns `default`. 86 """ 87 try: 88 return self[key].lower() in ('true', 'yes', 'y', '1') 89 except KeyError: 90 return default
91 92 getboolean = getbool 93
94 - def getint(self, key, default=None):
95 """ 96 Convenience function to convert the value to an int. 97 Valid values are anything `int(x)` will accept. 98 99 If key doesn't exist or can't be converted to int, returns `default`. 100 """ 101 try: 102 return int(self[key]) 103 except (KeyError, ValueError): 104 return default
105
106 - def getfloat(self, key, default=None):
107 """ 108 Convenience function to convert the value to a float. 109 Valid values are anything `float(x)` will accept. 110 111 If key doesn't exist or can't be converted to float, returns `default`. 112 """ 113 try: 114 return float(self[key]) 115 except (KeyError, ValueError): 116 return default
117
118 -class ConfigLine(object):
119 """ 120 Abstract class that represents a single line in the config. 121 """
122 - def __init__(self, line):
123 self.line = line
124
125 - def __str__(self):
126 return self.line
127 128 @property
129 - def setting(self):
130 """ 131 Return a key, value tuple if this line represents a setting. 132 Implemented in base classes. 133 """ 134 return None
135 136 @classmethod
137 - def parse(cls, line):
138 """ 139 Returns an instance of cls if this class can parse this line. Otherwise returns None. 140 Implemented in base classes. 141 """ 142 return None
143 144 @classmethod
145 - def checkError(cls, line, lineno):
146 """ 147 Checks the string for possible matches, considers why it doesn't match exactly if it's close 148 and returns a ConfigLineError. 149 Implemented in base classes. 150 """ 151 return None
152
153 -class SettingLine(ConfigLine):
154 """ 155 Represents a config line with a `key = value` pair. 156 """ 157 _regexp = re.compile(r'^(?P<key>[a-z][a-z\d_]*)\s*(?P<delim>(=|:|\s)+)\s*(?P<value>.+)$', re.I) 158
159 - def __init__(self, key, value=None, delim='='):
160 self.key = key 161 self.value = value 162 self.delim = delim
163 164 @property
165 - def setting(self):
166 return self.key, self.value
167
168 - def __str__(self):
169 return '{key} {delim} {value}'.format(**self.__dict__)
170 171 @classmethod
172 - def checkError(cls, line, lineno):
173 match = re.match(r'^(?P<key>.+?)\s*(?P<delim>(=|:|\s)+)\s*(?P<value>.+)$', line, re.I) 174 if match and not cls._regexp.match(line): 175 return ConfigLineKeyError('Invalid key "%s"' % match.groupdict()['key'], lineno)
176 177 178 @classmethod
179 - def parse(cls, line):
180 match = cls._regexp.match(line) 181 if match: 182 data = match.groupdict() 183 return cls(**data)
184
185 -class CommentLine(ConfigLine):
186 @classmethod
187 - def parse(cls, line):
188 if line.startswith('#'): 189 return cls(line[1:].strip())
190
191 - def __str__(self):
192 return '# %s' % self.line
193
194 -class EmptyLine(ConfigLine):
195 - def __init__(self):
196 pass
197 198 @classmethod
199 - def parse(cls, line):
200 if line == '': 201 return cls()
202
203 - def __str__(self):
204 return ''
205
206 -class InvalidLine(ConfigLine):
207 """ 208 Default line if no other ConfigLines matched. Assumed to be invalid 209 input. 210 """ 211 pass
212
213 -class ConfigFile(object):
214 """ 215 Parses Zenoss's config file format. 216 217 Example: 218 219 key value 220 key intvalue 221 key = value 222 key=value 223 key:value 224 key : value 225 """ 226 227 # Lines to parse the config against 228 _lineTypes = [ 229 SettingLine, 230 CommentLine, 231 EmptyLine, 232 ] 233 234 # Line to use if the line didn't match any other types 235 _invalidLineType = InvalidLine 236
237 - def __init__(self, file):
238 """ 239 @param file file-like-object 240 """ 241 self.file = file 242 self.filename = self.file.name if hasattr(self.file, 'name') else 'Unknown' 243 self._lines = None
244
245 - def _parseLine(self, line):
246 cleanedLine = line.strip() 247 for type in self._lineTypes: 248 match = type.parse(cleanedLine) 249 if match: 250 return match 251 252 return self._invalidLineType(cleanedLine)
253 254
255 - def _checkLine(self, line, lineno):
256 cleanedLine = line.strip() 257 for type in self._lineTypes: 258 match = type.checkError(cleanedLine, lineno) 259 if match: 260 return match
261
262 - def parse(self):
263 """ 264 Parse a config file which has key-value pairs.Returns a list of config 265 line information. This line information can be used to accuratly recreate 266 the config without losing comments or invalid data. 267 """ 268 if self._lines is None: 269 self._lines = [] 270 for line in self.file: 271 self._lines.append(self._parseLine(line)) 272 273 return self._lines
274
275 - def write(self, file):
276 """ 277 Write the config out to a file. Takes a new file argument 278 because the input file object often doesn't have write access. 279 280 @param file file-like-object 281 """ 282 for line in self: 283 file.write(str(line) + '\n')
284
285 - def validate(self):
286 """ 287 Validate that there are no errors in the config file 288 289 @throws ConfigError 290 """ 291 errors = [] 292 for lineno, line in enumerate(self): 293 if isinstance(line, self._invalidLineType): 294 # Identify possible errors 295 error = self._checkLine(line.line, lineno) 296 if error: 297 errors.append(error) 298 else: 299 errors.append(ConfigLineError('Unexpected config line "%s"' % line.line, lineno + 1)) 300 301 if errors: 302 raise ConfigErrors('There were errors parsing the config "%s".' % self.filename, errors)
303
304 - def __iter__(self):
305 for line in self.parse(): 306 yield line
307
308 - def items(self):
309 for line in self: 310 if line.setting: 311 yield line.setting
312
313 -class Parser(object):
314 - def __call__(self, file):
315 configFile = ConfigFile(file) 316 configFile.validate() 317 return configFile.items()
318
319 320 -class ConfigLoader(object):
321 """ 322 Lazily load the config when requested. 323 """
324 - def __init__(self, config_files, config=Config, parser=Parser()):
325 """ 326 @param config Config The config instance or class to load data into. Must support update which accepts an iterable of (key, value). 327 @param parser Parser The parser to use to parse the config files. Must be a callable and return an iterable of (key, value). 328 @param config_files list<string> A list of config file names to parse in order. 329 """ 330 if not isinstance(config_files, list): 331 config_files = [config_files] 332 333 self.config_files = config_files 334 self.parser = parser 335 self.config = config 336 self._config = None
337
338 - def load(self):
339 """ 340 Load the config_files into an instance of config_class 341 """ 342 if isinstance(self.config, type): 343 self._config = self.config() 344 else: 345 self._config = self.config 346 347 if not self.config_files: 348 raise ConfigError('Config loader has no config files to load.') 349 350 for file in self.config_files: 351 if not hasattr(file, 'read') and isinstance(file, basestring): 352 # Look like a file name, open it 353 with open(file, 'r') as fp: 354 options = self.parser(fp) 355 else: 356 options = self.parser(file) 357 358 self._config.update(options)
359
360 - def __call__(self):
361 """ 362 Lazily load the config file. 363 """ 364 if self._config is None: 365 self.load() 366 367 return self._config
368