source: nagslang/yamlish.py @ 380:a106d7e1415b

Last change on this file since 380:a106d7e1415b was 380:a106d7e1415b, checked in by Stefano Rivera <stefano@…>, 7 years ago

Colons are allowed in strings

File size: 8.1 KB
Line 
1'''
2Serializer and dumper for a simple, YAMLish format (actually a YAML subset).
3The top level object is a dict or list.
4lists and dicts can contain:
5 * lists, dicts,
6 * single line strings,
7 * ints, floats,
8 * True, False, and None
9dict keys can only be scalar.
10'''
11
12import re
13
14
15def dump(data, file_object):
16    file_object.write(dump_s(data))
17
18
19def dump_s(data):
20    return Dumper().dump(data)
21
22
23def load(file_object):
24    yaml = file_object.read()
25    return load_s(yaml)
26
27
28def load_s(yaml):
29    return Parser().parse(yaml.strip())
30
31
32class Dumper(object):
33    def dump(self, data):
34        return '\n'.join(self._dump_block(data)) + '\n'
35
36    def _dump_block(self, data, indent=0):
37        for type_ in (list, tuple, dict):
38            if isinstance(data, type_):
39                f = getattr(self, '_dump_%s_block' % type_.__name__)
40                return f(data, indent)
41        raise NotImplementedError()
42
43    def _dump_inline(self, data):
44        if data is True or data is False or data is None:
45            return self._dump_literal(data)
46        for type_ in (list, tuple, dict, basestring, int, float):
47            if isinstance(data, type_):
48                f = getattr(self, '_dump_%s' % type_.__name__)
49                return f(data)
50        raise NotImplementedError()
51
52    def _dump_list_block(self, data, indent):
53        output = []
54        for item in data:
55            if self._inlineable(item):
56                output.append('%s- %s' % (' ' * indent,
57                                          self._dump_inline(item)))
58            else:
59                dumped = self._dump_block(item, indent + 2)
60                dumped[0] = '%s- %s' % (' ' * indent, dumped[0][indent + 2:])
61                output += dumped
62        return output
63
64    _dump_tuple_block = _dump_list_block
65
66    def _dump_dict_block(self, data, indent):
67        output = []
68        for k, v in sorted(data.iteritems()):
69            output.append('%s%s:' % (' ' * indent, self._dump_inline(k)))
70            if self._inlineable(v):
71                output[-1] += ' ' + self._dump_inline(v)
72            elif isinstance(v, dict):
73                output += self._dump_block(v, indent + 2)
74            elif isinstance(v, (list, tuple)):
75                output += self._dump_block(v, indent)
76            else:
77                raise NotImplementedError("Cannot dump %r", data)
78        return output
79
80    def _inlineable(self, data):
81        if isinstance(data, (list, tuple)):
82            return all(not isinstance(item, (list, dict, tuple))
83                       for item in data)
84        elif isinstance(data, dict):
85            return all(not isinstance(item, (list, dict, tuple))
86                       for item in data.itervalues())
87        else:
88            return True
89
90    def _dump_list(self, data):
91        return '[%s]' % ', '.join(self._dump_inline(item) for item in data)
92
93    _dump_tuple = _dump_list
94
95    def _dump_dict(self, data):
96        return '{%s}' % ', '.join(
97            '%s: %s' % (self._dump_inline(key), self._dump_inline(value))
98            for key, value in data.iteritems())
99
100    def _dump_basestring(self, data):
101        if data in ('true', 'false', 'null'):
102            return "'%s'" % data
103        if "'" in data or ':' in data or data.startswith('['):
104            return "'%s'" % data.replace("'", "''")
105        if data == '':
106            return "''"
107        return data
108
109    def _dump_int(self, data):
110        return str(data)
111
112    def _dump_float(self, data):
113        return str(data)
114
115    def _dump_literal(self, data):
116        return {
117            True: 'true',
118            False: 'false',
119            None: 'null',
120        }[data]
121
122
123class Parser(object):
124    _spaces_re = re.compile(r'^(\s*)(.*)')
125    _list_re = re.compile(r'^(-\s+)(.*)')
126    _dict_re = re.compile(r"^((?![{['])[^-:]+):\s?(.*)")
127    _inline_list_re = re.compile(r"^([^',]+|(?:'')+|'.+?[^'](?:'')*')"
128                                 r"(?:, (.*))?$")
129
130    def __init__(self):
131        # Stack of (indent level, container object)
132        self._stack = []
133        # When a dict's value is a nested block, remember the key
134        self._parent_key = None
135
136    @property
137    def _indent(self):
138        return self._stack[-1][0]
139
140    @property
141    def _container(self):
142        return self._stack[-1][1]
143
144    @property
145    def _in_list(self):
146        return isinstance(self._container, list)
147
148    @property
149    def _in_dict(self):
150        return isinstance(self._container, dict)
151
152    def _push(self, container, indent=None):
153        in_list = self._in_list
154        assert in_list or self._parent_key
155
156        if indent is None:
157            indent = self._indent
158        self._stack.append((indent, container()))
159        if in_list:
160            self._stack[-2][1].append(self._container)
161        else:
162            self._stack[-2][1][self._parent_key] = self._container
163            self._parent_key = None
164
165    def parse(self, yaml):
166        if yaml.startswith(('[', '{')):
167            return self._parse_value(yaml)
168
169        if yaml.startswith('-'):
170            self._stack.append((0, []))
171        else:
172            self._stack.append((0, {}))
173
174        for line in yaml.splitlines():
175            spaces, line = self._spaces_re.match(line).groups()
176
177            while len(spaces) < self._indent:
178                self._stack.pop()
179
180            lm = self._list_re.match(line)
181            dm = self._dict_re.match(line)
182            if len(spaces) == self._indent:
183                if lm and self._in_dict:
184                    # Starting a list in a dict
185                    self._push(list)
186                elif dm and self._in_list:
187                    # Left an embedded list
188                    self._stack.pop()
189
190            if len(spaces) > self._indent:
191                assert self._parent_key
192                if dm:
193                    # Nested dict
194                    self._push(dict, len(spaces))
195                elif lm:
196                    # Over-indented list in a dict
197                    self._push(list, len(spaces))
198
199            indent = self._indent
200            while lm and lm.group(2).startswith('- '):
201                # Nested lists
202                prefix, line = lm.groups()
203                indent += len(prefix)
204                self._push(list, indent)
205                lm = self._list_re.match(line)
206            del indent
207
208            if lm:
209                prefix, line = lm.groups()
210                dm = self._dict_re.match(line)
211                if dm:
212                    self._push(dict, self._indent + len(prefix))
213                else:
214                    assert self._in_list
215                    self._container.append(self._parse_value(line))
216
217            if dm:
218                key, value = dm.groups()
219                key = self._parse_value(key)
220                assert self._in_dict
221                if value:
222                    value = self._parse_value(value)
223                    self._container[key] = value
224                else:
225                    self._parent_key = key
226
227        return self._stack[0][1]
228
229    def _parse_value(self, value):
230        if value.startswith("'") and value.endswith("'"):
231            return value[1:-1].replace("''", "'")
232        if value.startswith('[') and value.endswith(']'):
233            value = value[1:-1]
234            output = []
235            while value:
236                m = self._inline_list_re.match(value)
237                assert m, value
238                output.append(self._parse_value(m.group(1)))
239                value = m.group(2)
240            return output
241        if value.startswith('{') and value.endswith('}'):
242            value = value[1:-1]
243            output = {}
244            while value:
245                key, value = value.split(': ', 1)
246                m = self._inline_list_re.match(value)
247                assert m
248                output[key] = self._parse_value(m.group(1))
249                value = m.group(2)
250            return output
251        if value.startswith('!!'):
252            raise NotImplementedError()
253        if value == 'true':
254            return True
255        if value == 'false':
256            return False
257        if value == 'null':
258            return None
259        for type_ in (int, float):
260            try:
261                return type_(value)
262            except ValueError:
263                pass
264        return value
Note: See TracBrowser for help on using the repository browser.