source: nagslang/yamlish.py@ 132:e1ef3727b7f8

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

Wrap long line

File size: 7.8 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 strings.
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))
35
36 def _dump_block(self, data, indent=0):
37 for type_ in (list, 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 in (True, False, None):
45 return self._dump_literal(data)
46 for type_ in (list, 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 def _dump_dict_block(self, data, indent):
65 output = []
66 for k, v in sorted(data.iteritems()):
67 output.append('%s%s:' % (' ' * indent, k))
68 if self._inlineable(v):
69 output[-1] += ' ' + self._dump_inline(v)
70 elif isinstance(v, dict):
71 output += self._dump_block(v, indent + 2)
72 elif isinstance(v, list):
73 output += self._dump_block(v, indent)
74 else:
75 raise NotImplementedError()
76 return output
77
78 def _inlineable(self, data):
79 if isinstance(data, list):
80 return all(not isinstance(item, (list, dict)) for item in data)
81 elif isinstance(data, dict):
82 return all(not isinstance(item, (list, dict))
83 for item in data.itervalues())
84 else:
85 return True
86
87 def _dump_list(self, data):
88 return '[%s]' % ', '.join(self._dump_inline(item) for item in data)
89
90 def _dump_dict(self, data):
91 return '{%s}' % ', '.join(
92 '%s: %s' % (self._dump_inline(key), self._dump_inline(value))
93 for key, value in data.iteritems())
94
95 def _dump_basestring(self, data):
96 if data in ('true', 'false', 'null'):
97 return "'%s'" % data
98 if "'" in data:
99 return "'%s'" % data.replace("'", "''")
100 if data == '':
101 return "''"
102 return data
103
104 def _dump_int(self, data):
105 return str(data)
106
107 def _dump_float(self, data):
108 return str(data)
109
110 def _dump_literal(self, data):
111 return {
112 True: 'true',
113 False: 'false',
114 None: 'null',
115 }[data]
116
117
118class Parser(object):
119 _spaces_re = re.compile(r'^(\s*)(.*)')
120 _list_re = re.compile(r'^(-\s+)(.*)')
121 _dict_re = re.compile(r'^((?![{[])[^-:]+):\s?(.*)')
122 _inline_list_re = re.compile(r"^([^',]+|(?:'')+|'.+?[^'](?:'')*')"
123 r"(?:, (.*))?$")
124
125 def __init__(self):
126 # Stack of (indent level, container object)
127 self._stack = []
128 # When a dict's value is a nested block, remember the key
129 self._parent_key = None
130
131 @property
132 def _indent(self):
133 return self._stack[-1][0]
134
135 @property
136 def _container(self):
137 return self._stack[-1][1]
138
139 @property
140 def _in_list(self):
141 return isinstance(self._container, list)
142
143 @property
144 def _in_dict(self):
145 return isinstance(self._container, dict)
146
147 def _push(self, container, indent=None):
148 in_list = self._in_list
149 assert in_list or self._parent_key
150
151 if indent is None:
152 indent = self._indent
153 self._stack.append((indent, container()))
154 if in_list:
155 self._stack[-2][1].append(self._container)
156 else:
157 self._stack[-2][1][self._parent_key] = self._container
158 self._parent_key = None
159
160 def parse(self, yaml):
161 if yaml.startswith(('[', '{')):
162 return self._parse_value(yaml)
163
164 if yaml.startswith('-'):
165 self._stack.append((0, []))
166 else:
167 self._stack.append((0, {}))
168
169 for line in yaml.splitlines():
170 spaces, line = self._spaces_re.match(line).groups()
171
172 while len(spaces) < self._indent:
173 self._stack.pop()
174
175 lm = self._list_re.match(line)
176 dm = self._dict_re.match(line)
177 if len(spaces) == self._indent:
178 if lm and self._in_dict:
179 # Starting a list in a dict
180 self._push(list)
181 elif dm and self._in_list:
182 # Left an embedded list
183 self._stack.pop()
184
185 if len(spaces) > self._indent:
186 assert self._parent_key
187 if dm:
188 # Nested dict
189 self._push(dict, len(spaces))
190 elif lm:
191 # Over-indented list in a dict
192 self._push(list, len(spaces))
193
194 indent = self._indent
195 while lm and lm.group(2).startswith('- '):
196 # Nested lists
197 prefix, line = lm.groups()
198 indent += len(prefix)
199 self._push(list, indent)
200 lm = self._list_re.match(line)
201 del indent
202
203 if lm:
204 prefix, line = lm.groups()
205 dm = self._dict_re.match(line)
206 if dm:
207 self._push(dict, self._indent + len(prefix))
208 else:
209 assert self._in_list
210 self._container.append(self._parse_value(line))
211
212 if dm:
213 key, value = dm.groups()
214 assert self._in_dict
215 if value:
216 self._container[key] = self._parse_value(value)
217 else:
218 self._parent_key = key
219
220 return self._stack[0][1]
221
222 def _parse_value(self, value):
223 if value.startswith("'") and value.endswith("'"):
224 return value[1:-1].replace("''", "'")
225 if value.startswith('[') and value.endswith(']'):
226 value = value[1:-1]
227 output = []
228 while value:
229 m = self._inline_list_re.match(value)
230 assert m, value
231 output.append(self._parse_value(m.group(1)))
232 value = m.group(2)
233 return output
234 if value.startswith('{') and value.endswith('}'):
235 value = value[1:-1]
236 output = {}
237 while value:
238 key, value = value.split(': ', 1)
239 m = self._inline_list_re.match(value)
240 assert m
241 output[key] = self._parse_value(m.group(1))
242 value = m.group(2)
243 return output
244 if value.startswith('!!'):
245 raise NotImplementedError()
246 if value == 'true':
247 return True
248 if value == 'false':
249 return False
250 if value == 'null':
251 return None
252 for type_ in (int, float):
253 try:
254 return type_(value)
255 except ValueError:
256 pass
257 return value
Note: See TracBrowser for help on using the repository browser.