commit:     1769905581e8f4ea6f5486314c912de3f4385d39
Author:     Brian Dolbec <dolsen <AT> gentoo <DOT> org>
AuthorDate: Sat Jul 15 00:06:27 2017 +0000
Commit:     Brian Dolbec <dolsen <AT> gentoo <DOT> org>
CommitDate: Sat Jul 15 02:08:27 2017 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=17699055

repoman: Add a new config.py file with config loading utilities

These include recursively merging of multiple yaml files.
They are needed for masters stacking.

 repoman/pym/repoman/config.py | 143 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 143 insertions(+)

diff --git a/repoman/pym/repoman/config.py b/repoman/pym/repoman/config.py
new file mode 100644
index 000000000..2825aee04
--- /dev/null
+++ b/repoman/pym/repoman/config.py
@@ -0,0 +1,143 @@
+# Copyright 2015-2016 Gaikai Inc, a Sony Computer Entertainment company
+
+import copy
+import itertools
+import json
+import os
+import stat
+
+import yaml
+
+
+class ConfigError(Exception):
+
+    """Raised when a config file fails to load"""
+    pass
+
+
+def merge_config(base, head):
+    """
+    Merge two JSON or YAML documents into a single object. Arrays are
+    merged by extension. If dissimilar types are encountered, then the
+    head value overwrites the base value.
+    """
+
+    if isinstance(head, dict):
+        if not isinstance(base, dict):
+            return copy.deepcopy(head)
+
+        result = {}
+        for k in itertools.chain(head, base):
+            try:
+                result[k] = merge_config(base[k], head[k])
+            except KeyError:
+                try:
+                    result[k] = copy.deepcopy(head[k])
+                except KeyError:
+                    result[k] = copy.deepcopy(base[k])
+
+    elif isinstance(head, list):
+        result = []
+        if not isinstance(base, list):
+            result.extend(copy.deepcopy(x) for x in head)
+        else:
+            if any(isinstance(x, (dict, list)) for x in itertools.chain(head, 
base)):
+                # merge items with identical indexes
+                for x, y in zip(base, head):
+                    if isinstance(x, (dict, list)):
+                        result.append(merge_config(x, y))
+                    else:
+                        # head overwrites base (preserving index)
+                        result.append(copy.deepcopy(y))
+                # copy remaining items from the longer list
+                if len(base) != len(head):
+                    if len(base) > len(head):
+                        result.extend(copy.deepcopy(x) for x in 
base[len(head):])
+                    else:
+                        result.extend(copy.deepcopy(x) for x in 
head[len(base):])
+            else:
+                result.extend(copy.deepcopy(x) for x in base)
+                result.extend(copy.deepcopy(x) for x in head)
+
+    else:
+        result = copy.deepcopy(head)
+
+    return result
+
+def _yaml_load(filename):
+    """
+    Load filename as YAML and return a dict. Raise ConfigError if
+    it fails to load.
+    """
+    with open(filename, 'rt') as f:
+        try:
+            return yaml.safe_load(f)
+        except yaml.parser.ParserError as e:
+            raise ConfigError("{}: {}".format(filename, e))
+
+def _json_load(filename):
+    """
+    Load filename as JSON and return a dict. Raise ConfigError if
+    it fails to load.
+    """
+    with open(filename, 'rt') as f:
+        try:
+            return json.load(f) #nosec
+        except ValueError as e:
+            raise ConfigError("{}: {}".format(filename, e))
+
+def iter_files(files_dirs):
+    """
+    Iterate over nested file paths in lexical order.
+    """
+    stack = list(reversed(files_dirs))
+    while stack:
+        location = stack.pop()
+        try:
+            st = os.stat(location)
+        except FileNotFoundError:
+            continue
+
+        if stat.S_ISDIR(st.st_mode):
+            stack.extend(os.path.join(location, x)
+                for x in sorted(os.listdir(location), reverse=True))
+
+        elif stat.S_ISREG(st.st_mode):
+            yield location
+
+def load_config(conf_dirs, file_extensions=None):
+    """
+    Load JSON and/or YAML files from a directories, and merge them together
+    into a single object.
+    """
+
+    result = {}
+    for filename in iter_files(conf_dirs):
+        if file_extensions is not None and not 
filename.endswith(file_extensions):
+            continue
+
+        loaders = []
+        if filename.endswith('.json'):
+            loaders.append(_json_load)
+        elif filename.endswith('.yaml'):
+            loaders.append(_yaml_load)
+        else:
+            loaders.append(_yaml_load)
+            loaders.append(_json_load)
+
+        config = None
+        for loader in loaders:
+            try:
+                config = loader(filename) or {}
+            except ConfigError as e:
+                exception = e
+            else:
+                break
+
+        if config is None:
+            raise exception
+
+        if config:
+            result = merge_config(result, config)
+
+    return result

Reply via email to