from prettytoml.errors import NoArrayFoundError, DuplicateKeysError, DuplicateTablesError from contoml.file import structurer, toplevels, raw from contoml.file.array import ArrayOfTables from contoml.file.freshtable import FreshTable import prettytoml.elements.factory as element_factory import prettytoml.util as util class TOMLFile: """ A TOMLFile object that tries its best to prserve formatting and order of mappings of the input source. Raises InvalidTOMLFileError on invalid input elements. Raises DuplicateKeysError, DuplicateTableError when appropriate. """ def __init__(self, _elements): self._elements = [] self._navigable = {} self.append_elements(_elements) def __getitem__(self, item): try: value = self._navigable[item] if isinstance(value, (list, tuple)): return ArrayOfTables(toml_file=self, name=item, iterable=value) else: return value except KeyError: return FreshTable(parent=self, name=item, is_array=False) def get(self, item, default=None): """This was not here for who knows why.""" if item not in self: return default else: return self.__getitem__(item) def __contains__(self, item): return item in self.keys() def _setitem_with_key_seq(self, key_seq, value): """ Sets a the value in the TOML file located by the given key sequence. Example: self._setitem(('key1', 'key2', 'key3'), 'text_value') is equivalent to doing self['key1']['key2']['key3'] = 'text_value' """ table = self key_so_far = tuple() for key in key_seq[:-1]: key_so_far += (key,) self._make_sure_table_exists(key_so_far) table = table[key] table[key_seq[-1]] = value def _array_setitem_with_key_seq(self, array_name, index, key_seq, value): """ Sets a the array value in the TOML file located by the given key sequence. Example: self._array_setitem(array_name, index, ('key1', 'key2', 'key3'), 'text_value') is equivalent to doing self.array(array_name)[index]['key1']['key2']['key3'] = 'text_value' """ table = self.array(array_name)[index] key_so_far = tuple() for key in key_seq[:-1]: key_so_far += (key,) new_table = self._array_make_sure_table_exists(array_name, index, key_so_far) if new_table is not None: table = new_table else: table = table[key] table[key_seq[-1]] = value def _make_sure_table_exists(self, name_seq): """ Makes sure the table with the full name comprising of name_seq exists. """ t = self for key in name_seq[:-1]: t = t[key] name = name_seq[-1] if name not in t: self.append_elements([element_factory.create_table_header_element(name_seq), element_factory.create_table({})]) def _array_make_sure_table_exists(self, array_name, index, name_seq): """ Makes sure the table with the full name comprising of name_seq exists. """ t = self[array_name][index] for key in name_seq[:-1]: t = t[key] name = name_seq[-1] if name not in t: new_table = element_factory.create_table({}) self.append_elements([element_factory.create_table_header_element((array_name,) + name_seq), new_table]) return new_table def __delitem__(self, key): table_element_index = self._elements.index(self._navigable[key]) self._elements[table_element_index] = element_factory.create_table({}) self._on_element_change() def __setitem__(self, key, value): # Setting an array-of-tables if key and isinstance(value, (tuple, list)) and value and all(isinstance(v, dict) for v in value): for table in value: self.array(key).append(table) # Or setting a whole single table elif isinstance(value, dict): if key and key in self: del self[key] for key_seq, child_value in util.flatten_nested({key: value}).items(): self._setitem_with_key_seq(key_seq, child_value) # if key in self._navigable: # del self[key] # index = self._elements.index(self._navigable[key]) # self._elements = self._elements[:index] + [element_factory.create_table(value)] + self._elements[index+1:] # else: # if key: # self._elements.append(element_factory.create_table_header_element(key)) # self._elements.append(element_factory.create_table(value)) # Or updating the anonymous section table else: # It's mea self[''][key] = value self._on_element_change() def _detect_toplevels(self): """ Returns a sequence of TopLevel instances for the current state of this table. """ return tuple(e for e in toplevels.identify(self.elements) if isinstance(e, toplevels.Table)) def _update_table_fallbacks(self, table_toplevels): """ Updates the fallbacks on all the table elements to make relative table access possible. Raises DuplicateKeysError if appropriate. """ if len(self.elements) <= 1: return def parent_of(toplevel): # Returns an TopLevel parent of the given entry, or None. for parent_toplevel in table_toplevels: if toplevel.name.sub_names[:-1] == parent_toplevel.name.sub_names: return parent_toplevel for entry in table_toplevels: if entry.name.is_qualified: parent = parent_of(entry) if parent: child_name = entry.name.without_prefix(parent.name) parent.table_element.set_fallback({child_name.sub_names[0]: entry.table_element}) def _recreate_navigable(self): if self._elements: self._navigable = structurer.structure(toplevels.identify(self._elements)) def array(self, name): """ Returns the array of tables with the given name. """ if name in self._navigable: if isinstance(self._navigable[name], (list, tuple)): return self[name] else: raise NoArrayFoundError else: return ArrayOfTables(toml_file=self, name=name) def _on_element_change(self): self._recreate_navigable() table_toplevels = self._detect_toplevels() self._update_table_fallbacks(table_toplevels) def append_elements(self, elements): """ Appends more elements to the contained internal elements. """ self._elements = self._elements + list(elements) self._on_element_change() def prepend_elements(self, elements): """ Prepends more elements to the contained internal elements. """ self._elements = list(elements) + self._elements self._on_element_change() def dumps(self): """ Returns the TOML file serialized back to str. """ return ''.join(element.serialized() for element in self._elements) def dump(self, file_path): with open(file_path, mode='w') as fp: fp.write(self.dumps()) def keys(self): return set(self._navigable.keys()) | {''} def values(self): return self._navigable.values() def items(self): items = self._navigable.items() def has_anonymous_entry(): return any(key == '' for (key, _) in items) if has_anonymous_entry(): return items else: return list(items) + [('', self[''])] @property def primitive(self): """ Returns a primitive object representation for this container (which is a dict). WARNING: The returned container does not contain any markup or formatting metadata. """ raw_container = raw.to_raw(self._navigable) # Collapsing the anonymous table onto the top-level container is present if '' in raw_container: raw_container.update(raw_container['']) del raw_container[''] return raw_container def append_fresh_table(self, fresh_table): """ Gets called by FreshTable instances when they get written to. """ if fresh_table.name: elements = [] if fresh_table.is_array: elements += [element_factory.create_array_of_tables_header_element(fresh_table.name)] else: elements += [element_factory.create_table_header_element(fresh_table.name)] elements += [fresh_table, element_factory.create_newline_element()] self.append_elements(elements) else: # It's an anonymous table self.prepend_elements([fresh_table, element_factory.create_newline_element()]) @property def elements(self): return self._elements def __str__(self): is_empty = (not self['']) and (not tuple(k for k in self.keys() if k)) def key_name(key): return '[ANONYMOUS]' if not key else key def pair(key, value): return '%s = %s' % (key_name(key), str(value)) content_text = '' if is_empty else \ '\n\t' + ',\n\t'.join(pair(k, v) for (k, v) in self.items() if v) + '\n' return "TOMLFile{%s}" % content_text def __repr__(self): return str(self)