File: configfile.py

package info (click to toggle)
python-pyqtgraph 0.14.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 8,168 kB
  • sloc: python: 54,831; makefile: 128; ansic: 40; sh: 2
file content (199 lines) | stat: -rw-r--r-- 5,978 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""
configfile.py - Human-readable text configuration file library 
Copyright 2010  Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more information.

Used for reading and writing dictionary objects to a python-like configuration
file format. Data structures may be nested and contain any data type as long
as it can be converted to/from a string using repr and eval.
"""


import contextlib
import datetime
import os
import re
import sys
from collections import OrderedDict

import numpy

from . import units
from .colormap import ColorMap
from .Point import Point
from .Qt import QtCore

GLOBAL_PATH = None  # so not thread safe.


class ParseError(Exception):
    def __init__(self, message, lineNum, line, fileName=None):
        self.lineNum = lineNum
        self.line = line
        self.message = message
        self.fileName = fileName
        Exception.__init__(self, message)

    def __str__(self):
        if self.fileName is None:
            msg = f"Error parsing string at line {self.lineNum:d}:\n"
        else:
            msg = f"Error parsing config file '{self.fileName}' at line {self.lineNum:d}:\n"
        msg += f"{self.line}\n{Exception.__str__(self)}"
        return msg


def writeConfigFile(data, fname):
    s = genString(data)
    with open(fname, 'wt') as fd:
        fd.write(s)


def readConfigFile(fname, **scope):
    global GLOBAL_PATH
    if GLOBAL_PATH is not None:
        fname2 = os.path.join(GLOBAL_PATH, fname)
        if os.path.exists(fname2):
            fname = fname2

    GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))

    local = {
        **scope,
        **units.allUnits,
        'OrderedDict': OrderedDict,
        'readConfigFile': readConfigFile,
        'Point': Point,
        'QtCore': QtCore,
        'ColorMap': ColorMap,
        'datetime': datetime,
        # Needed for reconstructing numpy arrays
        'array': numpy.array,
    }
    for dtype in ['int8', 'uint8',
                  'int16', 'uint16', 'float16',
                  'int32', 'uint32', 'float32',
                  'int64', 'uint64', 'float64']:
        local[dtype] = getattr(numpy, dtype)

    try:
        with open(fname, "rt") as fd:
            s = fd.read()
        s = s.replace("\r\n", "\n")
        s = s.replace("\r", "\n")
        data = parseString(s, **local)[1]
    except ParseError:
        sys.exc_info()[1].fileName = fname
        raise
    except:
        print(f"Error while reading config file {fname}:")
        raise
    return data


def appendConfigFile(data, fname):
    s = genString(data)
    with open(fname, 'at') as fd:
        fd.write(s)


def genString(data, indent=''):
    s = ''
    for k in data:
        sk = str(k)
        if not sk:
            print(data)
            raise ValueError('blank dict keys not allowed (see data above)')
        if sk[0] == ' ' or ':' in sk:
            print(data)
            raise ValueError(
                f'dict keys must not contain ":" or start with spaces [offending key is "{sk}"]'
            )
        if isinstance(data[k], dict):
            s += f"{indent}{sk}:\n"
            s += genString(data[k], f'{indent}    ')
        else:
            line = repr(data[k]).replace("\n", "\\\n")
            s += f"{indent}{sk}: {line}\n"
    return s


def parseString(lines, start=0, **scope):
    data = OrderedDict()
    if isinstance(lines, str):
        lines = lines.replace("\\\n", "")
        lines = lines.split('\n')

    indent = None
    ln = start - 1
    l = ''

    try:
        while True:
            ln += 1
            if ln >= len(lines):
                break

            l = lines[ln]

            ## Skip blank lines or lines starting with #
            if not _line_is_real(l):
                continue

            ## Measure line indentation, make sure it is correct for this level
            lineInd = measureIndent(l)
            if indent is None:
                indent = lineInd
            if lineInd < indent:
                ln -= 1
                break
            if lineInd > indent:
                raise ParseError(f'Indentation is incorrect. Expected {indent:d}, got {lineInd:d}', ln + 1, l)

            if ':' not in l:
                raise ParseError('Missing colon', ln + 1, l)

            k, _, v = l.partition(':')
            k = k.strip()
            v = v.strip()

            ## set up local variables to use for eval
            if len(k) < 1:
                raise ParseError('Missing name preceding colon', ln + 1, l)
            if k[0] == '(' and k[-1] == ')':  # If the key looks like a tuple, try evaluating it.
                with contextlib.suppress(Exception):  # If tuple conversion fails, keep the string
                    k1 = eval(k, scope)
                    if type(k1) is tuple:
                        k = k1
            if _line_is_real(v):  # eval the value
                try:
                    val = eval(v, scope)
                except Exception as ex:
                    raise ParseError(
                        f"Error evaluating expression '{v}': [{ex.__class__.__name__}: {ex}]", ln + 1, l
                    ) from ex
            else:
                next_real_ln = next((i for i in range(ln + 1, len(lines)) if _line_is_real(lines[i])), len(lines))
                if ln + 1 >= len(lines) or measureIndent(lines[next_real_ln]) <= indent:
                    val = {}
                else:
                    ln, val = parseString(lines, start=ln + 1, **scope)
            if k in data:
                raise ParseError(f'Duplicate key: {k}', ln + 1, l)
            data[k] = val
    except ParseError:
        raise
    except Exception as ex:
        raise ParseError(f"{ex.__class__.__name__}: {ex}", ln + 1, l) from ex
    return ln, data


def _line_is_real(line):
    return not re.match(r'\s*#', line) and re.search(r'\S', line)


def measureIndent(s):
    n = 0
    while n < len(s) and s[n] == ' ':
        n += 1
    return n