Aluode/PerceptionLabPortable
0
1"""2configfile.py - Human-readable text configuration file library 3Copyright 2010 Luke Campagnola4Distributed under MIT/X11 license. See license.txt for more information.5 6Used for reading and writing dictionary objects to a python-like configuration7file format. Data structures may be nested and contain any data type as long8as it can be converted to/from a string using repr and eval.9"""10 11 12import contextlib13import datetime14import os15import re16import sys17from collections import OrderedDict18 19import numpy20 21from . import units22from .colormap import ColorMap23from .Point import Point24from .Qt import QtCore25 26GLOBAL_PATH = None # so not thread safe.27 28 29class ParseError(Exception):30 def __init__(self, message, lineNum, line, fileName=None):31 self.lineNum = lineNum32 self.line = line33 self.message = message34 self.fileName = fileName35 Exception.__init__(self, message)36 37 def __str__(self):38 if self.fileName is None:39 msg = f"Error parsing string at line {self.lineNum:d}:\n"40 else:41 msg = f"Error parsing config file '{self.fileName}' at line {self.lineNum:d}:\n"42 msg += f"{self.line}\n{Exception.__str__(self)}"43 return msg44 45 46def writeConfigFile(data, fname):47 s = genString(data)48 with open(fname, 'wt') as fd:49 fd.write(s)50 51 52def readConfigFile(fname, **scope):53 global GLOBAL_PATH54 if GLOBAL_PATH is not None:55 fname2 = os.path.join(GLOBAL_PATH, fname)56 if os.path.exists(fname2):57 fname = fname258 59 GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))60 61 local = {62 **scope,63 **units.allUnits,64 'OrderedDict': OrderedDict,65 'readConfigFile': readConfigFile,66 'Point': Point,67 'QtCore': QtCore,68 'ColorMap': ColorMap,69 'datetime': datetime,70 # Needed for reconstructing numpy arrays71 'array': numpy.array,72 }73 for dtype in ['int8', 'uint8',74 'int16', 'uint16', 'float16',75 'int32', 'uint32', 'float32',76 'int64', 'uint64', 'float64']:77 local[dtype] = getattr(numpy, dtype)78 79 try:80 with open(fname, "rt") as fd:81 s = fd.read()82 s = s.replace("\r\n", "\n")83 s = s.replace("\r", "\n")84 data = parseString(s, **local)[1]85 except ParseError:86 sys.exc_info()[1].fileName = fname87 raise88 except:89 print(f"Error while reading config file {fname}:")90 raise91 return data92 93 94def appendConfigFile(data, fname):95 s = genString(data)96 with open(fname, 'at') as fd:97 fd.write(s)98 99 100def genString(data, indent=''):101 s = ''102 for k in data:103 sk = str(k)104 if not sk:105 print(data)106 raise ValueError('blank dict keys not allowed (see data above)')107 if sk[0] == ' ' or ':' in sk:108 print(data)109 raise ValueError(110 f'dict keys must not contain ":" or start with spaces [offending key is "{sk}"]'111 )112 if isinstance(data[k], dict):113 s += f"{indent}{sk}:\n"114 s += genString(data[k], f'{indent} ')115 else:116 line = repr(data[k]).replace("\n", "\\\n")117 s += f"{indent}{sk}: {line}\n"118 return s119 120 121def parseString(lines, start=0, **scope):122 data = OrderedDict()123 if isinstance(lines, str):124 lines = lines.replace("\\\n", "")125 lines = lines.split('\n')126 127 indent = None128 ln = start - 1129 l = ''130 131 try:132 while True:133 ln += 1134 if ln >= len(lines):135 break136 137 l = lines[ln]138 139 ## Skip blank lines or lines starting with #140 if not _line_is_real(l):141 continue142 143 ## Measure line indentation, make sure it is correct for this level144 lineInd = measureIndent(l)145 if indent is None:146 indent = lineInd147 if lineInd < indent:148 ln -= 1149 break150 if lineInd > indent:151 raise ParseError(f'Indentation is incorrect. Expected {indent:d}, got {lineInd:d}', ln + 1, l)152 153 if ':' not in l:154 raise ParseError('Missing colon', ln + 1, l)155 156 k, _, v = l.partition(':')157 k = k.strip()158 v = v.strip()159 160 ## set up local variables to use for eval161 if len(k) < 1:162 raise ParseError('Missing name preceding colon', ln + 1, l)163 if k[0] == '(' and k[-1] == ')': # If the key looks like a tuple, try evaluating it.164 with contextlib.suppress(Exception): # If tuple conversion fails, keep the string165 k1 = eval(k, scope)166 if type(k1) is tuple:167 k = k1168 if _line_is_real(v): # eval the value169 try:170 val = eval(v, scope)171 except Exception as ex:172 raise ParseError(173 f"Error evaluating expression '{v}': [{ex.__class__.__name__}: {ex}]", ln + 1, l174 ) from ex175 else:176 next_real_ln = next((i for i in range(ln + 1, len(lines)) if _line_is_real(lines[i])), len(lines))177 if ln + 1 >= len(lines) or measureIndent(lines[next_real_ln]) <= indent:178 val = {}179 else:180 ln, val = parseString(lines, start=ln + 1, **scope)181 if k in data:182 raise ParseError(f'Duplicate key: {k}', ln + 1, l)183 data[k] = val184 except ParseError:185 raise186 except Exception as ex:187 raise ParseError(f"{ex.__class__.__name__}: {ex}", ln + 1, l) from ex188 return ln, data189 190 191def _line_is_real(line):192 return not re.match(r'\s*#', line) and re.search(r'\S', line)193 194 195def measureIndent(s):196 n = 0197 while n < len(s) and s[n] == ' ':198 n += 1199 return n200 