1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
7 # As a special exception, if other files instantiate templates or use macros
8 # or inline functions from this file, or you compile this file and link it
9 # with other works to produce a work based on this file, this file
10 # does not by itself cause the resulting work to be covered
11 # by the GNU General Public License.
13 # However the source code for this file must still be made available
14 # in accordance with section (3) of the GNU General Public License.
16 # This exception does not invalidate any other reasons why a work based
17 # on this file might be covered by the GNU General Public License.
19 # Copyright (c) 2016-2022 Intra2net AG <info@intra2net.com>
22 model: Cnf classes, collection of Cnf classes and multiple filtering methods.
25 - Cnf: class representing a CNF variable
26 - CnfList: a collection of `Cnf` instances
28 The classes above inherit from their base types with added mixins which
29 extend them with extra functionality.
31 .. seealso:: Overview Diagram linked to from doc main page
33 .. codeauthor:: Intra2net
37 from typing import Sequence, Callable, Any, Tuple, cast as type_hints_pseudo_cast
40 from .. import arnied_api
42 #: value used to detect unspecified arguments
44 #: encoding used by the get_cnf and set_cnf binaries
48 ###############################################################################
50 ###############################################################################
55 Custom string where comparisons are case-insensitive.
57 With this class we do not have to worry about case when comparing against
58 the name of cnfvars when filtering. The cnfvar backend is already case-
62 def __eq__(self, other):
63 if not isinstance(other, str):
65 return self.lower() == other.lower()
67 def __contains__(self, name):
68 return name.lower() in self.lower()
70 def startswith(self, prefix, *args, **kwargs):
71 return self.lower().startswith(prefix.lower(), *args, **kwargs)
73 def endswith(self, suffix, *args, **kwargs):
74 return self.lower().endswith(suffix.lower(), *args, **kwargs)
76 def replace(self, old, new, *args, **kwargs):
77 return self.lower().replace(old.lower(), new.lower(), *args, **kwargs)
80 ###############################################################################
82 ###############################################################################
85 class BaseCnfList(list):
86 """Base class representing a CNF list with minimal functionality."""
88 def __init__(self, cnf_iter=None, renumber=False):
92 :param cnf_iter: iterator producing CNF elements or arguments for the
93 constructor of the :py:class:`Cnf` class
94 :type: :py:class:`collections.abc.Iterator` producing elements of type
96 :param bool renumber: whether to fix up the number/ids of the CNFs
99 cnf = Cnf("my_cnf", "value")
102 ("other_cnf", "other value"),
103 ("user", "john", instance=3)
106 # Map the values of the iterator to support constructing this list
107 # from Cnf instances or arguments to the Cnf constructor
108 if cnf_iter is not None:
109 iter_ = map(lambda c: c if isinstance(c, Cnf) else Cnf(*c), cnf_iter)
112 super().__init__(iter_)
113 self._renumber_counter = None # initialized and used in renumber
118 """Fix line numbers of CNF variables from this list."""
119 # NOTE: we don't keep track of operations that change the list as this
120 # would require us to reimplement most of the methods. At least for now
121 # this method should be called again when serializing.
122 self._renumber_counter = 0
124 def renumber_fn(cnf):
125 self._renumber_counter += 1
126 cnf.lineno = self._renumber_counter
128 self.for_each_all(renumber_fn)
130 def where(self, where_filter):
132 Filter CNFs matching a given predicate.
134 :param where_filter: predicate to apply against CNFs
135 :type where_filter: function accepting a CNF and returning a boolean
136 :returns: an instance of this class with filtered members
137 :rtype: :py:class:`CnfList`
139 return CnfList(c for c in self if where_filter(c))
141 def where_child(self, where_filter):
143 Filter CNFs with children matching a given predicate.
145 :param where_filter: predicate to apply against children
146 :type where_filter: function accepting a CNF and returning a boolean
147 :returns: an instance of this class with filtered members
148 :rtype: :py:class:`CnfList`
150 def upper_filter(cnf):
151 return any(ch for ch in cnf.children if where_filter(ch))
152 return self.where(upper_filter)
154 def remove_where(self, where_filter):
156 Remove all CNFs from this list matching the given predicate.
158 :param where_filter: predicate to apply against children
159 :type where_filter: function accepting a CNF and returning a boolean
160 :returns: a list of the removed CNF variables
161 :rtype: [:py:class:`Cnf`]
164 # iterate by index for speed and in reverse to keep indexes valid
165 for i in range(len(self) - 1, -1, -1):
167 if where_filter(cnf):
172 def for_each(self, fn):
174 Apply a function to each element of this list.
176 :param fn: function to apply to the elements
177 :type fn: function accepting a CNF (result value is ignored)
178 :returns: this same instance
179 :rtype: :py:class:`CnfList`
181 .. note:: this is mostly the same as the built-in map() function,
182 except that it changes the list in place.
187 except StopIteration:
192 def for_each_child(self, fn):
194 Apply a function to each child of the elements of this list.
196 :param fn: function to apply to the elements
197 :type fn: function accepting a CNF (result value is ignored)
198 :returns: this same instance
199 :rtype: :py:class:`CnfList`
201 .. note:: if a CNF does not have children, it is ignored
204 children = c.children or CnfList()
208 except StopIteration:
211 # apply recursively, too
212 children.for_each_child(fn)
215 def for_each_all(self, fn):
217 Apply a function to every CNF of this list, parent or child.
219 :param fn: function to apply to the elements
220 :type fn: function accepting a CNF (result value is ignored)
221 :returns: this same instance
222 :rtype: :py:class:`CnfList`
227 except StopIteration:
230 children = c.children or CnfList()
231 children.for_each_all(fn)
236 Get a string representation of this instance.
238 :returns: a string in the cnfvar format
241 return "\n".join((str(c) for c in self))
243 def __add__(self, other):
244 return CnfList(super().__add__(other))
246 def add(self, *args, **kwargs):
248 Add a CNF variable to the list.
250 Arguments can either be a single instance of the :py:class:`Cnf`
251 class or a list of arguments to be passed to the constructor of
252 that class. Similar to the :py:func:`add_child` method for a `Cnf`.
254 :returns: the instance that was created
255 :rtype: :py:class:`Cnf`
257 # support passing a Cnf instance
258 if len(args) == 1 and not kwargs:
260 assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
262 cnf = Cnf(*args, **kwargs)
269 """Base class representing a CNF variable with minimal functionality."""
271 _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\""
272 _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\""
275 def __init__(self, name, value, instance=0, parent=None,
276 lineno=None, comment=None):
278 Create this instance.
280 :param str name: name of the cnfvar (case does not matter)
281 :param str value: value for this cnfvar (will be converted to string
282 if it is not of this type)
283 :param int instance: instance of this cnfvar
284 :param parent: a parent Cnf instance
285 :type parent: :py:class:`BaseCnf`
286 :param int lineno: line number
287 :param str comment: cnfvar comment
289 self.name = CnfName(name)
291 self.instance = int(instance)
293 self.lineno = int(lineno or 0)
294 self.comment = comment
295 self.__children = CnfList()
297 # Use getters and setters to keep internal consistency and fail-fast
298 # preventing invalid data from being sent to the cnfvar backend.
303 def _set_name(self, value):
304 # convert Python strings passed as name to our custom string
305 self.__name = CnfName(value)
306 name = property(_get_name, _set_name)
308 def _get_instance(self):
309 return self.__instance
311 def _set_instance(self, value):
312 # fail-fast and make sure instance is a valid integer
313 self.__instance = int(value)
314 instance = property(_get_instance, _set_instance)
316 def _get_lineno(self):
319 def _set_lineno(self, value):
320 # fail-fast and make sure lineno is a valid integer
321 self.__lineno = int(value)
322 lineno = property(_get_lineno, _set_lineno)
324 def _get_children(self):
325 return self.__children
326 # No setter to sure that the children property will not
327 # be replaced by something other than a `CnfList`
328 children = property(_get_children)
330 def _get_value(self):
333 def _set_value(self, value):
334 # Make sure the value is always stored as a string
335 # (no other types make sense to the cnfvar backend)
336 self.__value = str(value)
337 value = property(_get_value, _set_value)
339 def add_child(self, *args, **kwargs):
341 Add a child CNF variable.
343 Arguments can either be a single instance of the :py:class:`Cnf`
344 class or a list of arguments to be passed to the constructor of
347 :returns: the instance that was created
348 :rtype: :py:class:`Cnf`
352 cnf = Cnf("my_parent_cnf", "parent")
353 cnf2 = Cnf("my_child_cnf", "john")
355 # adding a child as a CNF instance
358 # adding a child passing arguments of the Cnf constructor
359 cnf.add_child("my_child_cnf", "jane", instance=2)
361 # support passing a Cnf instance
362 if len(args) == 1 and not kwargs:
364 assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
366 cnf = Cnf(*args, **kwargs)
368 # store a reference to parent to easily access it
371 # It seems the CNF backend (at least using set_cnf as opposed to the varlink
372 # API) only accepts instance with value of -1 for top-level variables, so
373 # just in case fix up instances when adding children with the default value.
374 if cnf.instance == -1:
376 for c in self.children:
377 if c.name == cnf.name:
380 self.children.append(cnf)
383 def add_children(self, *children):
385 Add multiple child CNF variables.
387 Each argument must be either an instance of the :py:class:`Cnf` class
388 or a tuple/list to be expanded and passed to construct that instance.
390 :returns: a list of the instances that were created
391 :rtype: :py:class:`CnfList`
394 cnf = Cnf("my_parent_cnf", "parent")
395 cnf2 = Cnf("my_child_cnf", "john")
398 cnf2, # cnf instance directly
399 ("my_child_cnf", "jane", instance=2), # pass a tuple with args
400 ["my_child_cnf", "jack", instance=3]) # pass a list with args
402 # adding a child passing arguments of the Cnf constructor
403 cnf.add_child("my_child_cnf", "jane", instance=2)
405 added_children = CnfList()
407 if isinstance(c, Cnf):
408 new_child = self.add_child(c)
409 elif isinstance(c, tuple) or isinstance(c, list):
410 new_child = self.add_child(*c)
412 raise ValueError(f"Children item {c} must be either a Cnf, a tuple or a list")
413 added_children.append(new_child)
414 return added_children
416 def __eq__(self, other):
418 Equality implementation.
420 :param other: object to compare this instance against
422 :returns: whether `other` is equal to this instance
425 This is particularly useful when comparing instances of
428 if not isinstance(other, Cnf):
431 # NOTE: we try to define two variables as equal in the same way as the
432 # set_cnf binary would if we were passing it an updated CNF variable.
433 # It does not take comments, children and lineno into account when we
434 # pass it a variable; it will rather compare the data we compare here,
435 # and if it finds a match it will update it with the changed children.
436 return self.name == other.name \
437 and self.value == other.value \
438 and self.instance == other.instance \
439 and self.parent == other.parent
443 Get a string representation of this instance.
445 :returns: a string in the cnfvar format
448 if self.parent is None:
449 this_str = self._PARENT_TEMPLATE.format(
451 name=self.name.upper(),
452 instance=self.instance,
458 while curr.parent is not None:
462 this_str = self._CHILD_TEMPLATE.format(
464 indent=self._NEST_INDENT * depth,
465 parent=self.parent.lineno,
466 name=self.name.upper(),
467 instance=self.instance,
471 if self.comment is not None:
472 this_str += f" # {self.comment}"
474 for child in self.children:
475 this_str += f"\n{child}"
481 Get a printable representation of this instance.
483 :returns: a string in the cnfvar format
486 repr_ = self._PARENT_TEMPLATE.format(
488 name=self.name.upper(),
489 instance=self.instance,
491 ) if self.parent is None else self._CHILD_TEMPLATE.format(
494 parent=self.parent.lineno,
495 name=self.name.upper(),
496 instance=self.instance,
499 return f"Cnf{{ {repr_} [children={len(self.children)}] }}"
502 ###############################################################################
504 ###############################################################################
506 # These mixins add functionality to the base API without polluting it.
509 class CnfListSerializationMixin(BaseCnfList):
510 """Add serialization support to BaseCnfList."""
512 def to_cnf_string(self, renumber=True):
514 Generate a string representation of this list in the cnfvar format.
516 :param bool renumber: whether to fix the lineno of the cnfvars
517 :returns: the CNF string
524 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
526 Dump a string representation of this list in the cnfvar format to a file.
528 :param str path: path to the file to write to
529 :param bool renumber: whether to fix the lineno of the cnfvars
530 :param str encoding: encoding to use to save the file
534 with open(path, "w", encoding=encoding) as fp:
537 def to_json_string(self, renumber=True):
539 Generate a JSON representation of this list in the cnfvar format.
541 :param bool renumber: whether to fix the lineno of the cnfvars
542 :returns: the JSON string
547 "number": cnf.lineno,
550 "instance": cnf.instance
552 if cnf.parent and cnf.parent.lineno:
553 d["parent"] = cnf.parent.lineno
554 if cnf.comment is not None:
555 d["comment"] = cnf.comment
556 if len(cnf.children) > 0:
557 d["children"] = [_to_dict(c) for c in cnf.children]
561 json_list = [_to_dict(c) for c in self]
562 return json.dumps({"cnf": json_list})
564 def to_json_file(self, path, renumber=True):
566 Dump a JSON representation of this list to a file.
568 :param str path: path to the file to write to
569 :param bool renumber: whether to fix the lineno of the cnfvars
571 with open(path, "w", encoding="utf8") as fp:
572 fp.write(self.to_json_string(renumber=renumber))
575 def _from_cnf_structure(cls, obj):
577 Create a list from a JSON structure obtainable from `get_cnf --json`.
579 :param obj: an object as defined in the :py:mod:`cnfvar`
580 :type obj: {str, {str, str or int}}
581 :returns: a list of cnfvars
582 :rtype: :py:class:`CnfList`
584 return cls(map(Cnf._from_cnf_structure, obj["cnf"]))
587 def from_cnf_string(cls, data):
589 Create a list from a cnfvar string.
591 :param str data: string to generate the list from
592 :returns: a list of cnfvars
593 :rtype: :py:class:`CnfList`
595 cnf_obj = string.read_cnf(data)
596 return CnfList._from_cnf_structure(cnf_obj)
599 def from_cnf_file(cls, path, encoding=ENCODING):
601 Create a list from a cnfvar file.
603 :param str path: path to the file to read
604 :param str encoding: encoding to use to open the file (defaults to
605 latin1 as this is the default encoding)
606 :returns: a list of cnfvars
607 :rtype: :py:class:`CnfList`
609 with open(path, "r", encoding=encoding) as fp:
610 return CnfList.from_cnf_string(fp.read())
613 def from_json_string(cls, data):
615 Create a list from a json string.
617 :param str data: string to generate the list from
618 :returns: a list of cnfvars
619 :rtype: :py:class:`CnfList`
621 cnf_obj = json.loads(data)
622 return CnfList._from_cnf_structure(cnf_obj)
625 def from_json_file(cls, path):
627 Create a list from a json file.
629 :param str path: path to the file to read
630 :returns: a list of cnfvars
631 :rtype: :py:class:`CnfList`
633 with open(path, "r", encoding="utf8") as fp:
634 return CnfList.from_json_string(fp.read())
637 class CnfSerializationMixin(BaseCnf):
638 """Add serialization support to BaseCnf."""
640 def to_cnf_string(self, renumber=True):
642 Generate a string representation of this list in the cnfvar format.
644 :param bool renumber: whether to fix the lineno of this cnfvar and its children
645 :returns: the CNF string
648 return CnfList([self]).to_cnf_string(renumber=renumber)
650 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
652 Dump a string representation of this instance to a file.
654 :param str path: path to the file to write to
655 :param bool renumber: whether to fix the lineno of this cnfvar and its children
656 :param str encoding: encoding to use to save the file
658 CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding)
660 def to_json_string(self, renumber=True):
662 Convert this instance to a JSON string.
664 :param bool renumber: whether to fix the lineno of the cnfvars
665 :returns: the JSON string
668 return CnfList([self]).to_json_string(renumber=renumber)
670 def to_json_file(self, path, renumber=True):
672 Dump a JSON representation of this instance to a file.
674 :param str path: path to the file to write to
675 :param bool renumber: whether to fix the lineno of this cnfvar and its children
677 CnfList([self]).to_json_file(path, renumber=renumber)
680 def _from_cnf_structure(cls, obj):
682 Create an instance from a JSON structure obtainable from `get_cnf --json`.
684 :param obj: dictionary to convert to this instance
685 :type obj: {str, str or int}
686 :returns: the cnf instance created
687 :rtype: :py:class:`Cnf`
689 cnf = Cnf(obj["varname"], obj["data"],
690 instance=obj["instance"], lineno=obj["number"],
691 comment=obj.get("comment", None))
692 for ch_obj in obj.get("children", []):
693 child_cnf = Cnf._from_cnf_structure(ch_obj)
694 cnf.add_child(child_cnf)
698 def from_cnf_string(cls, data):
700 Create an instance of this class from a cnfvar string.
702 :param str data: cnfvar string to convert
703 :returns: the cnf instance created
704 :rtype: :py:class:`Cnf`
706 return CnfListSerializationMixin.from_cnf_string(data).single()
709 def from_cnf_file(cls, path, encoding=ENCODING):
711 Create an instance of this class from a cnfvar file.
713 :param str path: path to the file to read
714 :param str encoding: encoding to use to read the file
715 :returns: the cnf instance created
716 :rtype: :py:class:`Cnf`
718 return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single()
721 def from_json_string(cls, data):
723 Create an instance of this class from a JSON string.
725 :param str data: JSON string to convert
726 :returns: the cnf instance created
727 :rtype: :py:class:`Cnf`
729 cnf_obj = json.loads(data)
730 return CnfList._from_cnf_structure(cnf_obj)
733 def from_json_file(cls, path):
735 Create an instance of this class from a json file.
737 :param str path: path to the file to read
738 :returns: the cnf instance created
739 :rtype: :py:class:`Cnf`
741 return CnfListSerializationMixin.from_json_file(path).single()
744 class CnfListArniedApiMixin(BaseCnfList):
745 """Add support for converting this class to and from Arnied API classes."""
747 def to_api_structure(self):
749 Convert this list to the corresponding object in the arnied API.
751 :returns: the converted object
752 :rtype: [:py:class:`arnied_api.CnfVar`]
754 return [c.to_api_structure() for c in self]
757 def from_api_structure(cls, cnfvar_list):
759 Convert a list from the arnied API into a list of this type.
761 :param cnfvar_list: list to convert
762 :type cnfvar_list: [:py:class:`arnied_api.CnfVar`]
763 :returns: the list created
764 :rtype: :py:class:`CnfList`
766 return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list),
770 class CnfArniedApiMixin(BaseCnf):
771 """Add support for converting this class to and from Arnied API classes."""
773 def to_api_structure(self):
775 Convert this instance to the corresponding object in the arnied API.
777 :returns: the converted object
778 :rtype: :py:class:`arnied_api.CnfVar`
780 return arnied_api.CnfVar(
784 False, # default here to False
785 children=[c.to_api_structure() for c in self.children])
788 def from_api_structure(cls, cnfobj):
790 Convert an object from the arnied API into an instance of this type.
792 :param cnfobj: object to convert
793 :type cnfobj: :py:class:`arnied_api.CnfVar`
794 :returns: the instance created
795 :rtype: :py:class:`Cnf`
797 cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance)
798 children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children))
801 cnf.children.extend(children)
805 class CnfShortcutsMixin(BaseCnf):
806 """Extend the base CNF class with useful methods."""
809 """Treat this variable as a boolean var and set its value to 1."""
813 """Treat this variable as a boolean var and set its value to 0."""
816 def is_enabled(self):
817 """Treat this variable as a boolean var and check if its value is 1."""
818 return self.value == "1"
820 def enable_child_flag(self, name):
822 Set the value of the child CNF matching `name` to "1".
824 :param str name: name of the child whose value to enable
826 .. note:: child will be created if it does not exist.
828 cnf = self.children.first_with_name(name, default=None)
830 self.add_child(name, "1")
834 def disable_child_flag(self, name):
836 Set the value of the child CNF matching `name` to "0".
838 :param str name: name of the child whose value to disable
840 .. note:: child will be created if it does not exist.
842 cnf = self.children.first_with_name(name, default=None)
844 self.add_child(name, "0")
848 def child_flag_enabled(self, name):
850 Check if a given child has a value equal to `1`.
852 :param str name: name of the child to check
853 :returns: whether the value of the given child, if it exists, is 1
856 cnf = self.children.first_with_name(name, default=None)
857 return cnf.is_enabled() if cnf is not None else False
860 class CnfListQueryingMixin(BaseCnfList):
861 """Mixing adding shortcuts for common filter operations."""
863 def single(self, where_filter=None, default=DEFAULT):
865 Get the only CNF of this list or raise if none or more than one exist.
867 :param where_filter: predicate to apply against CNFs beforehand
868 :type where_filter: function accepting a CNF and returning a boolean
869 :param default: value to return in case the list is empty
871 :raises: :py:class:`ValueError` if a single value cannot be found and
872 a default value was not specified
873 :returns: the first and only element of this list, or default if set
874 and no element is present
875 :rtype: :py:class:`Cnf`
877 list_ = self.where(where_filter) if where_filter is not None else self
881 elif len(list_) == 0 and default != DEFAULT:
884 raise ValueError(f"CnfList does not contain a single item (len={len(list_)})")
886 def first(self, where_filter=None, default=DEFAULT):
888 Get the first element in this list or raise if the list is empty.
890 :param where_filter: predicate to apply against CNFs beforehand
891 :type where_filter: function accepting a CNF and returning a boolean
892 :param default: value to return in case the list is empty
894 :raises: :py:class:`ValueError` if a single value cannot be found and
895 a default value was not specified
896 :returns: the first element of this list, or default if set and
897 no element is present
898 :rtype: :py:class:`Cnf`
900 list_ = self.where(where_filter) if where_filter is not None else self
903 elif default != DEFAULT:
906 raise ValueError("Cannot get the first item - CnfList is empty")
908 def with_value(self, value):
909 """Shortcut method for filtering by value."""
910 return self.where(lambda c: c.value == value)
912 def with_name(self, name):
913 """Shortcut method for filtering by name."""
914 return self.where(lambda c: c.name == name)
916 def with_instance(self, instance):
917 """Shortcut method for filtering by instance."""
918 return self.where(lambda c: c.instance == instance)
920 def single_with_name(self, name, default=DEFAULT):
921 """Shortcut method for getting the single item with a given name."""
922 return self.with_name(name).single(default=default)
924 def single_with_value(self, value, default=DEFAULT):
925 """Shortcut method for getting the single item with a given value."""
926 return self.with_value(value).single(default=default)
928 def single_with_instance(self, instance, default=DEFAULT):
929 """Shortcut method for getting the single item with a given instance."""
930 return self.with_instance(instance).single(default=default)
932 def first_with_name(self, name, default=DEFAULT):
933 """Shortcut method for getting the first item with a given name."""
934 return self.with_name(name).first(default=default)
936 def first_with_value(self, value, default=DEFAULT):
937 """Shortcut method for getting the first item with a given value."""
938 return self.with_value(value).first(default=default)
940 def first_with_instance(self, instance, default=DEFAULT):
941 """Shortcut method for getting the first item with a given instance."""
942 return self.with_instance(instance).first(default=default)
944 def highest_instance(self):
945 """Shortcut method for getting the next instance in a list of items."""
946 return max([c.instance for c in self]) if len(self) > 0 else -1
950 """A list of differences between :py:class:`BaseCnfList`s"""
952 def add_missing(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]):
953 self.append(("-", cnf, ancestry))
955 def add_excess(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]):
956 self.append(("+", cnf, ancestry))
958 def print(self, output_func: Callable[[str], Any] = print):
960 Create a string representation of this diff and "print" it, using given function
962 :param output_func: Function to use for printing
963 :return: Iterator over text lines
965 for diff_type, cnf, ancestry in self:
966 ancestral_string = ' > '.join(f"{anc.name}={anc.value}" for anc in ancestry)
967 output_func(f"cnf diff: {diff_type} {cnf.name} ({cnf.instance}) = {cnf.value!r} in "
971 class CnfCompareMixin(BaseCnfList):
972 """Mixin to add a `compare()` function."""
974 def compare(self, other: BaseCnfList, ignore_list: Sequence[str] = None,
975 ancestry: Tuple[BaseCnf, ...] = None) -> CnfDiff:
977 Compare this list of config variables to another list, return differences.
979 :param other: another list of configuration variables
980 :param ignore_list: names of variables to ignore
981 :param ancestry: when comparing recursively, we call this function on children (of children ...).
982 This is the "path" of parent cnf vars that lead to the list of children we
984 :return: difference between self and other
988 if ignore_list is None:
991 # check whether all own values also appear in other config
993 c_own = type_hints_pseudo_cast(BaseCnf, c_own)
994 if c_own.name in ignore_list:
996 c_other = other.where(lambda c: c_own == c)
997 if len(c_other) == 0:
998 diff.add_missing(c_own, ancestry)
999 elif len(c_other) == 1:
1000 # found matching entry. Recurse into children
1001 diff.extend(c_own.children.compare(c_other[0].children, ignore_list,
1002 ancestry + (c_own, )))
1004 # several variables in other have the same name, value, parent, and instance as c_own?!
1005 raise NotImplementedError("This should not be possible!")
1007 # reverse check: all other values also appear in own config?
1008 for c_other in other:
1009 c_other = type_hints_pseudo_cast(BaseCnf, c_other)
1010 if c_other.name in ignore_list:
1012 c_own = self.where(lambda c: c_other == c)
1014 diff.add_excess(c_other, ancestry)
1015 elif len(c_own) == 1:
1016 pass # no need to descend into children again
1018 # several variables in self have the same name, value, parent, and instance as c_other?!
1019 raise NotImplementedError("This should not be possible!")
1022 ###############################################################################
1024 ###############################################################################
1026 # Set up the classes with the mixins we want to be available by default.
1030 class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin, CnfCompareMixin):
1031 """Collection of Cnf variables."""
1036 class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
1037 """Class representing a cnfvar."""
1042 __all__ = ["CnfList", "Cnf", "CnfDiff"]