source: nagslang/yamlish.py@ 344:1d73867becbe

Last change on this file since 344:1d73867becbe was 344:1d73867becbe, checked in by Stefano Rivera <stefano@…>, 8 years ago

Allow tuples in dicts

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:
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.