github.com/hwaf/hwaf@v0.0.0-20140814122253-5465f73b20f1/py-hwaftools/orch/deconf.py (about) 1 #!/usr/bin/env python 2 '''A descending configuration parser. 3 4 This parser provides for composing a hierarchical data structure from 5 a Python (ConfigParser) configuration file. It also allows for a more 6 flexible string interpolation than the base parser. 7 8 The file is parsed (see parse() function) by the usual ConfigParser 9 module and the resulting data is interpreted (see interpret() 10 function) into a dictionary. Two special sections are checked. 11 12 - [keytype] :: a mapping of a key name to a "section type" 13 14 - [<name>] :: the starting point of interpretation, default <name> is "start" 15 16 The [keytype] section maps a key name to a section type. When a key 17 is found in a section which matches a keytype key the key is 18 interpreted as a comma-separated list of section names and its value 19 in the keytype dictionary as a section type. A section expresses its 20 type and name by starting with a line that matches "[<type> <name>]". 21 When a matching key is encountered the keytype mapping and the list of 22 names are used to set a list on that key in the returned data 23 structure which contains the result of interpreting the referred to 24 sections. 25 26 This interpretation continues until all potential sections have been 27 loaded. Not all sections in the file may be represented in the 28 resulting data structure if they have not been referenced through the 29 keytype mechanism described above. 30 31 After interpretation the data structure is inflated (see inflate() 32 function). The result is a new dictionary in which any daughter 33 dictionaries now contain all scalar, string (non-list, non-dictionary) 34 entries from all its mothers. 35 36 Finally, each dictionary in the hierarchy has all of its scalar, 37 string entries interpolated (see format_any() function). This portion 38 of the dictionary is itself used as a source of "{variable}" 39 interpolation. Infinite loops must be avoided. 40 41 By default a scalar, string value is formatted via its string format() 42 function. A "formatter(string, **kwds)" function can be provided to 43 provide customized formatting. 44 45 This formatting continues until all possible interpolations have been 46 resolved. Any unresolved ones will be retained. This interpolation 47 is done on intermediate dictionaries but it is only the ones at leafs 48 of the hierarchy which are likely useful to the application. 49 50 Either a single filename or a list of filenames can be given for 51 parsing. The [start] section may include a special key "includes" to 52 specify a list of other configuration files to also parse. The files 53 are searched first in the current working directory, next in the 54 directories holding the files already specified and next from any 55 colon-separated list of directories specified by a DECONF_INCLUDE_PATH 56 environment variable. 57 58 The section "defaults" may provide additional configuration items to 59 place in the "start" section. This allows a single file to specify 60 defaults used by multiple main configuration files 61 ''' 62 63 import os 64 65 def merge_defaults(cfg, start = 'start', sections = 'defaults'): 66 if isinstance(sections, type('')): 67 sections = [x.strip() for x in sections.split(',')] 68 #print 'CFG sections:', cfg.sections() 69 for sec in sections: 70 if not cfg.has_section(sec): 71 continue 72 for k,v in cfg.items(sec): 73 if cfg.has_option(start, k): 74 continue 75 #print 'DECONF: merging',k,v 76 cfg.set(start,k,v) 77 78 def parse(filename): 79 'Parse the filename, return an uninterpreted object' 80 try: from ConfigParser import SafeConfigParser 81 except ImportError: from configparser import SafeConfigParser 82 cfg = SafeConfigParser() 83 cfg.optionxform = str # want case sensitive 84 cfg.read(filename) 85 if isinstance(filename, type("")): 86 filename = [filename] 87 cfg.files = filename 88 89 return cfg 90 91 def to_list(lst): 92 return [x.strip() for x in lst.split(',')] 93 94 def get_first_typed_section(cfg, typ, name): 95 target = '%s %s' % (typ, name) 96 for sec in cfg.sections(): 97 if sec == target: 98 return sec 99 raise ValueError('No section: <%s> %s' % (typ,name)) 100 101 102 def find_file(fname, others = None): 103 '''Find file <fname> in current working directory, in the "others" 104 directories and finally in a environment path variable 105 DECONF_INCLUDE_PATH.''' 106 107 dirs = ['.'] 108 if others: 109 dirs += others 110 dirs += os.environ.get('DECONF_INCLUDE_PATH','').split(':') 111 112 for check in dirs: 113 maybe = os.path.join(check, fname) 114 if os.path.exists(maybe): 115 return maybe 116 return 117 118 119 def add_includes(cfg, sec): 120 ''' 121 Resolves any "includes" item in section <sec> by reading the 122 specified files into the cfg object. 123 ''' 124 if not cfg.has_option(sec,'includes'): 125 return 126 127 inc_val = cfg.get(sec,'includes') 128 inc_list = to_list(inc_val) 129 if not inc_list: 130 raise ValueError('Not includes: "%s"' % inc_val) 131 return 132 133 to_check = ['.'] 134 if hasattr(cfg, 'files') and cfg.files: 135 already_read = cfg.files 136 if isinstance(already_read, type('')): 137 already_read = [already_read] 138 other_dirs = map(os.path.realpath, map(os.path.dirname, already_read)) 139 to_check += other_dirs 140 to_check += os.environ.get('DECONF_INCLUDE_PATH','').split(':') 141 142 for fname in inc_list: 143 fpath = find_file(fname, to_check) 144 if not fpath: 145 raise ValueError( 146 'Failed to locate file: %s (%s)' % 147 (fname, ':'.join(to_check)) 148 ) 149 cfg.read(fpath) 150 if hasattr(cfg, 'files'): 151 cfg.files.append(fpath) 152 return 153 154 def resolve(cfg, sec, **kwds): 155 'Recursively load the configuration starting at the given section' 156 157 add_includes(cfg, sec) 158 159 secitems = dict(cfg.items(sec)) 160 161 # get special keytype section governing the hierarchy schema 162 keytype = dict(cfg.items('keytype')) 163 164 ret = {} 165 for k,v in secitems.items(): 166 typ = keytype.get(k) 167 if not typ: 168 ret[k] = v 169 continue 170 lst = [] 171 for name in to_list(v): 172 other_sec = get_first_typed_section(cfg, typ, name) 173 other = resolve(cfg, other_sec, **kwds) 174 other.setdefault(typ,name) 175 lst.append(other) 176 ret[k] = lst 177 return ret 178 179 def interpret(cfg, start = 'start', **kwds): 180 ''' 181 Interpret a parsed file by following any keytypes, return raw data 182 structure. 183 184 The <start> keyword can select a different section to start the 185 interpretation. Any additional keywords are override or otherwise 186 added to the initial section. 187 ''' 188 return resolve(cfg, start, **kwds) 189 190 def format_flat_dict(dat, formatter = str.format, **kwds): 191 kwds = dict(kwds) 192 unformatted = dict(dat) 193 formatted = dict() 194 195 while unformatted: 196 changed = False 197 for k,v in list(unformatted.items()): 198 try: 199 new_v = formatter(v, **kwds) 200 except KeyError: 201 continue # maybe next time 202 except TypeError: # can't be formatted 203 new_v = v # pretend we just did 204 205 changed = True 206 formatted[k] = new_v 207 kwds[k] = new_v 208 unformatted.pop(k) 209 continue 210 if not changed: 211 break 212 continue 213 if unformatted: 214 formatted.update(unformatted) 215 return formatted 216 217 def format_any(dat, formatter = str.format, **kwds): 218 if dat is None: 219 return dat 220 if isinstance(dat, type(b"")): 221 dat = "%s" % dat # py3: bytes -> string 222 if isinstance(dat, type("")): 223 try: 224 return formatter(dat, **kwds) 225 except KeyError: 226 return dat 227 if isinstance(dat, list): 228 return [format_any(x, formatter=formatter, **kwds) for x in dat] 229 flat = dict() 230 other = dict() 231 for k,v in dat.items(): 232 if isinstance(v, type("")): 233 flat[k] = v 234 else: 235 other[k] = v 236 ret = format_flat_dict(flat, formatter=formatter, **kwds) 237 for k,v in other.items(): 238 v = format_any(v, formatter=formatter, **kwds) 239 ret[k] = v 240 return ret 241 242 243 def inflate(src, defaults = None): 244 ''' 245 Copy scalar dictionary entries down the hierarchy to other 246 dictionaries. 247 ''' 248 if not defaults: defaults = dict() 249 ret = dict() 250 ret.update(defaults) 251 ret.update(src) 252 253 flat = dict() 254 other = dict() 255 for k,v in ret.items(): 256 if isinstance(v, type("")): 257 flat[k] = v 258 else: 259 other[k] = v 260 for key,lst in other.items(): 261 ret[key] = [inflate(x, flat) for x in lst] 262 263 return ret 264 265 def load(filename, start = 'start', formatter = str.format, **kwds): 266 ''' 267 Return the fully parsed, interpreted, inflated and formatted suite. 268 ''' 269 if not filename: 270 raise ValueError('deconf.load not given any files to load') 271 272 cfg = parse(filename) 273 add_includes(cfg,start) 274 merge_defaults(cfg, start) 275 data = interpret(cfg, start, **kwds) 276 data2 = inflate(data) 277 if not formatter: 278 return data2 279 data3 = format_any(data2, formatter=formatter, **kwds) 280 return data3 281 282 283 284 ### testing 285 286 def extra_formatter(string, **kwds): 287 tags = kwds.get('tags') 288 if tags: 289 tags = [x.strip() for x in tags.split(',')] 290 kwds.setdefault('tagsdashed', '-'.join(tags)) 291 kwds.setdefault('tagsunderscore', '_'.join(tags)) 292 293 version = kwds.get('version') 294 if version: 295 kwds.setdefault('version_2digit', '.'.join(version.split('.')[:2])) 296 kwds.setdefault('version_underscore', version.replace('.','_')) 297 kwds.setdefault('version_nodots', version.replace('.','')) 298 299 return string.format(**kwds) 300 301 302 def example_formatter(string, **kwds): 303 ''' 304 Example to add extra formatting 305 ''' 306 kwds.setdefault('prefix','/tmp/simple') 307 kwds.setdefault('PREFIX','/tmp/simple') 308 ret = extra_formatter(string, **kwds) 309 310 return ret 311 312 313 def test(): 314 from pprint import PrettyPrinter 315 pp = PrettyPrinter(indent=2) 316 317 cfg = parse('deconf.cfg') 318 data = interpret(cfg) 319 data2 = inflate(data) 320 data3 = format_any(data2) 321 322 data4 = load('deconf.cfg') 323 assert data3 == data4 324 325 print ('INTERPRETED:') 326 pp.pprint(data) 327 print ('INFLATED:') 328 pp.pprint(data2) 329 print ('FORMATTED:') 330 pp.pprint(data3) 331 332 def dump(filename, start='start', formatter=str.format): 333 from pprint import PrettyPrinter 334 pp = PrettyPrinter(indent=2) 335 data = load(filename, start=start, formatter=example_formatter) 336 print ('Starting from "%s"' % start) 337 pp.pprint(data) 338 339 if '__main__' == __name__: 340 import sys 341 dump(sys.argv[1:])