tree-gen
C++ code generator for tree structures
tree-gen-python.cpp
Go to the documentation of this file.
1 
5 #include <fstream>
6 #include <iostream>
7 #include <unordered_set>
8 #include "tree-gen-python.hpp"
9 
10 namespace tree_gen {
11 namespace python {
12 
17  std::ofstream &stream,
18  const std::string &doc,
19  const std::string &indent = ""
20 ) {
21  auto word = std::ostringstream();
22  auto line = std::ostringstream();
23  line << indent << "\"\"\"";
24  bool line_empty = false;
25  bool first_word = true;
26  for (char c : doc) {
27  bool flush = false;
28  if (c == '\n' || c == ' ') {
29  if (first_word) {
30  first_word = false;
31  } else {
32  line << " ";
33  }
34  line << word.str();
35  word.str("");
36  line_empty = false;
37  if (c == '\n') {
38  flush = true;
39  }
40  } else {
41  word << c;
42  flush = !line_empty && line.str().size() + word.str().size() > 79;
43  }
44  if (flush) {
45  stream << line.str() << std::endl;
46  line.str("");
47  line << indent;
48  line_empty = true;
49  first_word = true;
50  }
51  }
52  if (!word.str().empty()) {
53  line << " " << word.str();
54  line_empty = false;
55  }
56  if (!line_empty) {
57  stream << line.str();
58  }
59  if (line.str().size() + 3 > 79) {
60  stream << std::endl << indent;
61  }
62  stream << "\"\"\"" << std::endl;
63 }
64 
70  std::ofstream &output,
71  Node &node
72 ) {
73  if (node.derived.empty()) {
74  output << " if typ == '" << node.title_case_name << "':" << std::endl;
75  output << " return " << node.title_case_name << "._deserialize(cbor, seq_to_ob, links)" << std::endl;
76  } else {
77  for (auto &derived : node.derived) {
78  generate_deserialize_mux(output, *(derived.lock()));
79  }
80  }
81 }
82 
87  std::ofstream &output,
88  Specification &spec,
89  Node &node
90 ) {
91  auto all_fields = node.all_fields();
92 
93  // Print class header.
94  output << "class " << node.title_case_name << "(";
95  if (node.parent) {
96  output << node.parent->title_case_name;
97  } else {
98  output << "Node";
99  }
100  output << "):" << std::endl;
101  if (!node.doc.empty()) {
102  format_doc(output, node.doc, " ");
103  output << std::endl;
104  }
105 
106  // Print slots for the fields.
107  output << " __slots__ = [";
108  if (!node.fields.empty()) {
109  output << std::endl;
110  for (const auto &field : node.fields) {
111  output << " '_attr_" << field.name << "'," << std::endl;
112  }
113  output << " ";
114  }
115  output << "]" << std::endl << std::endl;
116 
117  // Print constructor.
118  output << " def __init__(";
119  if (all_fields.empty()) {
120  output << "self";
121  } else {
122  output << std::endl;
123  output << " self," << std::endl;
124  for (const auto &field : all_fields) {
125  output << " " << field.name << "=None," << std::endl;
126  }
127  output << " ";
128  }
129  output << "):" << std::endl;
130  output << " super().__init__(";
131  if (node.parent) {
132  bool first = true;
133  for (const auto &field : node.parent->all_fields()) {
134  if (first) {
135  first = false;
136  } else {
137  output << ", ";
138  }
139  output << field.name << "=" << field.name;
140  }
141  }
142  output << ")" << std::endl;
143  for (const auto &field : node.fields) {
144  output << " self." << field.name << " = " << field.name << std::endl;
145  }
146  output << std::endl;
147 
148  // Print the field getters, setters, and deleters.
149  for (const auto &field : node.fields) {
150 
151  // Attributes representing Any or Many edges require a list-like class
152  // to sit between the field accessors and the user to make things
153  // like indexing work with type-safety. First figure out if this is such
154  // a field.
155  bool is_prim = field.type == Prim && field.ext_type == Prim;
156  bool is_any = field.type == Any || (field.type == Prim && field.ext_type == Any);
157  bool is_many = field.type == Many || (field.type == Prim && field.ext_type == Many);
158  bool is_any_or_many = is_any || is_many;
159  bool is_link = field.type == Link || (field.type == Prim && field.ext_type == Link);
160  is_link |= field.type == OptLink || (field.type == Prim && field.ext_type == OptLink);
161  std::string type = (field.type == Prim) ? field.py_prim_type : field.node_type->title_case_name;
162 
163  if (is_any_or_many) {
164  auto split = type.rfind('.');
165  if (split == std::string::npos) {
166  type = "Multi" + type;
167  } else {
168  type = type.substr(0, split + 1) + "Multi" + type.substr(split + 1);
169  }
170  }
171 
172  // Getter.
173  output << " @property" << std::endl;
174  output << " def " << field.name << "(self):" << std::endl;
175  if (!field.doc.empty()) {
176  format_doc(output, field.doc, " ");
177  }
178  output << " return self._attr_" << field.name << std::endl << std::endl;
179 
180  // Setter. Assigning None is the same as deleting.
181  output << " @" << field.name << ".setter" << std::endl;
182  output << " def " << field.name << "(self, val):" << std::endl;
183  output << " if val is None:" << std::endl;
184  output << " del self." << field.name << std::endl;
185  output << " return" << std::endl;
186  output << " if not isinstance(val, " << type << "):" << std::endl;
187  if (!is_link) {
188  output << " # Try to \"typecast\" if this isn't an obvious mistake." << std::endl;
189  output << " if isinstance(val, Node):" << std::endl;
190  output << " raise TypeError('" << field.name << " must be of type " << type << "')" << std::endl;
191  output << " val = " << type << "(val)" << std::endl;
192  } else {
193  // Can't typecast links; making a new object makes no sense.
194  output << " raise TypeError('" << field.name << " must be of type " << type << "')" << std::endl;
195  }
196  output << " self._attr_" << field.name << " = ";
197  output << "val" << std::endl << std::endl;
198 
199  // Deleter. Doesn't actually delete, but rather replaces with the
200  // default value.
201  output << " @" << field.name << ".deleter" << std::endl;
202  output << " def " << field.name << "(self):" << std::endl;
203  output << " self._attr_" << field.name;
204  if (is_prim || is_any_or_many) {
205  output << " = " << type << "()";
206  } else {
207  output << " = None";
208  }
209  output << std::endl << std::endl;
210 
211  }
212 
213  // Print equality function.
214  if (node.derived.empty()) {
215  output << " def __eq__(self, other):" << std::endl;
216  format_doc(output, "Equality operator. Ignores annotations!", " ");
217  output << " if not isinstance(other, " << node.title_case_name << "):" << std::endl;
218  output << " return False" << std::endl;
219  for (const auto &field : all_fields) {
220  EdgeType type = (field.type == Prim) ? field.ext_type : field.type;
221  switch (type) {
222  case Maybe:
223  case One:
224  case Any:
225  case Many:
226  case Prim:
227  output << " if self." << field.name << " != other." << field.name << ":" << std::endl;
228  break;
229  case Link:
230  case OptLink:
231  output << " if self." << field.name << " is not other." << field.name << ":" << std::endl;
232  break;
233  }
234  output << " return False" << std::endl;
235  }
236  output << " return True" << std::endl << std::endl;
237  }
238 
239  // Print dump function.
240  if (node.derived.empty()) {
241  output << " def dump(self, indent=0, annotations=None, links=1):" << std::endl;
242  format_doc(output,
243  "Returns a debug representation of this tree as a "
244  "multiline string. indent is the number of double spaces "
245  "prefixed before every line. annotations, if specified, "
246  "must be a set-like object containing the key strings of "
247  "the annotations that are to be printed. links specifies "
248  "the maximum link recursion depth.", " ");
249  output << " s = [' '*indent]" << std::endl;
250  output << " s.append('" << node.title_case_name << "(')" << std::endl;
251  output << " if annotations is None:" << std::endl;
252  output << " annotations = []" << std::endl;
253  output << " for key in annotations:" << std::endl;
254  output << " if key in self:" << std::endl;
255  output << " s.append(' # {}: {}'.format(key, self[key]))" << std::endl;
256  output << " s.append('\\n')" << std::endl;
257  if (!all_fields.empty()) {
258  output << " indent += 1" << std::endl;
259  for (auto &field : all_fields) {
260  EdgeType type = (field.type == Prim) ? field.ext_type : field.type;
261  output << " s.append(' '*indent)" << std::endl;
262  output << " s.append('" << field.name;
263  if (type == Link || type == OptLink) {
264  output << " --> ";
265  } else {
266  output << ": ";
267  }
268  output << "')" << std::endl;
269  switch (type) {
270  case Maybe:
271  case One:
272  case OptLink:
273  case Link:
274  output << " if self." << field.name << " is None:" << std::endl;
275  if (type == One || type == Link) {
276  output << " s.append('!MISSING\\n')" << std::endl;
277  } else {
278  output << " s.append('-\\n')" << std::endl;
279  }
280  output << " else:" << std::endl;
281  output << " s.append('<\\n')" << std::endl;
282  if (field.ext_type == Link || field.ext_type == OptLink) {
283  output << " if links:" << std::endl;
284  output << " s.append(self." << field.name << ".dump(indent + 1, annotations, links - 1) + '\\n')" << std::endl;
285  output << " else:" << std::endl;
286  output << " s.append(' '*(indent+1) + '...\\n')" << std::endl;
287  } else {
288  output << " s.append(self." << field.name << ".dump(indent + 1, annotations, links) + '\\n')" << std::endl;
289  }
290  output << " s.append(' '*indent + '>\\n')" << std::endl;
291  break;
292 
293  case Any:
294  case Many:
295  output << " if not self." << field.name << ":" << std::endl;
296  if (field.ext_type == Many) {
297  output << " s.append('!MISSING\\n')" << std::endl;
298  } else {
299  output << " s.append('-\\n')" << std::endl;
300  }
301  output << " else:" << std::endl;
302  output << " s.append('[\\n')" << std::endl;
303  output << " for child in self." << field.name << ":" << std::endl;
304  output << " s.append(child.dump(indent + 1, annotations, links) + '\\n')" << std::endl;
305  output << " s.append(' '*indent + ']\\n')" << std::endl;
306  break;
307 
308  case Prim:
309  output << " s.append(str(self." << field.name << ") + '\\n')" << std::endl;
310  break;
311 
312  }
313  }
314  output << " indent -= 1" << std::endl;
315  output << " s.append(' '*indent)" << std::endl;
316  }
317  output << " s.append(')')" << std::endl;
318  output << " return ''.join(s)" << std::endl << std::endl;
319  output << " __str__ = dump" << std::endl;
320  output << " __repr__ = dump" << std::endl << std::endl;
321  }
322 
323  // Print find_reachable() function.
324  if (node.derived.empty()) {
325  output << " def find_reachable(self, id_map=None):" << std::endl;
326  format_doc(output,
327  "Returns a dictionary mapping Python id() values to "
328  "stable sequence numbers for all nodes in the tree rooted "
329  "at this node. If id_map is specified, found nodes are "
330  "appended to it.", " ");
331  output << " if id_map is None:" << std::endl;
332  output << " id_map = {}" << std::endl;
333  output << " if id(self) in id_map:" << std::endl;
334  output << " raise NotWellFormed('node {!r} with id {} occurs more than once'.format(self, id(self)))" << std::endl;
335  output << " id_map[id(self)] = len(id_map)" << std::endl;
336  for (const auto &field : all_fields) {
337  EdgeType type = (field.type == Prim) ? field.ext_type : field.type;
338  switch (type) {
339  case Maybe:
340  case One:
341  output << " if self._attr_" << field.name << " is not None:" << std::endl;
342  output << " self._attr_" << field.name << ".find_reachable(id_map)" << std::endl;
343  break;
344  case Any:
345  case Many:
346  output << " for el in self._attr_" << field.name << ":" << std::endl;
347  output << " el.find_reachable(id_map)" << std::endl;
348  break;
349  case Link:
350  case OptLink:
351  case Prim:
352  break;
353  }
354  }
355  output << " return id_map" << std::endl << std::endl;
356  }
357 
358  // Print check_complete() function.
359  if (node.derived.empty()) {
360  output << " def check_complete(self, id_map=None):" << std::endl;
361  format_doc(output,
362  "Raises NotWellFormed if the tree rooted at this node "
363  "is not well-formed. If id_map is specified, this tree is "
364  "only a subtree in the context of a larger tree, and id_map "
365  "must be a dict mapping from Python id() codes to tree "
366  "indices for all reachable nodes.", " ");
367  output << " if id_map is None:" << std::endl;
368  output << " id_map = self.find_reachable()" << std::endl;
369  for (const auto &field : all_fields) {
370  EdgeType type = (field.type == Prim) ? field.ext_type : field.type;
371  switch (type) {
372  case One:
373  output << " if self._attr_" << field.name << " is None:" << std::endl;
374  output << " raise NotWellFormed('";
375  output << field.name << " is required but not set')" << std::endl;
376  // fallthrough
377  case Maybe:
378  output << " if self._attr_" << field.name << " is not None:" << std::endl;
379  output << " self._attr_" << field.name << ".check_complete(id_map)" << std::endl;
380  break;
381  case Many:
382  output << " if not self._attr_" << field.name << ":" << std::endl;
383  output << " raise NotWellFormed('";
384  output << field.name << " needs at least one node but has zero')" << std::endl;
385  // fallthrough
386  case Any:
387  output << " for child in self._attr_" << field.name << ":" << std::endl;
388  output << " child.check_complete(id_map)" << std::endl;
389  break;
390  case Link:
391  output << " if self._attr_" << field.name << " is None:" << std::endl;
392  output << " raise NotWellFormed('";
393  output << field.name << " is required but not set')" << std::endl;
394  // fallthrough
395  case OptLink:
396  output << " if self._attr_" << field.name << " is not None:" << std::endl;
397  output << " if id(self._attr_" << field.name << ") not in id_map:" << std::endl;
398  output << " raise NotWellFormed('";
399  output << field.name << " links to unreachable node')" << std::endl;
400  break;
401  case Prim:
402  break;
403  }
404  }
405  output << std::endl;
406  }
407 
408  // Print copy() function.
409  if (node.derived.empty()) {
410  output << " def copy(self):" << std::endl;
411  format_doc(output, "Returns a shallow copy of this node.", " ");
412  output << " return " << node.title_case_name << "(" << std::endl;
413  bool first = true;
414  for (const auto &field : all_fields) {
415  if (first) {
416  first = false;
417  } else {
418  output << "," << std::endl;
419  }
420  output << " " << field.name << "=";
421  auto type = (field.type != Prim) ? field.type : field.ext_type;
422  switch (type) {
423  case Maybe:
424  case One:
425  case OptLink:
426  case Link:
427  case Prim:
428  output << "self._attr_" << field.name;
429  break;
430  case Any:
431  case Many:
432  output << "self._attr_" << field.name << ".copy()";
433  break;
434  }
435  }
436  output << std::endl << " )" << std::endl << std::endl;
437  }
438 
439  // Print clone() function.
440  if (node.derived.empty()) {
441  output << " def clone(self):" << std::endl;
442  format_doc(output,
443  "Returns a deep copy of this node. This mimics the "
444  "C++ interface, deficiencies with links included; that is, "
445  "links always point to the original tree. If you're not "
446  "cloning a subtree in a context where this is the desired "
447  "behavior, you may want to use the copy.deepcopy() from the "
448  "stdlib instead, which should copy links correctly.",
449  " ");
450  output << " return " << node.title_case_name << "(" << std::endl;
451  bool first = true;
452  for (const auto &field : all_fields) {
453  if (first) {
454  first = false;
455  } else {
456  output << "," << std::endl;
457  }
458  output << " " << field.name << "=";
459  auto type = (field.type != Prim) ? field.type : field.ext_type;
460  switch (type) {
461  case Maybe:
462  case One:
463  case Any:
464  case Many:
465  case Prim:
466  output << "_cloned(self._attr_" << field.name << ")";
467  break;
468  case OptLink:
469  case Link:
470  output << "self._attr_" << field.name;
471  break;
472  }
473  }
474  output << std::endl << " )" << std::endl << std::endl;
475  }
476 
477  // Print deserialize() function.
478  output << " @staticmethod" << std::endl;
479  output << " def _deserialize(cbor, seq_to_ob, links):" << std::endl;
480  format_doc(output,
481  "Attempts to deserialize the given cbor object (in Python "
482  "primitive representation) into a node of this type. All "
483  "(sub)nodes are added to the seq_to_ob dict, indexed by their "
484  "cbor sequence number. All links are registered in the links "
485  "list by means of a two-tuple of the setter function for the "
486  "link field and the sequence number of the target node.",
487  " ");
488  output << " if not isinstance(cbor, dict):" << std::endl;
489  output << " raise TypeError('node description object must be a dict')" << std::endl;
490  output << " typ = cbor.get('@t', None)" << std::endl;
491  output << " if typ is None:" << std::endl;
492  output << " raise ValueError('type (@t) field is missing from node serialization')" << std::endl;
493  if (node.derived.empty()) {
494  output << " if typ != '" << node.title_case_name << "':" << std::endl;
495  output << " raise ValueError('found node serialization for ' + typ + ', but expected ";
496  output << node.title_case_name << "')" << std::endl;
497  if (all_fields.empty()) {
498  output << std::endl;
499  output << " # Construct the " << node.title_case_name << " node." << std::endl;
500  output << " node = " << node.title_case_name << "()" << std::endl;
501  } else {
502  std::vector<std::string> links;
503  for (const auto &field : all_fields) {
504  output << std::endl;
505  output << " # Deserialize the " << field.name << " field." << std::endl;
506  output << " field = cbor.get('" << field.name << "', None)" << std::endl;
507  output << " if not isinstance(field, dict):" << std::endl;
508  output << " raise ValueError('missing or invalid serialization of field " << field.name << "')" << std::endl;
509  auto type = (field.type == Prim) ? field.ext_type : field.type;
510  auto type_name = (field.type == Prim) ? field.py_prim_type : field.node_type->title_case_name;
511  auto multi_name = (field.type == Prim) ? field.py_multi_type : ("Multi" + field.node_type->title_case_name);
512  if (type != Prim) {
513  output << " if field.get('@T') != '";
514  switch (type) {
515  case Maybe: output << "?"; break;
516  case One: output << "1"; break;
517  case Any: output << "*"; break;
518  case Many: output << "+"; break;
519  case OptLink: output << "@"; break;
520  case Link: output << "$"; break;
521  case Prim: throw std::runtime_error("internal error, should be unreachable");
522  }
523  output << "':" << std::endl;
524  output << " raise ValueError('unexpected edge type for field " << field.name << "')" << std::endl;
525  }
526  switch (type) {
527  case Maybe:
528  case One:
529  output << " if field.get('@t', None) is None:" << std::endl;
530  output << " f_" << field.name << " = None" << std::endl;
531  output << " else:" << std::endl;
532  output << " f_" << field.name << " = " << type_name << "._deserialize(field, seq_to_ob, links)" << std::endl;
533  break;
534  case Any:
535  case Many:
536  output << " data = field.get('@d', None)" << std::endl;
537  output << " if not isinstance(data, list):" << std::endl;
538  output << " raise ValueError('missing serialization of Any/Many contents')" << std::endl;
539  output << " f_" << field.name << " = " << multi_name << "()" << std::endl;
540  output << " for element in data:" << std::endl;
541  output << " if element.get('@T') != '1':" << std::endl;
542  output << " raise ValueError('unexpected edge type for Any/Many element')" << std::endl;
543  output << " f_" << field.name << ".append(" << type_name << "._deserialize(element, seq_to_ob, links))" << std::endl;
544  break;
545  case Link:
546  case OptLink:
547  output << " f_" << field.name << " = None" << std::endl;
548  output << " l_" << field.name << " = field.get('@l', None)" << std::endl;
549  links.push_back(field.name);
550  break;
551  case Prim:
552  output << " if hasattr(" << field.py_prim_type << ", 'deserialize_cbor'):" << std::endl;
553  output << " f_" << field.name << " = " << field.py_prim_type << ".deserialize_cbor(field)" << std::endl;
554  output << " else:" << std::endl;
555  if (spec.py_deserialize_fn.empty()) {
556  output << " raise ValueError('no deserialization function seems to exist for field type " << field.py_prim_type << "')" << std::endl;
557  } else {
558  output << " f_" << field.name << " = " << spec.py_deserialize_fn << "(" << field.py_prim_type << ", field)" << std::endl;
559  }
560  break;
561  }
562  }
563  output << std::endl;
564  output << " # Construct the " << node.title_case_name << " node." << std::endl;
565  output << " node = " << node.title_case_name << "(";
566  bool first = true;
567  for (const auto &field : all_fields) {
568  if (first) {
569  first = false;
570  } else {
571  output << ", ";
572  }
573  output << "f_" << field.name;
574  }
575  output << ")" << std::endl;
576  if (!links.empty()) {
577  output << std::endl;
578  output << " # Register links to be made after tree construction." << std::endl;
579  for (const auto &link : links) {
580  output << " links.append((lambda val: " << node.title_case_name << "." << link << ".fset(node, val), l_" << link << "))" << std::endl;
581  }
582  }
583  }
584  output << std::endl;
585  output << " # Deserialize annotations." << std::endl;
586  output << " for key, val in cbor.items():" << std::endl;
587  output << " if not (key.startswith('{') and key.endswith('}')):" << std::endl;
588  output << " continue" << std::endl;
589  output << " key = key[1:-1]" << std::endl;
590  if (spec.py_deserialize_fn.empty()) {
591  output << " node[key] = val" << std::endl;
592  } else {
593  output << " node[key] = " << spec.py_deserialize_fn << "(key, val)" << std::endl;
594  }
595  output << std::endl;
596  output << " # Register node in sequence number lookup." << std::endl;
597  output << " seq = cbor.get('@i', None)" << std::endl;
598  output << " if not isinstance(seq, int):" << std::endl;
599  output << " raise ValueError('sequence number field (@i) is not an integer or missing from node serialization')" << std::endl;
600  output << " if seq in seq_to_ob:" << std::endl;
601  output << " raise ValueError('duplicate sequence number %d' % seq)" << std::endl;
602  output << " seq_to_ob[seq] = node" << std::endl << std::endl;
603  output << " return node" << std::endl;
604  } else {
605  generate_deserialize_mux(output, node);
606  output << " raise ValueError('unknown or unexpected type (@t) found in node serialization')" << std::endl;
607  }
608  output << std::endl;
609 
610  // Print serialize() function.
611  output << " def _serialize(self, id_map):" << std::endl;
612  format_doc(output,
613  "Serializes this node to the Python primitive "
614  "representation of its CBOR serialization. The tree that the "
615  "node belongs to must be well-formed. id_map must match Python "
616  "id() calls for all nodes to unique integers, to use for the "
617  "sequence number representation of links.",
618  " ");
619  output << " cbor = {'@i': id_map[id(self)], '@t': '" << node.title_case_name << "'}" << std::endl;
620  for (const auto &field : all_fields) {
621  output << std::endl;
622  output << " # Serialize the " << field.name << " field." << std::endl;
623  auto type = (field.type == Prim) ? field.ext_type : field.type;
624  auto type_name = (field.type == Prim) ? field.py_prim_type : field.node_type->title_case_name;
625  if (type == Prim) {
626  output << " if hasattr(self._attr_" << field.name << ", 'serialize_cbor'):" << std::endl;
627  output << " cbor['" << field.name << "'] = self._attr_" << field.name << ".serialize_cbor()" << std::endl;
628  output << " else:" << std::endl;
629  if (spec.py_serialize_fn.empty()) {
630  output << " raise ValueError('no serialization function seems to exist for field type " << field.py_prim_type << "')" << std::endl;
631  } else {
632  output << " cbor['" << field.name << "'] = " << spec.py_serialize_fn << "(" << field.py_prim_type << ", self._attr_" << field.name << ")" << std::endl;
633  }
634  } else {
635  output << " field = {'@T': '";
636  switch (type) {
637  case Maybe: output << "?"; break;
638  case One: output << "1"; break;
639  case Any: output << "*"; break;
640  case Many: output << "+"; break;
641  case OptLink: output << "@"; break;
642  case Link: output << "$"; break;
643  case Prim: throw std::runtime_error("internal error, should be unreachable");
644  }
645  output << "'}" << std::endl;
646  switch (type) {
647  case Maybe:
648  case One:
649  output << " if self._attr_" << field.name << " is None:" << std::endl;
650  output << " field['@t'] = None" << std::endl;
651  output << " else:" << std::endl;
652  output << " field.update(self._attr_" << field.name << "._serialize(id_map))" << std::endl;
653  break;
654  case Any:
655  case Many:
656  output << " lst = []" << std::endl;
657  output << " for el in self._attr_" << field.name << ":" << std::endl;
658  output << " el = el._serialize(id_map)" << std::endl;
659  output << " el['@T'] = '1'" << std::endl;
660  output << " lst.append(el)" << std::endl;
661  output << " field['@d'] = lst" << std::endl;
662  break;
663  case Link:
664  case OptLink:
665  output << " if self._attr_" << field.name << " is None:" << std::endl;
666  output << " field['@l'] = None" << std::endl;
667  output << " else:" << std::endl;
668  output << " field['@l'] = id_map[id(self._attr_" << field.name << ")]" << std::endl;
669  break;
670  case Prim: throw std::runtime_error("internal error, should be unreachable");
671  }
672  output << " cbor['" << field.name << "'] = field" << std::endl;
673  }
674  }
675  output << std::endl;
676  output << " # Serialize annotations." << std::endl;
677  output << " for key, val in self._annot.items():" << std::endl;
678  if (spec.py_serialize_fn.empty()) {
679  output << " try:" << std::endl;
680  output << " cbor['{%s}' % key] = _py_to_cbor(val)" << std::endl;
681  output << " except TypeError:" << std::endl;
682  output << " pass" << std::endl;
683  } else {
684  output << " cbor['{%s}' % key] = _py_to_cbor(" << spec.py_serialize_fn << "(key, val))" << std::endl;
685  }
686  output << std::endl;
687  output << " return cbor" << std::endl << std::endl;
688 
689  output << std::endl;
690 
691  // Print Multi* class.
692  output << "class Multi" << node.title_case_name << "(_Multiple):" << std::endl;
693  auto doc = "Wrapper for an edge with multiple " + node.title_case_name + " objects.";
694  format_doc(output, doc, " ");
695  output << std::endl;
696  output << " _T = " << node.title_case_name << std::endl;
697  output << std::endl << std::endl;
698 
699  // Add to the typemap.
700  output << "_typemap['" << node.title_case_name << "'] = " << node.title_case_name << std::endl << std::endl;
701 
702 }
703 
707 void generate(
708  const std::string &python_filename,
709  Specification &specification
710 ) {
711  auto nodes = specification.nodes;
712 
713  // Open the output file.
714  auto output = std::ofstream(python_filename);
715  if (!output.is_open()) {
716  std::cerr << "Failed to open Python file for writing" << std::endl;
717  std::exit(1);
718  }
719 
720  // Generate header.
721  if (!specification.python_doc.empty()) {
722  format_doc(output, specification.python_doc);
723  output << std::endl;
724  }
725  output << "import functools" << std::endl;
726  output << "import struct" << std::endl;
727  for (auto &include : specification.python_includes) {
728  output << include << std::endl;
729  }
730  output << std::endl;
731 
732  // Write the classes that are always the same.
733  output << R"PY(
734 _typemap = {}
735 
736 
737 def _cbor_read_intlike(cbor, offset, info):
738  """Parses the additional information and reads any additional bytes it
739  specifies the existence of, and returns the encoded integer. offset
740  should point to the byte immediately following the initial byte. Returns
741  the encoded integer and the offset immediately following the object."""
742 
743  # Info less than 24 is a shorthand for the integer itself.
744  if info < 24:
745  return info, offset
746 
747  # 24 is 8-bit following the info byte.
748  if info == 24:
749  return cbor[offset], offset + 1
750 
751  # 25 is 16-bit following the info byte.
752  if info == 25:
753  val, = struct.unpack('>H', cbor[offset:offset+2])
754  return val, offset + 2
755 
756  # 26 is 32-bit following the info byte.
757  if info == 26:
758  val, = struct.unpack('>I', cbor[offset:offset+4])
759  return val, offset + 4
760 
761  # 27 is 64-bit following the info byte.
762  if info == 27:
763  val, = struct.unpack('>Q', cbor[offset:offset+8])
764  return val, offset + 8
765 
766  # Info greater than or equal to 28 is illegal. Note that 31 is used for
767  # indefinite lengths, so this must be checked prior to calling this
768  # method.
769  raise ValueError("invalid CBOR: illegal additional info for integer or object length")
770 
771 )PY" << R"PY(
772 def _sub_cbor_to_py(cbor, offset):
773  """Converts the CBOR object starting at cbor[offset] to its Python
774  representation for as far as tree-gen supports CBOR. Returns this Python
775  representation and the offset immediately following the CBOR representation
776  thereof. Supported types:
777 
778  - 0: unsigned integer (int)
779  - 1: negative integer (int)
780  - 2: byte string (bytes)
781  - 3: UTF-8 string (str)
782  - 4: array (list)
783  - 5: map (dict)
784  - 6: semantic tag (ignored)
785  - 7.20: false (bool)
786  - 7.21: true (bool)
787  - 7.22: null (NoneType)
788  - 7.27: double-precision float (float)
789 
790  Both definite-length and indefinite-length notation is supported for sized
791  objects (strings, arrays, maps). A ValueError is thrown if the CBOR is
792  invalid or contains unsupported structures."""
793 
794  # Read the initial byte.
795  initial = cbor[offset]
796  typ = initial >> 5
797  info = initial & 0x1F
798  offset += 1
799 
800  # Handle unsigned integer (0) and negative integer (1).
801  if typ <= 1:
802  value, offset = _cbor_read_intlike(cbor, offset, info)
803  if typ == 1:
804  value = -1 - value
805  return value, offset
806 
807  # Handle byte string (2) and UTF-8 string (3).
808  if typ <= 3:
809 
810  # Gather components of the string in here.
811  if info == 31:
812 
813  # Handle indefinite length strings. These consist of a
814  # break-terminated (0xFF) list of definite-length strings of the
815  # same type.
816  value = []
817  while True:
818  sub_initial = cbor[offset]; offset += 1
819  if sub_initial == 0xFF:
820  break
821  sub_typ = sub_initial >> 5
822  sub_info = sub_initial & 0x1F
823  if sub_typ != typ:
824  raise ValueError('invalid CBOR: illegal indefinite-length string component')
825 
826  # Seek past definite-length string component. The size in
827  # bytes is encoded as an integer.
828  size, offset = _cbor_read_intlike(cbor, offset, sub_info)
829  value.append(cbor[offset:offset + size])
830  offset += size
831  value = b''.join(value)
832 
833  else:
834 
835  # Handle definite-length strings. The size in bytes is encoded as
836  # an integer.
837  size, offset = _cbor_read_intlike(cbor, offset, info)
838  value = cbor[offset:offset + size]
839  offset += size
840 
841  if typ == 3:
842  value = value.decode('UTF-8')
843  return value, offset
844 
845  # Handle array (4) and map (5).
846  if typ <= 5:
847 
848  # Create result container.
849  container = [] if typ == 4 else {}
850 
851  # Handle indefinite length arrays and maps.
852  if info == 31:
853 
854  # Read objects/object pairs until we encounter a break.
855  while cbor[offset] != 0xFF:
856  if typ == 4:
857  value, offset = _sub_cbor_to_py(cbor, offset)
858  container.append(value)
859  else:
860  key, offset = _sub_cbor_to_py(cbor, offset)
861  if not isinstance(key, str):
862  raise ValueError('invalid CBOR: map key is not a UTF-8 string')
863  value, offset = _sub_cbor_to_py(cbor, offset)
864  container[key] = value
865 
866  # Seek past the break.
867  offset += 1
868 
869  else:
870 
871  # Handle definite-length arrays and maps. The amount of
872  # objects/object pairs is encoded as an integer.
873  size, offset = _cbor_read_intlike(cbor, offset, info)
874  for _ in range(size):
875  if typ == 4:
876  value, offset = _sub_cbor_to_py(cbor, offset)
877  container.append(value)
878  else:
879  key, offset = _sub_cbor_to_py(cbor, offset)
880  if not isinstance(key, str):
881  raise ValueError('invalid CBOR: map key is not a UTF-8 string')
882  value, offset = _sub_cbor_to_py(cbor, offset)
883  container[key] = value
884 
885  return container, offset
886 
887  # Handle semantic tags.
888  if typ == 6:
889 
890  # We don't use semantic tags for anything, but ignoring them is
891  # legal and reading past them is easy enough.
892  _, offset = _cbor_read_intlike(cbor, offset, info)
893  return _sub_cbor_to_py(cbor, offset)
894 
895  # Handle major type 7. Here, the type is defined by the additional info.
896  # Additional info 24 is reserved for having the type specified by the
897  # next byte, but all such values are unassigned.
898  if info == 20:
899  # false
900  return False, offset
901 
902  if info == 21:
903  # true
904  return True, offset
905 
906  if info == 22:
907  # null
908  return None, offset
909 
910  if info == 23:
911  # Undefined value.
912  raise ValueError('invalid CBOR: undefined value is not supported')
913 
914  if info == 25:
915  # Half-precision float.
916  raise ValueError('invalid CBOR: half-precision float is not supported')
917 
918  if info == 26:
919  # Single-precision float.
920  raise ValueError('invalid CBOR: single-precision float is not supported')
921 
922  if info == 27:
923  # Double-precision float.
924  value, = struct.unpack('>d', cbor[offset:offset+8])
925  return value, offset + 8
926 
927  if info == 31:
928  # Break value used for indefinite-length objects.
929  raise ValueError('invalid CBOR: unexpected break')
930 
931  raise ValueError('invalid CBOR: unknown type code')
932 
933 )PY" << R"PY(
934 def _cbor_to_py(cbor):
935  """Converts the given CBOR object (bytes) to its Python representation for
936  as far as tree-gen supports CBOR. Supported types:
937 
938  - 0: unsigned integer (int)
939  - 1: negative integer (int)
940  - 2: byte string (bytes)
941  - 3: UTF-8 string (str)
942  - 4: array (list)
943  - 5: map (dict)
944  - 6: semantic tag (ignored)
945  - 7.20: false (bool)
946  - 7.21: true (bool)
947  - 7.22: null (NoneType)
948  - 7.27: double-precision float (float)
949 
950  Both definite-length and indefinite-length notation is supported for sized
951  objects (strings, arrays, maps). A ValueError is thrown if the CBOR is
952  invalid or contains unsupported structures."""
953 
954  value, length = _sub_cbor_to_py(cbor, 0)
955  if length < len(cbor):
956  raise ValueError('invalid CBOR: garbage at the end')
957  return value
958 
959 
960 class _Cbor(bytes):
961  """Marker class indicating that this bytes object represents CBOR."""
962  pass
963 
964 
965 def _cbor_write_intlike(value, major=0):
966  """Converts the given integer to its minimal representation in CBOR. The
967  major code can be overridden to write lengths for strings, arrays, and
968  maps."""
969 
970  # Negative integers use major code 1.
971  if value < 0:
972  major = 1
973  value = -1 - value
974  initial = major << 5
975 
976  # Use the minimal representation.
977  if value < 24:
978  return struct.pack('>B', initial | value)
979  if value < 0x100:
980  return struct.pack('>BB', initial | 24, value)
981  if value < 0x10000:
982  return struct.pack('>BH', initial | 25, value)
983  if value < 0x100000000:
984  return struct.pack('>BI', initial | 26, value)
985  if value < 0x10000000000000000:
986  return struct.pack('>BQ', initial | 27, value)
987 
988  raise ValueError('integer too large for CBOR (bigint not supported)')
989 
990 
991 def _py_to_cbor(value, type_converter=None):
992  """Inverse of _cbor_to_py(). type_converter optionally specifies a function
993  that takes a value and either converts it to a primitive for serialization,
994  converts it to a _Cbor object manually, or raises a TypeError if no
995  conversion is known. If no type_converter is specified, a TypeError is
996  raised in all cases the type_converter would otherwise be called. The cbor
997  serialization is returned using a _Cbor object, which is just a marker class
998  behaving just like bytes."""
999  if isinstance(value, _Cbor):
1000  return value
1001 
1002  if isinstance(value, int):
1003  return _Cbor(_cbor_write_intlike(value))
1004 
1005  if isinstance(value, float):
1006  return _Cbor(struct.pack('>Bd', 0xFB, value))
1007 
1008  if isinstance(value, str):
1009  value = value.encode('UTF-8')
1010  return _Cbor(_cbor_write_intlike(len(value), 3) + value)
1011 
1012  if isinstance(value, bytes):
1013  return _Cbor(_cbor_write_intlike(len(value), 2) + value)
1014 
1015  if value is False:
1016  return _Cbor(b'\xF4')
1017 
1018  if value is True:
1019  return _Cbor(b'\xF5')
1020 
1021  if value is None:
1022  return _Cbor(b'\xF6')
1023 
1024  if isinstance(value, (list, tuple)):
1025  cbor = [_cbor_write_intlike(len(value), 4)]
1026  for val in value:
1027  cbor.append(_py_to_cbor(val, type_converter))
1028  return _Cbor(b''.join(cbor))
1029 
1030  if isinstance(value, dict):
1031  cbor = [_cbor_write_intlike(len(value), 5)]
1032  for key, val in sorted(value.items()):
1033  if not isinstance(key, str):
1034  raise TypeError('dict keys must be strings')
1035  cbor.append(_py_to_cbor(key, type_converter))
1036  cbor.append(_py_to_cbor(val, type_converter))
1037  return _Cbor(b''.join(cbor))
1038 
1039  if type_converter is not None:
1040  return _py_to_cbor(type_converter(value))
1041 
1042  raise TypeError('unsupported type for conversion to cbor: %r' % (value,))
1043 
1044 )PY" << R"PY(
1045 class NotWellFormed(ValueError):
1046  """Exception class for well-formedness checks."""
1047 
1048  def __init__(self, msg):
1049  super().__init__('not well-formed: ' + str(msg))
1050 
1051 
1052 class Node(object):
1053  """Base class for nodes."""
1054 
1055  __slots__ = ['_annot']
1056 
1057  def __init__(self):
1058  super().__init__()
1059  self._annot = {}
1060 
1061  def __getitem__(self, key):
1062  """Returns the annotation object with the specified key, or raises
1063  KeyError if not found."""
1064  if not isinstance(key, str):
1065  raise TypeError('indexing a node with something other than an '
1066  'annotation key string')
1067  return self._annot[key]
1068 
1069  def __setitem__(self, key, val):
1070  """Assigns the annotation object with the specified key."""
1071  if not isinstance(key, str):
1072  raise TypeError('indexing a node with something other than an '
1073  'annotation key string')
1074  self._annot[key] = val
1075 
1076  def __delitem__(self, key):
1077  """Deletes the annotation object with the specified key."""
1078  if not isinstance(key, str):
1079  raise TypeError('indexing a node with something other than an '
1080  'annotation key string')
1081  del self._annot[key]
1082 
1083  def __contains__(self, key):
1084  """Returns whether an annotation exists for the specified key."""
1085  return key in self._annot
1086 
1087  @staticmethod
1088  def find_reachable(self, id_map=None):
1089  """Returns a dictionary mapping Python id() values to stable sequence
1090  numbers for all nodes in the tree rooted at this node. If id_map is
1091  specified, found nodes are appended to it. Note that this is overridden
1092  by the actual node class implementations; this base function does very
1093  little."""
1094  if id_map is None:
1095  id_map = {}
1096  return id_map
1097 
1098  def check_complete(self, id_map=None):
1099  """Raises NotWellFormed if the tree rooted at this node is not
1100  well-formed. If id_map is specified, this tree is only a subtree in the
1101  context of a larger tree, and id_map must be a dict mapping from Python
1102  id() codes to tree indices for all reachable nodes. Note that this is
1103  overridden by the actual node class implementations; this base function
1104  always raises an exception."""
1105  raise NotWellFormed('found node of abstract type ' + type(self).__name__)
1106 
1107  def check_well_formed(self):
1108  """Checks whether the tree starting at this node is well-formed. That
1109  is:
1110 
1111  - all One, Link, and Many edges have (at least) one entry;
1112  - all the One entries internally stored by Any/Many have an entry;
1113  - all Link and filled OptLink nodes link to a node that's reachable
1114  from this node;
1115  - the nodes referred to be One/Maybe only appear once in the tree
1116  (except through links).
1117 
1118  If it isn't well-formed, a NotWellFormed is thrown."""
1119  self.check_complete()
1120 
1121  def is_well_formed(self):
1122  """Returns whether the tree starting at this node is well-formed. That
1123  is:
1124 
1125  - all One, Link, and Many edges have (at least) one entry;
1126  - all the One entries internally stored by Any/Many have an entry;
1127  - all Link and filled OptLink nodes link to a node that's reachable
1128  from this node;
1129  - the nodes referred to be One/Maybe only appear once in the tree
1130  (except through links)."""
1131  try:
1132  self.check_well_formed()
1133  return True
1134  except NotWellFormed:
1135  return False
1136 
1137  def copy(self):
1138  """Returns a shallow copy of this node. Note that this is overridden by
1139  the actual node class implementations; this base function always raises
1140  an exception."""
1141  raise TypeError('can\'t copy node of abstract type ' + type(self).__name__)
1142 
1143  def clone(self):
1144  """Returns a deep copy of this node. Note that this is overridden by
1145  the actual node class implementations; this base function always raises
1146  an exception."""
1147  raise TypeError('can\'t clone node of abstract type ' + type(self).__name__)
1148 
1149  @classmethod
1150  def deserialize(cls, cbor):
1151  """Attempts to deserialize the given cbor object (either as bytes or as
1152  its Python primitive representation) into a node of this type."""
1153  if isinstance(cbor, bytes):
1154  cbor = _cbor_to_py(cbor)
1155  seq_to_ob = {}
1156  links = []
1157  root = cls._deserialize(cbor, seq_to_ob, links)
1158  for link_setter, seq in links:
1159  ob = seq_to_ob.get(seq, None)
1160  if ob is None:
1161  raise ValueError('found link to nonexistent object')
1162  link_setter(ob)
1163  return root
1164 
1165  def serialize(self):
1166  """Serializes this node into its cbor representation in the form of a
1167  bytes object."""
1168  id_map = self.find_reachable()
1169  self.check_complete(id_map)
1170  return _py_to_cbor(self._serialize(id_map))
1171 
1172  @staticmethod
1173  def _deserialize(cbor, seq_to_ob, links):
1174  if not isinstance(cbor, dict):
1175  raise TypeError('node description object must be a dict')
1176  typ = cbor.get('@t', None)
1177  if typ is None:
1178  raise ValueError('type (@t) field is missing from node serialization')
1179  node_type = _typemap.get(cbor.get('@t'), None)
1180  if node_type is None:
1181  raise ValueError('unknown node type (@t): ' + str(cbor.get('@t')))
1182  return node_type._deserialize(cbor, seq_to_ob, links)
1183 
1184 )PY" << R"PY(
1185 @functools.total_ordering
1186 class _Multiple(object):
1187  """Base class for the Any* and Many* edge helper classes. Inheriting
1188  classes must set the class constant _T to the node type they are made
1189  for."""
1190 
1191  __slots__ = ['_l']
1192 
1193  def __init__(self, *args, **kwargs):
1194  super().__init__()
1195  self._l = list(*args, **kwargs)
1196  for idx, val in enumerate(self._l):
1197  if not isinstance(val, self._T):
1198  raise TypeError(
1199  'object {!r} at index {:d} is not an instance of {!r}'
1200  .format(val, idx, self._T))
1201 
1202  def __repr__(self):
1203  return '{}({!r})'.format(type(self).__name__, self._l)
1204 
1205  def clone(self):
1206  return self.__class__(map(lambda node: node.clone(), self._l))
1207 
1208  def __len__(self):
1209  return len(self._l)
1210 
1211  def __getitem__(self, idx):
1212  return self._l[idx]
1213 
1214  def __setitem__(self, idx, val):
1215  if not isinstance(val, self._T):
1216  raise TypeError(
1217  'object {!r} is not an instance of {!r}'
1218  .format(val, idx, self._T))
1219  self._l[idx] = val
1220 
1221  def __delitem__(self, idx):
1222  del self._l[idx]
1223 
1224  def __iter__(self):
1225  return iter(self._l)
1226 
1227  def __reversed__(self):
1228  return reversed(self._l)
1229 
1230  def __contains__(self, val):
1231  return val in self._l
1232 
1233  def append(self, val):
1234  if not isinstance(val, self._T):
1235  raise TypeError(
1236  'object {!r} is not an instance of {!r}'
1237  .format(val, self._T))
1238  self._l.append(val)
1239 
1240  def extend(self, iterable):
1241  for val in iterable:
1242  self.append(val)
1243 
1244  def insert(self, idx, val):
1245  if not isinstance(val, self._T):
1246  raise TypeError(
1247  'object {!r} is not an instance of {!r}'
1248  .format(val, self._T))
1249  self._l.insert(idx, val)
1250 
1251  def remote(self, val):
1252  self._l.remove(val)
1253 
1254  def pop(self, idx=-1):
1255  return self._l.pop(idx)
1256 
1257  def clear(self):
1258  self._l.clear()
1259 
1260  def idx(self, val, start=0, end=-1):
1261  return self._l.idx(val, start, end)
1262 
1263  def count(self, val):
1264  return self._l.count(val)
1265 
1266  def sort(self, key=None, reverse=False):
1267  self._l.sort(key=key, reverse=reverse)
1268 
1269  def reverse(self):
1270  self._l.reverse()
1271 
1272  def copy(self):
1273  return self.__class__(self)
1274 
1275  def __eq__(self, other):
1276  if not isinstance(other, _Multiple):
1277  return False
1278  return self._l == other._l
1279 
1280  def __lt__(self, other):
1281  return self._l < other._l
1282 
1283  def __iadd__(self, other):
1284  self.extend(other)
1285 
1286  def __add__(self, other):
1287  copy = self.copy()
1288  copy += other
1289  return copy
1290 
1291  def __imul__(self, other):
1292  self._l *= other
1293 
1294  def __mul__(self, other):
1295  copy = self.copy()
1296  copy *= other
1297  return copy
1298 
1299  def __rmul__(self, other):
1300  copy = self.copy()
1301  copy *= other
1302  return copy
1303 
1304 
1305 class MultiNode(_Multiple):
1306  """Wrapper for an edge with multiple Node objects."""
1307 
1308  _T = Node
1309 
1310 
1311 def _cloned(obj):
1312  """Attempts to clone the given object by calling its clone() method, if it
1313  has one."""
1314  if hasattr(obj, 'clone'):
1315  return obj.clone()
1316  return obj
1317 
1318 
1319 )PY";
1320 
1321  // Generate the node classes.
1322  std::unordered_set<std::string> generated;
1323  for (auto node : nodes) {
1324  if (generated.count(node->snake_case_name)) {
1325  continue;
1326  }
1327  auto ancestors = Nodes();
1328  while (node) {
1329  ancestors.push_back(node);
1330  node = node->parent;
1331  }
1332  for (auto node_it = ancestors.rbegin(); node_it != ancestors.rend(); node_it++) {
1333  node = *node_it;
1334  if (generated.count(node->snake_case_name)) {
1335  continue;
1336  }
1337  generated.insert(node->snake_case_name);
1338  generate_node_class(output, specification, *node);
1339  }
1340  }
1341 
1342 }
1343 
1344 } // namespace python
1345 } // namespace tree_gen
std::vector< Field > all_fields() const
Gathers all child nodes, including those in parent classes.
Definition: tree-gen.cpp:16
Exactly one node.
Definition: tree-gen.hpp:645
Header file for tree-gen-python.cpp.
std::vector< std::shared_ptr< Node > > Nodes
List of nodes.
Definition: tree-gen.hpp:785
Represents a type of AST node.
Definition: tree-gen.hpp:733
Struct containing everything needed for a complete specification.
Definition: tree-gen.hpp:850
Zero or one nodes.
Definition: tree-gen.hpp:640
std::vector< std::weak_ptr< Node > > derived
Node types derived from this one.
Definition: tree-gen.hpp:758
Namespace for the tree-gen program.
std::string title_case_name
Name in TitleCase.
Definition: tree-gen.hpp:743
Link to zero or one nodes elsewhere in the tree.
Definition: tree-gen.hpp:660
EdgeType
Types of edges between nodes and primitives.
Definition: tree-gen.hpp:635
Primitive type.
Definition: tree-gen.hpp:670
void generate(const std::string &python_filename, Specification &specification)
Generates the complete Python code.
std::string doc
Class documentation.
Definition: tree-gen.hpp:748
void generate_deserialize_mux(std::ofstream &output, Node &node)
Recursive function to print a muxing if statement for all node classes derived from the given node cl...
One or more nodes.
Definition: tree-gen.hpp:655
std::vector< std::string > python_includes
The include statements to stick at the top of the Python file.
Definition: tree-gen.hpp:894
std::vector< Field > fields
Child nodes.
Definition: tree-gen.hpp:763
std::string python_doc
Python file documentation.
Definition: tree-gen.hpp:879
std::string py_deserialize_fn
Deserialization function to use when serializing primitives in the Python domain, using ...
Definition: tree-gen.hpp:942
Zero or more nodes.
Definition: tree-gen.hpp:650
std::shared_ptr< Node > parent
The node type this is derived from, if any.
Definition: tree-gen.hpp:753
Nodes nodes
All the nodes.
Definition: tree-gen.hpp:954
void format_doc(std::ofstream &stream, const std::string &doc, const std::string &indent="")
Formats a Python docstring.
std::string py_serialize_fn
Serialization function to use when serializing primitives in the Python domain, using ...
Definition: tree-gen.hpp:931
void generate_node_class(std::ofstream &output, Specification &spec, Node &node)
Generates the class for the given node.
Link to exactly one node elsewhere in the tree.
Definition: tree-gen.hpp:665