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

Source Code for Module Products.ZenUtils.config

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