# The software in this package is distributed under the GNU General # Public License version 2 (with a special exception described below). # # A copy of GNU General Public License (GPL) is included in this distribution, # in the file COPYING.GPL. # # As a special exception, if other files instantiate templates or use macros # or inline functions from this file, or you compile this file and link it # with other works to produce a work based on this file, this file # does not by itself cause the resulting work to be covered # by the GNU General Public License. # # However the source code for this file must still be made available # in accordance with section (3) of the GNU General Public License. # # This exception does not invalidate any other reasons why a work based # on this file might be covered by the GNU General Public License. # # Copyright (c) 2016-2022 Intra2net AG """ model: Cnf classes, collection of Cnf classes and multiple filtering methods. Featuring: - Cnf: class representing a CNF variable - CnfList: a collection of `Cnf` instances The classes above inherit from their base types with added mixins which extend them with extra functionality. .. seealso:: Overview Diagram linked to from doc main page .. codeauthor:: Intra2net """ import json from typing import Sequence, Callable, Any, Tuple, cast as type_hints_pseudo_cast from . import string from .. import arnied_api #: value used to detect unspecified arguments DEFAULT = object() #: encoding used by the get_cnf and set_cnf binaries ENCODING = "latin1" ############################################################################### # HELPERS ############################################################################### class CnfName(str): """ Custom string where comparisons are case-insensitive. With this class we do not have to worry about case when comparing against the name of cnfvars when filtering. The cnfvar backend is already case- insensitive anyway. """ def __eq__(self, other): if not isinstance(other, str): return False return self.lower() == other.lower() def __contains__(self, name): return name.lower() in self.lower() def startswith(self, prefix, *args, **kwargs): return self.lower().startswith(prefix.lower(), *args, **kwargs) def endswith(self, suffix, *args, **kwargs): return self.lower().endswith(suffix.lower(), *args, **kwargs) def replace(self, old, new, *args, **kwargs): return self.lower().replace(old.lower(), new.lower(), *args, **kwargs) ############################################################################### # BASE API ############################################################################### class BaseCnfList(list): """Base class representing a CNF list with minimal functionality.""" def __init__(self, cnf_iter=None, renumber=False): """ Class constructor. :param cnf_iter: iterator producing CNF elements or arguments for the constructor of the :py:class:`Cnf` class :type: :py:class:`collections.abc.Iterator` producing elements of type :py:class:`Cnf` :param bool renumber: whether to fix up the number/ids of the CNFs Example:: cnf = Cnf("my_cnf", "value") cnf_list = CnfList([ cnf, ("other_cnf", "other value"), ("user", "john", instance=3) ]) """ # Map the values of the iterator to support constructing this list # from Cnf instances or arguments to the Cnf constructor if cnf_iter is not None: iter_ = map(lambda c: c if isinstance(c, Cnf) else Cnf(*c), cnf_iter) else: iter_ = [] super().__init__(iter_) self._renumber_counter = None # initialized and used in renumber if renumber: self.renumber() def renumber(self): """Fix line numbers of CNF variables from this list.""" # NOTE: we don't keep track of operations that change the list as this # would require us to reimplement most of the methods. At least for now # this method should be called again when serializing. self._renumber_counter = 0 def renumber_fn(cnf): self._renumber_counter += 1 cnf.lineno = self._renumber_counter self.for_each_all(renumber_fn) def where(self, where_filter): """ Filter CNFs matching a given predicate. :param where_filter: predicate to apply against CNFs :type where_filter: function accepting a CNF and returning a boolean :returns: an instance of this class with filtered members :rtype: :py:class:`CnfList` """ return CnfList(c for c in self if where_filter(c)) def where_child(self, where_filter): """ Filter CNFs with children matching a given predicate. :param where_filter: predicate to apply against children :type where_filter: function accepting a CNF and returning a boolean :returns: an instance of this class with filtered members :rtype: :py:class:`CnfList` """ def upper_filter(cnf): return any(ch for ch in cnf.children if where_filter(ch)) return self.where(upper_filter) def remove_where(self, where_filter): """ Remove all CNFs from this list matching the given predicate. :param where_filter: predicate to apply against children :type where_filter: function accepting a CNF and returning a boolean :returns: a list of the removed CNF variables :rtype: [:py:class:`Cnf`] """ r = [] # iterate by index for speed and in reverse to keep indexes valid for i in range(len(self) - 1, -1, -1): cnf = self[i] if where_filter(cnf): del self[i] r.append(cnf) return r def for_each(self, fn): """ Apply a function to each element of this list. :param fn: function to apply to the elements :type fn: function accepting a CNF (result value is ignored) :returns: this same instance :rtype: :py:class:`CnfList` .. note:: this is mostly the same as the built-in map() function, except that it changes the list in place. """ for c in self: try: fn(c) except StopIteration: # support breaking break return self def for_each_child(self, fn): """ Apply a function to each child of the elements of this list. :param fn: function to apply to the elements :type fn: function accepting a CNF (result value is ignored) :returns: this same instance :rtype: :py:class:`CnfList` .. note:: if a CNF does not have children, it is ignored """ for c in self: children = c.children or CnfList() for ch in children: try: fn(ch) except StopIteration: # support breaking break # apply recursively, too children.for_each_child(fn) return self def for_each_all(self, fn): """ Apply a function to every CNF of this list, parent or child. :param fn: function to apply to the elements :type fn: function accepting a CNF (result value is ignored) :returns: this same instance :rtype: :py:class:`CnfList` """ for c in self: try: fn(c) except StopIteration: # support breaking break children = c.children or CnfList() children.for_each_all(fn) return self def __str__(self): """ Get a string representation of this instance. :returns: a string in the cnfvar format :rtype: str """ return "\n".join((str(c) for c in self)) def __add__(self, other): return CnfList(super().__add__(other)) def add(self, *args, **kwargs): """ Add a CNF variable to the list. Arguments can either be a single instance of the :py:class:`Cnf` class or a list of arguments to be passed to the constructor of that class. Similar to the :py:func:`add_child` method for a `Cnf`. :returns: the instance that was created :rtype: :py:class:`Cnf` """ # support passing a Cnf instance if len(args) == 1 and not kwargs: cnf = args[0] assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument" else: cnf = Cnf(*args, **kwargs) self.append(cnf) return cnf class BaseCnf: """Base class representing a CNF variable with minimal functionality.""" _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\"" _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\"" _NEST_INDENT = " " def __init__(self, name, value, instance=0, parent=None, lineno=None, comment=None): """ Create this instance. :param str name: name of the cnfvar (case does not matter) :param str value: value for this cnfvar (will be converted to string if it is not of this type) :param int instance: instance of this cnfvar :param parent: a parent Cnf instance :type parent: :py:class:`BaseCnf` :param int lineno: line number :param str comment: cnfvar comment """ self.name = CnfName(name) self.value = value self.instance = int(instance) self.parent = parent self.lineno = int(lineno or 0) self.comment = comment self.__children = CnfList() # Use getters and setters to keep internal consistency and fail-fast # preventing invalid data from being sent to the cnfvar backend. def _get_name(self): return self.__name def _set_name(self, value): # convert Python strings passed as name to our custom string self.__name = CnfName(value) name = property(_get_name, _set_name) def _get_instance(self): return self.__instance def _set_instance(self, value): # fail-fast and make sure instance is a valid integer self.__instance = int(value) instance = property(_get_instance, _set_instance) def _get_lineno(self): return self.__lineno def _set_lineno(self, value): # fail-fast and make sure lineno is a valid integer self.__lineno = int(value) lineno = property(_get_lineno, _set_lineno) def _get_children(self): return self.__children # No setter to sure that the children property will not # be replaced by something other than a `CnfList` children = property(_get_children) def _get_value(self): return self.__value def _set_value(self, value): # Make sure the value is always stored as a string # (no other types make sense to the cnfvar backend) self.__value = str(value) value = property(_get_value, _set_value) def add_child(self, *args, **kwargs): """ Add a child CNF variable. Arguments can either be a single instance of the :py:class:`Cnf` class or a list of arguments to be passed to the constructor of that class. :returns: the instance that was created :rtype: :py:class:`Cnf` Example:: cnf = Cnf("my_parent_cnf", "parent") cnf2 = Cnf("my_child_cnf", "john") # adding a child as a CNF instance cnf.add_child(cnf2) # adding a child passing arguments of the Cnf constructor cnf.add_child("my_child_cnf", "jane", instance=2) """ # support passing a Cnf instance if len(args) == 1 and not kwargs: cnf = args[0] assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument" else: cnf = Cnf(*args, **kwargs) # store a reference to parent to easily access it cnf.parent = self # It seems the CNF backend (at least using set_cnf as opposed to the varlink # API) only accepts instance with value of -1 for top-level variables, so # just in case fix up instances when adding children with the default value. if cnf.instance == -1: cnf.instance = 0 for c in self.children: if c.name == cnf.name: cnf.instance += 1 self.children.append(cnf) return cnf def add_children(self, *children): """ Add multiple child CNF variables. Each argument must be either an instance of the :py:class:`Cnf` class or a tuple/list to be expanded and passed to construct that instance. :returns: a list of the instances that were created :rtype: :py:class:`CnfList` Example:: cnf = Cnf("my_parent_cnf", "parent") cnf2 = Cnf("my_child_cnf", "john") cnf.add_children( cnf2, # cnf instance directly ("my_child_cnf", "jane", instance=2), # pass a tuple with args ["my_child_cnf", "jack", instance=3]) # pass a list with args # adding a child passing arguments of the Cnf constructor cnf.add_child("my_child_cnf", "jane", instance=2) """ added_children = CnfList() for c in children: if isinstance(c, Cnf): new_child = self.add_child(c) elif isinstance(c, tuple) or isinstance(c, list): new_child = self.add_child(*c) else: raise ValueError(f"Children item {c} must be either a Cnf, a tuple or a list") added_children.append(new_child) return added_children def __eq__(self, other): """ Equality implementation. :param other: object to compare this instance against :type other: any :returns: whether `other` is equal to this instance :rtype: bool This is particularly useful when comparing instances of :py:class:`CnfList` """ if not isinstance(other, Cnf): return False # NOTE: we try to define two variables as equal in the same way as the # set_cnf binary would if we were passing it an updated CNF variable. # It does not take comments, children and lineno into account when we # pass it a variable; it will rather compare the data we compare here, # and if it finds a match it will update it with the changed children. return self.name == other.name \ and self.value == other.value \ and self.instance == other.instance \ and self.parent == other.parent def __str__(self): """ Get a string representation of this instance. :returns: a string in the cnfvar format :rtype: str """ if self.parent is None: this_str = self._PARENT_TEMPLATE.format( lineno=self.lineno, name=self.name.upper(), instance=self.instance, value=self.value ) else: depth = 0 curr = self while curr.parent is not None: depth += 1 curr = curr.parent this_str = self._CHILD_TEMPLATE.format( lineno=self.lineno, indent=self._NEST_INDENT * depth, parent=self.parent.lineno, name=self.name.upper(), instance=self.instance, value=self.value ) if self.comment is not None: this_str += f" # {self.comment}" for child in self.children: this_str += f"\n{child}" return this_str def __repr__(self): """ Get a printable representation of this instance. :returns: a string in the cnfvar format :rtype: str """ repr_ = self._PARENT_TEMPLATE.format( lineno=self.lineno, name=self.name.upper(), instance=self.instance, value=self.value ) if self.parent is None else self._CHILD_TEMPLATE.format( lineno=self.lineno, indent="", parent=self.parent.lineno, name=self.name.upper(), instance=self.instance, value=self.value ) return f"Cnf{{ {repr_} [children={len(self.children)}] }}" ############################################################################### # MIXINS ############################################################################### # # These mixins add functionality to the base API without polluting it. # class CnfListSerializationMixin(BaseCnfList): """Add serialization support to BaseCnfList.""" def to_cnf_string(self, renumber=True): """ Generate a string representation of this list in the cnfvar format. :param bool renumber: whether to fix the lineno of the cnfvars :returns: the CNF string :rtype: str """ if renumber: self.renumber() return str(self) def to_cnf_file(self, path, renumber=True, encoding=ENCODING): """ Dump a string representation of this list in the cnfvar format to a file. :param str path: path to the file to write to :param bool renumber: whether to fix the lineno of the cnfvars :param str encoding: encoding to use to save the file """ if renumber: self.renumber() with open(path, "w", encoding=encoding) as fp: fp.write(str(self)) def to_json_string(self, renumber=True): """ Generate a JSON representation of this list in the cnfvar format. :param bool renumber: whether to fix the lineno of the cnfvars :returns: the JSON string :rtype: str """ def _to_dict(cnf): d = { "number": cnf.lineno, "varname": cnf.name, "data": cnf.value, "instance": cnf.instance } if cnf.parent and cnf.parent.lineno: d["parent"] = cnf.parent.lineno if cnf.comment is not None: d["comment"] = cnf.comment if len(cnf.children) > 0: d["children"] = [_to_dict(c) for c in cnf.children] return d if renumber: self.renumber() json_list = [_to_dict(c) for c in self] return json.dumps({"cnf": json_list}) def to_json_file(self, path, renumber=True): """ Dump a JSON representation of this list to a file. :param str path: path to the file to write to :param bool renumber: whether to fix the lineno of the cnfvars """ with open(path, "w", encoding="utf8") as fp: fp.write(self.to_json_string(renumber=renumber)) @classmethod def _from_cnf_structure(cls, obj): """ Create a list from a JSON structure obtainable from `get_cnf --json`. :param obj: an object as defined in the :py:mod:`cnfvar` :type obj: {str, {str, str or int}} :returns: a list of cnfvars :rtype: :py:class:`CnfList` """ return cls(map(Cnf._from_cnf_structure, obj["cnf"])) @classmethod def from_cnf_string(cls, data): """ Create a list from a cnfvar string. :param str data: string to generate the list from :returns: a list of cnfvars :rtype: :py:class:`CnfList` """ cnf_obj = string.read_cnf(data) return CnfList._from_cnf_structure(cnf_obj) @classmethod def from_cnf_file(cls, path, encoding=ENCODING): """ Create a list from a cnfvar file. :param str path: path to the file to read :param str encoding: encoding to use to open the file (defaults to latin1 as this is the default encoding) :returns: a list of cnfvars :rtype: :py:class:`CnfList` """ with open(path, "r", encoding=encoding) as fp: return CnfList.from_cnf_string(fp.read()) @classmethod def from_json_string(cls, data): """ Create a list from a json string. :param str data: string to generate the list from :returns: a list of cnfvars :rtype: :py:class:`CnfList` """ cnf_obj = json.loads(data) return CnfList._from_cnf_structure(cnf_obj) @classmethod def from_json_file(cls, path): """ Create a list from a json file. :param str path: path to the file to read :returns: a list of cnfvars :rtype: :py:class:`CnfList` """ with open(path, "r", encoding="utf8") as fp: return CnfList.from_json_string(fp.read()) class CnfSerializationMixin(BaseCnf): """Add serialization support to BaseCnf.""" def to_cnf_string(self, renumber=True): """ Generate a string representation of this list in the cnfvar format. :param bool renumber: whether to fix the lineno of this cnfvar and its children :returns: the CNF string :rtype: str """ return CnfList([self]).to_cnf_string(renumber=renumber) def to_cnf_file(self, path, renumber=True, encoding=ENCODING): """ Dump a string representation of this instance to a file. :param str path: path to the file to write to :param bool renumber: whether to fix the lineno of this cnfvar and its children :param str encoding: encoding to use to save the file """ CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding) def to_json_string(self, renumber=True): """ Convert this instance to a JSON string. :param bool renumber: whether to fix the lineno of the cnfvars :returns: the JSON string :rtype: str """ return CnfList([self]).to_json_string(renumber=renumber) def to_json_file(self, path, renumber=True): """ Dump a JSON representation of this instance to a file. :param str path: path to the file to write to :param bool renumber: whether to fix the lineno of this cnfvar and its children """ CnfList([self]).to_json_file(path, renumber=renumber) @classmethod def _from_cnf_structure(cls, obj): """ Create an instance from a JSON structure obtainable from `get_cnf --json`. :param obj: dictionary to convert to this instance :type obj: {str, str or int} :returns: the cnf instance created :rtype: :py:class:`Cnf` """ cnf = Cnf(obj["varname"], obj["data"], instance=obj["instance"], lineno=obj["number"], comment=obj.get("comment", None)) for ch_obj in obj.get("children", []): child_cnf = Cnf._from_cnf_structure(ch_obj) cnf.add_child(child_cnf) return cnf @classmethod def from_cnf_string(cls, data): """ Create an instance of this class from a cnfvar string. :param str data: cnfvar string to convert :returns: the cnf instance created :rtype: :py:class:`Cnf` """ return CnfListSerializationMixin.from_cnf_string(data).single() @classmethod def from_cnf_file(cls, path, encoding=ENCODING): """ Create an instance of this class from a cnfvar file. :param str path: path to the file to read :param str encoding: encoding to use to read the file :returns: the cnf instance created :rtype: :py:class:`Cnf` """ return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single() @classmethod def from_json_string(cls, data): """ Create an instance of this class from a JSON string. :param str data: JSON string to convert :returns: the cnf instance created :rtype: :py:class:`Cnf` """ cnf_obj = json.loads(data) return CnfList._from_cnf_structure(cnf_obj) @classmethod def from_json_file(cls, path): """ Create an instance of this class from a json file. :param str path: path to the file to read :returns: the cnf instance created :rtype: :py:class:`Cnf` """ return CnfListSerializationMixin.from_json_file(path).single() class CnfListArniedApiMixin(BaseCnfList): """Add support for converting this class to and from Arnied API classes.""" def to_api_structure(self): """ Convert this list to the corresponding object in the arnied API. :returns: the converted object :rtype: [:py:class:`arnied_api.CnfVar`] """ return [c.to_api_structure() for c in self] @classmethod def from_api_structure(cls, cnfvar_list): """ Convert a list from the arnied API into a list of this type. :param cnfvar_list: list to convert :type cnfvar_list: [:py:class:`arnied_api.CnfVar`] :returns: the list created :rtype: :py:class:`CnfList` """ return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list), renumber=True) class CnfArniedApiMixin(BaseCnf): """Add support for converting this class to and from Arnied API classes.""" def to_api_structure(self): """ Convert this instance to the corresponding object in the arnied API. :returns: the converted object :rtype: :py:class:`arnied_api.CnfVar` """ return arnied_api.CnfVar( self.name.upper(), self.instance, self.value, False, # default here to False children=[c.to_api_structure() for c in self.children]) @classmethod def from_api_structure(cls, cnfobj): """ Convert an object from the arnied API into an instance of this type. :param cnfobj: object to convert :type cnfobj: :py:class:`arnied_api.CnfVar` :returns: the instance created :rtype: :py:class:`Cnf` """ cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance) children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children)) for c in children: c.parent = cnf cnf.children.extend(children) return cnf class CnfShortcutsMixin(BaseCnf): """Extend the base CNF class with useful methods.""" def enable(self): """Treat this variable as a boolean var and set its value to 1.""" self.value = "1" def disable(self): """Treat this variable as a boolean var and set its value to 0.""" self.value = "0" def is_enabled(self): """Treat this variable as a boolean var and check if its value is 1.""" return self.value == "1" def enable_child_flag(self, name): """ Set the value of the child CNF matching `name` to "1". :param str name: name of the child whose value to enable .. note:: child will be created if it does not exist. """ cnf = self.children.first_with_name(name, default=None) if cnf is None: self.add_child(name, "1") else: cnf.enable() def disable_child_flag(self, name): """ Set the value of the child CNF matching `name` to "0". :param str name: name of the child whose value to disable .. note:: child will be created if it does not exist. """ cnf = self.children.first_with_name(name, default=None) if cnf is None: self.add_child(name, "0") else: cnf.disable() def child_flag_enabled(self, name): """ Check if a given child has a value equal to `1`. :param str name: name of the child to check :returns: whether the value of the given child, if it exists, is 1 :rtype: bool """ cnf = self.children.first_with_name(name, default=None) return cnf.is_enabled() if cnf is not None else False class CnfListQueryingMixin(BaseCnfList): """Mixing adding shortcuts for common filter operations.""" def single(self, where_filter=None, default=DEFAULT): """ Get the only CNF of this list or raise if none or more than one exist. :param where_filter: predicate to apply against CNFs beforehand :type where_filter: function accepting a CNF and returning a boolean :param default: value to return in case the list is empty :type default: any :raises: :py:class:`ValueError` if a single value cannot be found and a default value was not specified :returns: the first and only element of this list, or default if set and no element is present :rtype: :py:class:`Cnf` """ list_ = self.where(where_filter) if where_filter is not None else self if len(list_) == 1: return list_[0] elif len(list_) == 0 and default != DEFAULT: return default else: raise ValueError(f"CnfList does not contain a single item (len={len(list_)})") def first(self, where_filter=None, default=DEFAULT): """ Get the first element in this list or raise if the list is empty. :param where_filter: predicate to apply against CNFs beforehand :type where_filter: function accepting a CNF and returning a boolean :param default: value to return in case the list is empty :type default: any :raises: :py:class:`ValueError` if a single value cannot be found and a default value was not specified :returns: the first element of this list, or default if set and no element is present :rtype: :py:class:`Cnf` """ list_ = self.where(where_filter) if where_filter is not None else self if len(list_) > 0: return list_[0] elif default != DEFAULT: return default else: raise ValueError("Cannot get the first item - CnfList is empty") def with_value(self, value): """Shortcut method for filtering by value.""" return self.where(lambda c: c.value == value) def with_name(self, name): """Shortcut method for filtering by name.""" return self.where(lambda c: c.name == name) def with_instance(self, instance): """Shortcut method for filtering by instance.""" return self.where(lambda c: c.instance == instance) def single_with_name(self, name, default=DEFAULT): """Shortcut method for getting the single item with a given name.""" return self.with_name(name).single(default=default) def single_with_value(self, value, default=DEFAULT): """Shortcut method for getting the single item with a given value.""" return self.with_value(value).single(default=default) def single_with_instance(self, instance, default=DEFAULT): """Shortcut method for getting the single item with a given instance.""" return self.with_instance(instance).single(default=default) def first_with_name(self, name, default=DEFAULT): """Shortcut method for getting the first item with a given name.""" return self.with_name(name).first(default=default) def first_with_value(self, value, default=DEFAULT): """Shortcut method for getting the first item with a given value.""" return self.with_value(value).first(default=default) def first_with_instance(self, instance, default=DEFAULT): """Shortcut method for getting the first item with a given instance.""" return self.with_instance(instance).first(default=default) def highest_instance(self): """Shortcut method for getting the next instance in a list of items.""" return max([c.instance for c in self]) if len(self) > 0 else -1 class CnfDiff(list): """A list of differences between :py:class:`BaseCnfList`s""" def add_missing(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]): self.append(("-", cnf, ancestry)) def add_excess(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]): self.append(("+", cnf, ancestry)) def print(self, output_func: Callable[[str], Any] = print): """ Create a string representation of this diff and "print" it, using given function :param output_func: Function to use for printing :return: Iterator over text lines """ for diff_type, cnf, ancestry in self: if ancestry: ancestral_string = 'in ' + ' > '.join(f"{anc.name}={anc.value}" for anc in ancestry) else: ancestral_string = '' output_func(f"cnf diff: {diff_type} {cnf.name} ({cnf.instance}) = {cnf.value!r} " + ancestral_string) class CnfCompareMixin(BaseCnfList): """Mixin to add a `compare()` function.""" def compare(self, other: BaseCnfList, ignore_list: Sequence[str] = None, ancestry: Tuple[BaseCnf, ...] = None) -> CnfDiff: """ Compare this list of config variables to another list, return differences. :param other: another list of configuration variables :param ignore_list: names of variables to ignore :param ancestry: when comparing recursively, we call this function on children (of children ...). This is the "path" of parent cnf vars that lead to the list of children we currently compare. :return: difference between self and other """ if ancestry is None: ancestry = () if ignore_list is None: ignore_list = () diff = CnfDiff() # check whether all own values also appear in other config for c_own in self: c_own = type_hints_pseudo_cast(BaseCnf, c_own) if c_own.name in ignore_list: continue c_other = other.where(lambda c: c_own == c) if len(c_other) == 0: diff.add_missing(c_own, ancestry) elif len(c_other) == 1: # found matching entry. Recurse into children diff.extend(c_own.children.compare(c_other[0].children, ignore_list, ancestry + (c_own, ))) else: # several variables in other have the same name, value, parent, and instance as c_own?! raise NotImplementedError("This should not be possible!") # reverse check: all other values also appear in own config? for c_other in other: c_other = type_hints_pseudo_cast(BaseCnf, c_other) if c_other.name in ignore_list: continue c_own = self.where(lambda c: c_other == c) if len(c_own) == 0: diff.add_excess(c_other, ancestry) elif len(c_own) == 1: pass # no need to descend into children again else: # several variables in self have the same name, value, parent, and instance as c_other?! raise NotImplementedError("This should not be possible!") return diff ############################################################################### # PUBLIC CLASSES ############################################################################### # # Set up the classes with the mixins we want to be available by default. # class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin, CnfCompareMixin): """Collection of Cnf variables.""" pass class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin): """Class representing a cnfvar.""" pass __all__ = ["CnfList", "Cnf", "CnfDiff"]