diff options
| author | Benjamin Schubert <contact@benschubert.me> | 2019-05-03 21:42:13 +0100 |
|---|---|---|
| committer | Benjamin Schubert <ben.c.schubert@gmail.com> | 2019-05-04 09:47:19 +0100 |
| commit | 4c2429145bc1f1929c1f546e04d2cabd14365381 (patch) | |
| tree | 35012bfb0ccc87b453b25e907fde87b302877bee | |
| parent | fbb32cfb91c4a6af52390e87dd16c56934837282 (diff) | |
| download | buildstream-4c2429145bc1f1929c1f546e04d2cabd14365381.tar.gz | |
WIP: _yaml: reimplement using cython for performance
| -rw-r--r-- | buildstream/_yaml.pyx (renamed from buildstream/_yaml.py) | 311 | ||||
| -rwxr-xr-x | setup.py | 3 |
2 files changed, 170 insertions, 144 deletions
diff --git a/buildstream/_yaml.py b/buildstream/_yaml.pyx index cdab4269e..38947d595 100644 --- a/buildstream/_yaml.py +++ b/buildstream/_yaml.pyx @@ -61,16 +61,32 @@ from ._exceptions import LoadError, LoadErrorReason # line = Node[2] # column = Node[3] # -class Node(namedtuple('Node', ['value', 'file_index', 'line', 'column'])): +cdef class Node: + cdef public object value + # FIXME: we want a int here + cdef public object file_index + cdef public int line + cdef public int column + + def __cinit__(self, object value, object file_index, int line, int column): + self.value = value + self.file_index = file_index + self.line = line + self.column = column + def __contains__(self, what): - # Delegate to the inner value, though this will likely not work - # very well if the node is a list or string, it's unlikely that - # code which has access to such nodes would do this. - return what in self[0] + return what in self.value + +# class Node(namedtuple('Node', ['value', 'file_index', 'line', 'column'])): +# def __contains__(self, what): +# # Delegate to the inner value, though this will likely not work +# # very well if the node is a list or string, it's unlikely that +# # code which has access to such nodes would do this. +# return what in self[0] # File name handling -_FILE_LIST = [] +cdef list _FILE_LIST = [] # Purely synthetic node will have None for the file number, have line number @@ -98,7 +114,7 @@ class ProvenanceInformation: def __init__(self, nodeish): self.node = nodeish - if (nodeish is None) or (nodeish[1] is None): + if (nodeish is None) or (nodeish.file_index is None): self.filename = "" self.shortname = "" self.displayname = "" @@ -107,13 +123,13 @@ class ProvenanceInformation: self.toplevel = None self.project = None else: - fileinfo = _FILE_LIST[nodeish[1]] + fileinfo = _FILE_LIST[nodeish.file_index] self.filename = fileinfo[0] self.shortname = fileinfo[1] self.displayname = fileinfo[2] # We add 1 here to convert from computerish to humanish - self.line = nodeish[2] + 1 - self.col = nodeish[3] + self.line = nodeish.line + 1 + self.col = nodeish.column self.toplevel = fileinfo[3] self.project = fileinfo[4] self.is_synthetic = (self.filename == '') or (self.col < 0) @@ -148,13 +164,11 @@ class YAMLLoadError(Exception): # Mappings must only have string keys, values are always represented as # strings if they are scalar, or else as simple dictionaries and lists. # -class Representer: - __slots__ = ( - "_file_index", - "state", - "output", - "keys", - ) +cdef class Representer: + cdef object _file_index # FIXME: this should be an int + cdef str state + cdef list output + cdef list keys # Initialise a new representer # @@ -163,7 +177,7 @@ class Representer: # # Args: # file_index (int): The index of this YAML file - def __init__(self, file_index): + def __cinit__(self, object file_index): self._file_index = file_index self.state = "init" self.output = [] @@ -226,14 +240,14 @@ class Representer: def _handle_wait_value_ScalarEvent(self, ev): key = self.keys.pop() - self.output[-1][0][key] = \ + self.output[-1].value[key] = \ Node(ev.value, self._file_index, ev.start_mark.line, ev.start_mark.column) return "wait_key" def _handle_wait_value_MappingStartEvent(self, ev): new_state = self._handle_doc_MappingStartEvent(ev) key = self.keys.pop() - self.output[-2][0][key] = self.output[-1] + self.output[-2].value[key] = self.output[-1] return new_state def _handle_wait_key_MappingEndEvent(self, ev): @@ -241,7 +255,7 @@ class Representer: # unless it's the last one in which case we leave it if len(self.output) > 1: self.output.pop() - if type(self.output[-1][0]) is list: + if type(self.output[-1].value) is list: return "wait_list_item" else: return "wait_key" @@ -250,13 +264,13 @@ class Representer: def _handle_wait_value_SequenceStartEvent(self, ev): self.output.append(Node([], self._file_index, ev.start_mark.line, ev.start_mark.column)) - self.output[-2][0][self.keys[-1]] = self.output[-1] + self.output[-2].value[self.keys[-1]] = self.output[-1] return "wait_list_item" def _handle_wait_list_item_SequenceStartEvent(self, ev): - self.keys.append(len(self.output[-1][0])) + self.keys.append(len(self.output[-1].value)) self.output.append(Node([], self._file_index, ev.start_mark.line, ev.start_mark.column)) - self.output[-2][0].append(self.output[-1]) + self.output[-2].value.append(self.output[-1]) return "wait_list_item" def _handle_wait_list_item_SequenceEndEvent(self, ev): @@ -271,13 +285,13 @@ class Representer: return "wait_key" def _handle_wait_list_item_ScalarEvent(self, ev): - self.output[-1][0].append( + self.output[-1].value.append( Node(ev.value, self._file_index, ev.start_mark.line, ev.start_mark.column)) return "wait_list_item" def _handle_wait_list_item_MappingStartEvent(self, ev): new_state = self._handle_doc_MappingStartEvent(ev) - self.output[-2][0].append(self.output[-1]) + self.output[-2].value.append(self.output[-1]) return new_state def _handle_doc_DocumentEndEvent(self, ev): @@ -332,6 +346,9 @@ def load(filename, shortname=None, copy_tree=False, *, project=None): "{} is a directory. bst command expects a .bst file." .format(filename)) from e except LoadError as e: + print("ERROR HERE", e) + import traceback + traceback.print_exc() raise LoadError(e.reason, "{}: {}".format(displayname, e)) from e @@ -351,14 +368,15 @@ def load_data(data, file_index=None, file_name=None, copy_tree=False): raise LoadError(LoadErrorReason.INVALID_YAML, "Severely malformed YAML:\n\n{}\n\n".format(e)) from e - if not isinstance(contents, tuple) or not isinstance(contents[0], dict): + # FIXME: we can probably remove the instance check + if not isinstance(contents, Node) or not isinstance(contents.value, dict): # Special case allowance for None, when the loaded file has only comments in it. if contents is None: contents = Node({}, file_index, 0, 0) else: raise LoadError(LoadErrorReason.INVALID_YAML, "YAML file has content of type '{}' instead of expected type 'dict': {}" - .format(type(contents[0]).__name__, file_name)) + .format(type(contents.value).__name__, file_name)) # Store this away because we'll use it later for "top level" provenance if file_index is not None: @@ -409,11 +427,11 @@ def node_get_provenance(node, key=None, indices=None): return ProvenanceInformation(node) if key and not indices: - return ProvenanceInformation(node[0].get(key)) + return ProvenanceInformation(node.value.get(key)) - nodeish = node[0].get(key) + nodeish = node.value.get(key) for idx in indices: - nodeish = nodeish[0][idx] + nodeish = nodeish.value[idx] return ProvenanceInformation(nodeish) @@ -451,11 +469,11 @@ def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, if indices is None: if default_value is _sentinel: - value = node[0].get(key, Node(default_value, None, 0, 0)) + value = node.value.get(key, Node(default_value, None, 0, 0)) else: - value = node[0].get(key, Node(default_value, None, 0, next(_SYNTHETIC_COUNTER))) + value = node.value.get(key, Node(default_value, None, 0, next(_SYNTHETIC_COUNTER))) - if value[0] is _sentinel: + if value.value is _sentinel: provenance = node_get_provenance(node) raise LoadError(LoadErrorReason.INVALID_DATA, "{}: Dictionary did not contain expected key '{}'".format(provenance, key)) @@ -464,35 +482,36 @@ def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, # No need to synthesise useful node content as we destructure it immediately value = Node(node_get(node, list, key), None, 0, 0) for index in indices: - value = value[0][index] + value = value.value[index] if type(value) is not Node: - value = (value,) + value = Node(value, None, 0, 0) # Optionally allow None as a valid value for any type - if value[0] is None and (allow_none or default_value is None): + if value.value is None and (allow_none or default_value is None): return None - if (expected_type is not None) and (not isinstance(value[0], expected_type)): + if (expected_type is not None) and (not isinstance(value.value, expected_type)): # Attempt basic conversions if possible, typically we want to # be able to specify numeric values and convert them to strings, # but we dont want to try converting dicts/lists try: - if (expected_type == bool and isinstance(value[0], str)): + if (expected_type == bool and isinstance(value.value, str)): # Dont coerce booleans to string, this makes "False" strings evaluate to True # We don't structure into full nodes since there's no need. - if value[0] in ('True', 'true'): - value = (True, None, 0, 0) - elif value[0] in ('False', 'false'): - value = (False, None, 0, 0) + if value.value in ('True', 'true'): + value = Node(True, None, 0, 0) + elif value.value in ('False', 'false'): + value = Node(False, None, 0, 0) else: raise ValueError() elif not (expected_type == list or expected_type == dict or - isinstance(value[0], (list, dict))): - value = (expected_type(value[0]), None, 0, 0) + isinstance(value.value, (list, dict))): + value = Node(expected_type(value.value), None, 0, 0) else: raise ValueError() - except (ValueError, TypeError): + except (ValueError, TypeError) as e: + print(e) provenance = node_get_provenance(node, key=key, indices=indices) if indices: path = [key] @@ -505,8 +524,8 @@ def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, .format(provenance, path, expected_type.__name__)) # Now collapse lists, and scalars, to their value, leaving nodes as-is - if type(value[0]) is not dict: - value = value[0] + if type(value.value) is not dict: + value = value.value # Trim it at the bud, let all loaded strings from yaml be stripped of whitespace if type(value) is str: @@ -520,17 +539,17 @@ def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, return value -def __trim_list_provenance(value): +cdef list __trim_list_provenance(list value): ret = [] for entry in value: if type(entry) is not Node: - entry = (entry, None, 0, 0) - if type(entry[0]) is list: - ret.append(__trim_list_provenance(entry[0])) - elif type(entry[0]) is dict: + entry = Node(entry, None, 0, 0) + if type(entry.value) is list: + ret.append(__trim_list_provenance(entry.value)) + elif type(entry.value) is dict: ret.append(entry) else: - ret.append(entry[0]) + ret.append(entry.value) return ret @@ -541,30 +560,30 @@ def __trim_list_provenance(value): # create entries before using `node_set` # # Args: -# node (tuple): The node +# node (Node): The node # key (str): The key name # value: The value # indices: Any indices to index into the list referenced by key, like in # `node_get` (must be a list of integers) # -def node_set(node, key, value, indices=None): +def node_set(Node node, str key, value, list indices=None): if indices: - node = node[0][key] + node = node.value[key] key = indices.pop() for idx in indices: node = node[0][idx] if type(value) is Node: - node[0][key] = value + node.value[key] = value else: try: # Need to do this just in case we're modifying a list - old_value = node[0][key] + old_value = node.value[key] except KeyError: old_value = None if old_value is None: - node[0][key] = Node(value, node[1], node[2], next(_SYNTHETIC_COUNTER)) + node.value[key] = Node(value, node.file_index, node.line, next(_SYNTHETIC_COUNTER)) else: - node[0][key] = Node(value, old_value[1], old_value[2], old_value[3]) + node.value[key] = Node(value, old_value.file_index, old_value.line, old_value.column) # node_extend_list() @@ -585,20 +604,20 @@ def node_set(node, key, value, indices=None): def node_extend_list(node, key, length, default): assert type(default) is str or default in ([], {}) - list_node = node[0].get(key) + list_node = node.value.get(key) if list_node is None: list_node = node[0][key] = Node([], node[1], node[2], next(_SYNTHETIC_COUNTER)) - assert type(list_node[0]) is list + assert type(list_node.value) is list - the_list = list_node[0] + the_list = list_node.value def_type = type(default) - file_index = node[1] + file_index = node.file_index if the_list: line_num = the_list[-1][2] else: - line_num = list_node[2] + line_num = list_node.line while length > len(the_list): if def_type is str: @@ -628,15 +647,15 @@ def node_extend_list(node, key, length, default): def node_items(node): if type(node) is not Node: node = Node(node, None, 0, 0) - for key, value in node[0].items(): + for key, value in node.value.items(): if type(value) is not Node: value = Node(value, None, 0, 0) - if type(value[0]) is dict: + if type(value.value) is dict: yield (key, value) - elif type(value[0]) is list: - yield (key, __trim_list_provenance(value[0])) + elif type(value.value) is list: + yield (key, __trim_list_provenance(value.value)) else: - yield (key, value[0]) + yield (key, value.value) # node_keys() @@ -653,7 +672,7 @@ def node_items(node): def node_keys(node): if type(node) is not Node: node = Node(node, None, 0, 0) - yield from node[0].keys() + yield from node.value.keys() # node_del() @@ -668,7 +687,7 @@ def node_keys(node): # def node_del(node, key, safe=False): try: - del node[0][key] + del node.value[key] except KeyError: if not safe: raise @@ -689,7 +708,7 @@ def node_del(node, key, safe=False): def is_node(maybenode): # It's a programming error to give this a Node which isn't a mapping # so assert that. - assert (type(maybenode) is not Node) or (type(maybenode[0]) is dict) + assert (type(maybenode) is not Node) or (type(maybenode.value) is dict) # Now return the type check return type(maybenode) is Node @@ -729,7 +748,7 @@ def new_synthetic_file(filename, project=None): # def new_empty_node(ref_node=None): if ref_node is not None: - return Node({}, ref_node[1], ref_node[2], next(_SYNTHETIC_COUNTER)) + return Node({}, ref_node.file_index, ref_node.line, next(_SYNTHETIC_COUNTER)) else: return Node({}, None, 0, 0) @@ -785,9 +804,9 @@ def __new_node_from_list(inlist): # (LoadError): If node was a mapping and contained a mix of # list composition directives and other keys # -def _is_composite_list(node): +cdef bint _is_composite_list(Node node): - if type(node[0]) is dict: + if type(node.value) is dict: has_directives = False has_keys = False @@ -857,18 +876,18 @@ def _compose_composite_list(target, source): # target (Node): The target list to be composed into # source (Node): The composition list to be composed from # -def _compose_list(target, source): - clobber = source[0].get("(=)") - prefix = source[0].get("(<)") - suffix = source[0].get("(>)") +def _compose_list(Node target, Node source): # FIXME: make private + clobber = source.value.get("(=)") + prefix = source.value.get("(<)") + suffix = source.value.get("(>)") if clobber is not None: - target[0].clear() - target[0].extend(clobber[0]) + target.value.clear() + target.value.extend(clobber.value) if prefix is not None: - for v in reversed(prefix[0]): - target[0].insert(0, v) + for v in reversed(prefix.value): + target.value.insert(0, v) if suffix is not None: - target[0].extend(suffix[0]) + target.value.extend(suffix.value) # composite_dict() @@ -882,16 +901,17 @@ def _compose_list(target, source): # # Raises: CompositeError # -def composite_dict(target, source, path=None): +def composite_dict(Node target, Node source, list path=None): if path is None: path = [] - for k, v in source[0].items(): + # FIXME: type k and v + for k, v in source.value.items(): path.append(k) - if type(v[0]) is list: + if type(v.value) is list: # List clobbers anything list-like - target_value = target[0].get(k) + target_value = target.value.get(k) if not (target_value is None or - type(target_value[0]) is list or + type(target_value.value) is list or _is_composite_list(target_value)): raise CompositeError(path, "{}: List cannot overwrite {} at: {}" @@ -899,51 +919,51 @@ def composite_dict(target, source, path=None): k, node_get_provenance(target, k))) # Looks good, clobber it - target[0][k] = v + target.value[k] = v elif _is_composite_list(v): - if k not in target[0]: + if k not in target.value: # Composite list clobbers empty space - target[0][k] = v - elif type(target[0][k][0]) is list: + target.value[k] = v + elif type(target.value[k].value) is list: # Composite list composes into a list - _compose_list(target[0][k], v) - elif _is_composite_list(target[0][k]): + _compose_list(target.value[k], v) + elif _is_composite_list(target.value[k]): # Composite list merges into composite list - _compose_composite_list(target[0][k], v) + _compose_composite_list(target.value[k], v) else: # Else composing on top of normal dict or a scalar, so raise... raise CompositeError(path, "{}: Cannot compose lists onto {}".format( node_get_provenance(v), - node_get_provenance(target[0][k]))) - elif type(v[0]) is dict: + node_get_provenance(target.value[k]))) + elif type(v.value) is dict: # We're composing a dict into target now - if k not in target[0]: + if k not in target.value: # Target lacks a dict at that point, make a fresh one with # the same provenance as the incoming dict - target[0][k] = Node({}, v[1], v[2], v[3]) - if type(target[0]) is not dict: + target.value[k] = Node({}, v.file_index, v.line, v.column) + if type(target.value) is not dict: raise CompositeError(path, "{}: Cannot compose dictionary onto {}".format( node_get_provenance(v), - node_get_provenance(target[0][k]))) - composite_dict(target[0][k], v, path) + node_get_provenance(target.value[k]))) + composite_dict(target.value[k], v, path) else: - target_value = target[0].get(k) - if target_value is not None and type(target_value[0]) is not str: + target_value = target.value.get(k) + if target_value is not None and type(target_value.value) is not str: raise CompositeError(path, "{}: Cannot compose scalar on non-scalar at {}".format( node_get_provenance(v), - node_get_provenance(target[0][k]))) - target[0][k] = v + node_get_provenance(target.value[k]))) + target.value[k] = v path.pop() # Like composite_dict(), but raises an all purpose LoadError for convenience # def composite(target, source): - assert type(source[0]) is dict - assert type(target[0]) is dict + assert type(source.value) is dict + assert type(target.value) is dict try: composite_dict(target, source) @@ -961,12 +981,13 @@ def composite(target, source): # Like composite(target, source), but where target overrides source instead. # +# FIXME: private? def composite_and_move(target, source): composite(source, target) - to_delete = [key for key in target[0].keys() if key not in source[0]] - for key, value in source[0].items(): - target[0][key] = value + to_delete = [key for key in target.value.keys() if key not in source.value] + for key, value in source.value.items(): + target.value[key] = value for key in to_delete: del target[0][key] @@ -987,7 +1008,7 @@ def node_sanitize(node, *, dict_type=OrderedDict): # If we have an unwrappable node, unwrap it if node_type is Node: - node = node[0] + node = node.value node_type = type(node) # Short-circuit None which occurs ca. twice per element @@ -1039,7 +1060,7 @@ def node_validate(node, valid_keys): # Probably the fastest way to do this: https://stackoverflow.com/a/23062482 valid_keys = set(valid_keys) - invalid = next((key for key in node[0] if key not in valid_keys), None) + invalid = next((key for key in node.value if key not in valid_keys), None) if invalid: provenance = node_get_provenance(node, key=invalid) @@ -1075,10 +1096,10 @@ __NODE_ASSERT_COMPOSITION_DIRECTIVES = ('(>)', '(<)', '(=)') # Returns: # (Node): A deep copy of source with provenance preserved. # -def node_copy(source): +def node_copy(Node source): copy = {} - for key, value in source[0].items(): - value_type = type(value[0]) + for key, value in source.value.items(): + value_type = type(value.value) if value_type is dict: copy[key] = node_copy(value) elif value_type is list: @@ -1088,14 +1109,18 @@ def node_copy(source): else: raise ValueError("Unable to be quick about node_copy of {}".format(value_type)) - return Node(copy, source[1], source[2], source[3]) + return Node(copy, source.file_index, source.line, source.column) # Internal function to help node_copy() but for lists. -def _list_copy(source): +cdef Node _list_copy(Node source): copy = [] - for item in source[0]: - item_type = type(item[0]) + for item in source.value: + try: + item_type = type(item.value) + except AttributeError: # FIXME: this seems... wrong + item_type = type(item[0]) + if item_type is dict: copy.append(node_copy(item)) elif item_type is list: @@ -1105,7 +1130,7 @@ def _list_copy(source): else: raise ValueError("Unable to be quick about list_copy of {}".format(item_type)) - return Node(copy, source[1], source[2], source[3]) + return Node(copy, source.file_index, source.line, source.column) # node_final_assertions() @@ -1119,10 +1144,10 @@ def _list_copy(source): # Raises: # (LoadError): If any assertions fail # -def node_final_assertions(node): - assert type(node) is Node +def node_final_assertions(Node node): + # assert type(node) is Node - for key, value in node[0].items(): + for key, value in node.value.items(): # Assert that list composition directives dont remain, this # indicates that the user intended to override a list which @@ -1133,7 +1158,7 @@ def node_final_assertions(node): raise LoadError(LoadErrorReason.TRAILING_LIST_DIRECTIVE, "{}: Attempt to override non-existing list".format(provenance)) - value_type = type(value[0]) + value_type = type(value.value) if value_type is dict: node_final_assertions(value) @@ -1142,9 +1167,9 @@ def node_final_assertions(node): # Helper function for node_final_assertions(), but for lists. -def _list_final_assertions(values): - for value in values[0]: - value_type = type(value[0]) +def _list_final_assertions(Node values): + for value in values.value: + value_type = type(value.value) if value_type is dict: node_final_assertions(value) @@ -1214,11 +1239,11 @@ def assert_symbol_name(provenance, symbol_name, purpose, *, allow_dashes=True): # # Returns: # (list): A path from `node` to `target` or None if `target` is not in the subtree -def node_find_target(node, target, *, key=None): - assert type(node) is Node - assert type(target) is Node +def node_find_target(Node node, Node target, *, str key=None): + assert type(node) is Node # FIXME: remove this assert + assert type(target) is Node # FIXME: remove this assert if key is not None: - target = target[0][key] + target = target.value[key] path = [] if _walk_find_target(node, path, target): @@ -1230,19 +1255,19 @@ def node_find_target(node, target, *, key=None): # Helper for node_find_target() which walks a value -def _walk_find_target(node, path, target): - if node[1:] == target[1:]: +def _walk_find_target(Node node, list path, Node target): + if node.file_index == target.file_index and node.line == target.line and node.column == target.column: return True - elif type(node[0]) is dict: + elif type(node.value) is dict: return _walk_dict_node(node, path, target) - elif type(node[0]) is list: + elif type(node.value) is list: return _walk_list_node(node, path, target) return False # Helper for node_find_target() which walks a list -def _walk_list_node(node, path, target): - for i, v in enumerate(node[0]): +def _walk_list_node(Node node, list path, Node target): + for i, v in enumerate(node.value): path.append(i) if _walk_find_target(v, path, target): return True @@ -1251,8 +1276,8 @@ def _walk_list_node(node, path, target): # Helper for node_find_target() which walks a mapping -def _walk_dict_node(node, path, target): - for k, v in node[0].items(): +def _walk_dict_node(Node node, list path, Node target): + for k, v in node.value.items(): path.append(k) if _walk_find_target(v, path, target): return True @@ -367,7 +367,8 @@ setup(name='BuildStream', ], ext_modules=cythonize( [ - "buildstream/_variables.pyx" + "buildstream/_variables.pyx", + "buildstream/_yaml.pyx", ], annotate=True, language_level="3", |
