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 variabe
27 - CnfList: a collection of `Cnf` instances
29 The classes above inherit from their base types with added mixins which
30 extend them with extra functionality.
32 .. codeauthor:: Intra2net
37 from .. import cnfvar_old, arnied_api
39 #: value used to detect unspecified arguments
41 #: encoding used by the get_cnf and set_cnf binaries
45 ###############################################################################
47 ###############################################################################
52 Custom string where comparisons are case-insensitive.
54 With this class we do not have to worry about case when comparing against
55 the name of cnfvars when filtering. The cnfvar backend is already case-
59 def __eq__(self, other):
60 if not isinstance(other, str):
62 return self.lower() == other.lower()
64 def __contains__(self, name):
65 return name.lower() in self.lower()
67 def startswith(self, prefix):
68 return self.lower().startswith(prefix.lower())
70 def endswith(self, prefix):
71 return self.lower().endswith(prefix.lower())
74 ###############################################################################
76 ###############################################################################
79 class BaseCnfList(list):
80 """Base class representing a CNF list with minimal functionality."""
82 def __init__(self, cnf_iter=None, renumber=False):
86 :param cnf_iter: iterator producing CNF elements or arguments for the
87 constructor of the :py:class:`Cnf` class
88 :type: :py:class:`collections.abc.Iterator` producing elements of type
90 :param bool renumber: whether to fix up the number/ids of the CNFs
93 cnf = Cnf("my_cnf", "value")
96 ("other_cnf", "other value"),
97 ("user", "john", instance=3)
100 # Map the values of the iterator to support constructing this list
101 # from Cnf instances or arguments to the Cnf constructor
102 if cnf_iter is not None:
103 iter_ = map(lambda c: c if isinstance(c, Cnf) else Cnf(*c), cnf_iter)
106 super().__init__(iter_)
111 """Fix line numbers of CNF variables from this list."""
112 # NOTE: we don't keep track of operations that change the list as this
113 # would require us to reimplement most of the methods. At least for now
114 # this method should be called again when serializing.
117 def renumber_fn(cnf):
119 cnf.lineno = self._counter
121 self.for_each_all(renumber_fn)
123 def where(self, where_filter):
125 Filter CNFs matching a given predicate.
127 :param where_filter: predicate to apply against CNFs
128 :type where_filter: function accepting a CNF and returning a boolean
129 :returns: a instance of this class with filtered members
130 :rtype: :py:class:`CnfList`
132 return CnfList(c for c in self if where_filter(c))
134 def where_child(self, where_filter):
136 Filter CNFs with children matching a given predicate.
138 :param where_filter: predicate to apply against children
139 :type where_filter: function accepting a CNF and returning a boolean
140 :returns: a instance of this class with filtered members
141 :rtype: :py:class:`CnfList`
143 def upper_filter(cnf):
144 return any(ch for ch in cnf.children if where_filter(ch))
145 return self.where(upper_filter)
147 def remove_where(self, where_filter):
149 Remove all CNFs from this list matching the given predicate.
151 :param where_filter: predicate to apply against children
152 :type where_filter: function accepting a CNF and returning a boolean
153 :returns: a list of the removed CNF variables
154 :rtype: [:py:class:`Cnf`]
157 # iterate by index for speed and in reverse to keep indexes valid
158 for i in range(len(self) - 1, -1, -1):
160 if where_filter(cnf):
165 def for_each(self, fn):
167 Apply a function to each element of this list.
169 :param fn: function to apply to the elements
170 :type fn: function accepting a CNF (result value is ignored)
171 :returns: this same instance
172 :rtype: :py:class:`CnfList`
174 .. note:: this is mostly the same as the built-in map() function,
175 except that it changes the list in place.
180 except StopIteration:
185 def for_each_child(self, fn):
187 Apply a function to each child of the elements of this list.
189 :param fn: function to apply to the elements
190 :type fn: function accepting a CNF (result value is ignored)
191 :returns: this same instance
192 :rtype: :py:class:`CnfList`
194 .. note:: if a CNF does not have children, it is ignored
197 children = c.children or CnfList()
201 except StopIteration:
204 # apply recursively, too
205 children.for_each_child(fn)
208 def for_each_all(self, fn):
210 Apply a function to every CNF of this list, parent or child.
212 :param fn: function to apply to the elements
213 :type fn: function accepting a CNF (result value is ignored)
214 :returns: this same instance
215 :rtype: :py:class:`CnfList`
220 except StopIteration:
223 children = c.children or CnfList()
224 children.for_each_all(fn)
229 Get a string representation of this instance.
231 :returns: a string in the cnfvar format
234 return "\n".join((str(c) for c in self))
236 def __add__(self, other):
237 return CnfList(super().__add__(other))
241 """Base class representing a CNF variable with minimal functionality."""
243 _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\""
244 _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\""
247 def __init__(self, name, value, instance=-1, parent=None,
248 lineno=None, comment=None):
250 Create this instance.
252 :param str name: name of the cnfvar (case does not matter)
253 :param str value: value for this cnfvar (will be converted to string
254 if it is not of this type)
255 :param int instance: instance of this cnfvar
256 :param parent: a parent Cnf instance
257 :type parent: :py:class:`BaseCnf`
258 :param int lineno: line number
259 :param str comment: cnfvar comment
261 self.name = CnfName(name)
263 self.instance = int(instance)
265 self.lineno = int(lineno or 0)
266 self.comment = comment
267 self.__children = CnfList()
269 # Use getters and setters to keep internal consistency and fail-fast
270 # preventing invalid data from being sent to the cnfvar backend.
275 def _set_name(self, value):
276 # convert Python strings passed as name to our custom string
277 self.__name = CnfName(value)
278 name = property(_get_name, _set_name)
280 def _get_instance(self):
281 return self.__instance
283 def _set_instance(self, value):
284 # fail-fast and make sure instance is a valid integer
285 self.__instance = int(value)
286 instance = property(_get_instance, _set_instance)
288 def _get_lineno(self):
291 def _set_lineno(self, value):
292 # fail-fast and make sure lineno is a valid integer
293 self.__lineno = int(value)
294 lineno = property(_get_lineno, _set_lineno)
296 def _get_children(self):
297 return self.__children
298 # No setter to sure that the children property will not
299 # be replaced by something other than a `CnfList`
300 children = property(_get_children)
302 def _get_value(self):
305 def _set_value(self, value):
306 # Make sure the value is always stored as a string
307 # (no other types make sense to the cnfvar backend)
308 self.__value = str(value)
309 value = property(_get_value, _set_value)
311 def add_child(self, *args, **kwargs):
313 Add a child CNF variable.
315 Arguments can either be a single instance of the :py:class:`Cnf`
316 class or a list of arguments to be passed to the constructor of
319 :returns: the instance that was created
320 :rtype: :py:class:`Cnf`
324 cnf = Cnf("my_parent_cnf", "parent")
325 cnf2 = Cnf("my_child_cnf", "john")
327 # adding a child as a CNF instance
330 # adding a child passing arguments of the Cnf constructor
331 cnf.add_child("my_child_cnf", "jane", instance=2)
333 # support passing a Cnf instance
334 if len(args) == 1 and not kwargs:
336 assert isinstance(cnf, Cnf), \
337 "With one argument, a Cnf instance is mandatory"
339 cnf = Cnf(*args, **kwargs)
341 # store a reference to parent to easily access it
344 # It seems the CNF backend (at least using set_cnf as opposed to the varlink
345 # API) only accepts instance with value of -1 for top-level variables, so
346 # just in case fix up instances when adding children with the default value.
347 if cnf.instance == -1:
349 for c in self.children:
350 if c.name == cnf.name:
353 self.children.append(cnf)
356 def add_children(self, *children):
358 Add multiple child CNF variables.
360 Each argument must be either an instance of the :py:class:`Cnf` class
361 or a tuple/list to be expanded and passed to construct that instance.
363 :returns: a list of the instances that were created
364 :rtype: :py:class:`CnfList`
367 cnf = Cnf("my_parent_cnf", "parent")
368 cnf2 = Cnf("my_child_cnf", "john")
371 cnf2, # cnf instance directly
372 ("my_child_cnf", "jane", instance=2), # pass a tuple with args
373 ["my_child_cnf", "jack", instance=3]) # pass a list with args
375 # adding a child passing arguments of the Cnf constructor
376 cnf.add_child("my_child_cnf", "jane", instance=2)
378 added_children = CnfList()
380 if isinstance(c, Cnf):
381 new_child = self.add_child(c)
382 elif isinstance(c, tuple) or isinstance(c, list):
383 new_child = self.add_child(*c)
385 raise ValueError(f"Children item {c} must be either a Cnf, a tuple or a list")
386 added_children.append(new_child)
387 return added_children
389 def __eq__(self, other):
391 Equality implementation.
393 :param other: object to compare this instance against
395 :returns: whether `other` is equal to this instance
398 This is particularly useful when comparing instances of
401 if not isinstance(other, Cnf):
404 # NOTE: we try to define two variables as equal in the same way as the
405 # set_cnf binary would if we were passing it an updated CNF variable.
406 # It does not take comments, children and lineno into account when we
407 # pass it a variable; it will rather compare the data we compare here,
408 # and if it finds a match it will update it with the changed children.
409 return self.name == other.name \
410 and self.value == other.value \
411 and self.instance == other.instance \
412 and self.parent == other.parent
416 Get a string representation of this instance.
418 :returns: a string in the cnfvar format
421 if self.parent is None:
422 this_str = self._PARENT_TEMPLATE.format(
424 name=self.name.upper(),
425 instance=self.instance,
431 while curr.parent is not None:
435 this_str = self._CHILD_TEMPLATE.format(
437 indent=self._NEST_INDENT * depth,
438 parent=self.parent.lineno,
439 name=self.name.upper(),
440 instance=self.instance,
444 if self.comment is not None:
445 this_str += f" # {self.comment}"
447 for child in self.children:
448 this_str += f"\n{child}"
454 Get a printable representation of this instance.
456 :returns: a string in the cnfvar format
459 repr_ = self._PARENT_TEMPLATE.format(
461 name=self.name.upper(),
462 instance=self.instance,
464 ) if self.parent is None else self._CHILD_TEMPLATE.format(
467 parent=self.parent.lineno,
468 name=self.name.upper(),
469 instance=self.instance,
472 return f"Cnf{{ {repr_} [children={len(self.children)}] }}"
475 ###############################################################################
477 ###############################################################################
479 # These mixins add functionality to the base API without polluting it.
482 class CnfListSerializationMixin(BaseCnfList):
483 """Add serialization support to BaseCnfList."""
485 def to_cnf_structure(self, renumber=True):
487 Convert this list to an object meaningful to :py:mod:`cnfvar`.
489 :param bool renumber: whether to fix up the number/ids of the CNFs
490 :returns: a dictionary with the converted values
491 :rtype: {str, {str, str or int}}
495 return {"cnf": [x.to_cnfvar_dict() for x in self]}
497 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
499 Dump a string representation of this list in the cnfvar format to a file.
501 :param str path: path to the file to write to
502 :param bool renumber: whether to fix the lineno of the cnfvars
503 :param str encoding: encoding to use to save the file
507 with open(path, "w", encoding=encoding) as fp:
510 def to_json_string(self, renumber=True):
512 Generate a JSON representation of this list in the cnfvar format.
514 :param bool renumber: whether to fix the lineno of the cnfvars
515 :returns: the JSON string
520 "number": cnf.lineno,
523 "instance": cnf.instance
525 if cnf.parent and cnf.parent.lineno:
526 d["parent"] = cnf.parent.lineno
527 if cnf.comment is not None:
528 d["comment"] = cnf.comment
529 if len(cnf.children) > 0:
530 d["children"] = [_to_dict(c) for c in cnf.children]
534 json_list = [_to_dict(c) for c in self]
535 return json.dumps({"cnf": json_list})
537 def to_json_file(self, path, renumber=True):
539 Dump a JSON representation of this list to a file.
541 :param str path: path to the file to write to
542 :param bool renumber: whether to fix the lineno of the cnfvars
544 with open(path, "w", encoding="utf8") as fp:
545 fp.write(self.to_json_string(renumber=renumber))
548 def from_cnf_structure(cls, obj):
550 Create a list from a cnfvar object from the :py:mod:`cnfvar` module.
552 :param obj: an object as defined in the :py:mod:`cnfvar`
553 :type obj: {str, {str, str or int}}
554 :returns: a list of cnfvars
555 :rtype: :py:class:`CnfList`
557 return cls(map(Cnf.from_cnf_structure, obj["cnf"]))
560 def from_cnf_string(cls, data):
562 Create a list from a cnfvar string.
564 :param str data: string to generate the list from
565 :returns: a list of cnfvars
566 :rtype: :py:class:`CnfList`
568 cnf_obj = cnfvar_old.read_cnf(data)
569 return CnfList.from_cnf_structure(cnf_obj)
572 def from_json_string(cls, data):
574 Create a list from a json string.
576 :param str data: string to generate the list from
577 :returns: a list of cnfvars
578 :rtype: :py:class:`CnfList`
580 cnf_obj = json.loads(data)
581 return CnfList.from_cnf_structure(cnf_obj)
584 def from_cnf_file(cls, path, encoding=ENCODING):
586 Create a list from a cnfvar file.
588 :param str path: path to the file to read
589 :param str encoding: encoding to use to open the file (defaults to
590 latin1 as this is the default encoding)
591 :returns: a list of cnfvars
592 :rtype: :py:class:`CnfList`
594 with open(path, "r", encoding=encoding) as fp:
595 return CnfList.from_cnf_string(fp.read())
598 def from_json_file(cls, path):
600 Create a list from a json file.
602 :param str path: path to the file to read
603 :returns: a list of cnfvars
604 :rtype: :py:class:`CnfList`
606 with open(path, "r", encoding="utf8") as fp:
607 return CnfList.from_json_string(fp.read())
610 class CnfSerializationMixin(BaseCnf):
611 """Add serialization support to BaseCnf."""
613 def to_cnfvar_dict(self):
615 Convert this instance to dictionary from the :py:mod:`cnfvar` module.
617 :returns: the dictionary created
618 :rtype: {str, str or int}
620 .. todo:: this method is still needed because dumping cnf variables
621 to strings (json or not) is still delegated to the old cnfvar module.
624 "number": self.lineno,
625 "varname": self.name,
627 "instance": self.instance
629 if self.parent and self.parent.lineno:
630 d["parent"] = self.parent.lineno
631 if self.comment is not None:
632 d["comment"] = self.comment
633 if len(self.children) > 0:
634 d["children"] = [c.to_cnfvar_dict() for c in self.children]
637 def to_json_string(self, renumber=True):
639 Convert this instance to a JSON string.
641 :param bool renumber: whether to fix the lineno of the cnfvars
642 :returns: the JSON string
645 return CnfList([self]).to_json_string(renumber=renumber)
647 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
649 Dump a string representation of this instance to a file.
651 :param str path: path to the file to write to
652 :param bool renumber: whether to fix the lineno of this cnfvar and its children
653 :param str encoding: encoding to use to save the file
655 CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding)
657 def to_json_file(self, path, renumber=True):
659 Dump a JSON representation of this instance to a file.
661 :param str path: path to the file to write to
662 :param bool renumber: whether to fix the lineno of this cnfvar and its children
664 CnfList([self]).to_json_file(path, renumber=renumber)
667 def from_cnf_structure(cls, obj):
669 Create an instance from a dictionary from the :py:mod:`cnfvar` module.
671 :param obj: dictionary to convert to this instance
672 :type obj: {str, str or int}
673 :returns: the cnf instance created
674 :rtype: :py:class:`Cnf`
676 cnf = Cnf(obj["varname"], obj["data"],
677 instance=obj["instance"], lineno=obj["number"],
678 comment=obj.get("comment", None))
679 for ch_obj in obj.get("children", []):
680 child_cnf = Cnf.from_cnf_structure(ch_obj)
681 cnf.add_child(child_cnf)
685 def from_cnf_string(cls, data):
687 Create an instance of this class from a cnfvar string.
689 :param str data: cnfvar string to convert
690 :returns: the cnf instance created
691 :rtype: :py:class:`Cnf`
693 return CnfListSerializationMixin.from_cnf_string(data).single()
696 def from_json_string(cls, data):
698 Create an instance of this class from a JSON string.
700 :param str data: JSON string to convert
701 :returns: the cnf instance created
702 :rtype: :py:class:`Cnf`
704 return CnfListSerializationMixin.from_json_string(data).single()
707 def from_cnf_file(cls, path, encoding=ENCODING):
709 Create an instance of this class from a cnfvar file.
711 :param str path: path to the file to read
712 :param str encoding: encoding to use to read the file
713 :returns: the cnf instance created
714 :rtype: :py:class:`Cnf`
716 return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single()
719 def from_json_file(cls, path):
721 Create an instance of this class from a json file.
723 :param str path: path to the file to read
724 :returns: the cnf instance created
725 :rtype: :py:class:`Cnf`
727 return CnfListSerializationMixin.from_json_file(path).single()
730 class CnfListArniedApiMixin(BaseCnfList):
731 """Add support for converting this class to and from Arnied API classes."""
733 def to_api_structure(self):
735 Convert this list to the corresponding object in the arnied API.
737 :returns: the converted object
738 :rtype: [:py:class:`arnied_api.CnfVar`]
740 return [c.to_api_structure() for c in self]
743 def from_api_structure(cls, cnfvar_list):
745 Convert a list from the arnied API into a list of this type.
747 :param cnfvar_list: list to convert
748 :type cnfvar_list: [:py:class:`arnied_api.CnfVar`]
749 :returns: the list created
750 :rtype: :py:class:`CnfList`
752 return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list),
756 class CnfArniedApiMixin(BaseCnf):
757 """Add support for converting this class to and from Arnied API classes."""
759 def to_api_structure(self):
761 Convert this instance to the corresponding object in the arnied API.
763 :returns: the converted object
764 :rtype: :py:class:`arnied_api.CnfVar`
766 return arnied_api.CnfVar(
770 False, # default here to False
771 children=[c.to_api_structure() for c in self.children])
774 def from_api_structure(cls, cnfobj):
776 Convert an object from the arnied API into an instance of this type.
778 :param cnfobj: object to convert
779 :type cnfobj: :py:class:`arnied_api.CnfVar`
780 :returns: the instance created
781 :rtype: :py:class:`Cnf`
783 cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance)
784 children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children))
787 cnf.children.extend(children)
791 class CnfShortcutsMixin(BaseCnf):
792 """Extend the base CNF class with useful methods."""
795 """Treat this variable as a boolean var and set its value to 1."""
799 """Treat this variable as a boolean var and set its value to 0."""
802 def is_enabled(self):
803 """Treat this variable as a boolean var and check if its value is 1."""
804 return self.value == "1"
806 def enable_child_flag(self, name):
808 Set the value of the child CNF matching `name` to "1".
810 :param str name: name of the child whose value to enable
812 .. note:: child will be created if it does not exist.
814 cnf = self.children.first_with_name(name, default=None)
816 cnf = self.add_child(name, "1")
820 def disable_child_flag(self, name):
822 Set the value of the child CNF matching `name` to "0".
824 :param str name: name of the child whose value to disable
826 .. note:: child will be created if it does not exist.
828 cnf = self.children.first_with_name(name, default=None)
830 cnf = self.add_child(name, "0")
834 def child_flag_enabled(self, name):
836 Check if a given child has a value equal to `1`.
838 :param str name: name of the child to check
839 :returns: whether the value of the given child, if it exists, is 1
842 cnf = self.children.first_with_name(name, default=None)
843 return cnf.is_enabled() if cnf is not None else False
846 class CnfListQueryingMixin(BaseCnfList):
847 """Mixing adding shortcuts for common filter operations."""
849 def single(self, where_filter=None, default=DEFAULT):
851 Get the only CNF of this list or raise if none or more than one exist.
853 :param where_filter: predicate to apply against CNFs beforehand
854 :type where_filter: function accepting a CNF and returning a boolean
855 :param default: value to return in case the list is empty
857 :raises: :py:class:`ValueError` if a single value cannot be found and
858 a default value was not specified
859 :returns: the first and only element of this list, or default if set
860 and no element is present
861 :rtype: :py:class:`Cnf`
863 list_ = self.where(where_filter) if where_filter is not None else self
867 elif len(list_) == 0 and default != DEFAULT:
870 raise ValueError(f"CnfList does not contain a single item (len={len(list_)})")
872 def first(self, where_filter=None, default=DEFAULT):
874 Get the first element in this list or raise if the list is empty.
876 :param where_filter: predicate to apply against CNFs beforehand
877 :type where_filter: function accepting a CNF and returning a boolean
878 :param default: value to return in case the list is empty
880 :raises: :py:class:`ValueError` if a single value cannot be found and
881 a default value was not specified
882 :returns: the first element of this list, or default if set and
883 no element is present
884 :rtype: :py:class:`Cnf`
886 list_ = self.where(where_filter) if where_filter is not None else self
889 elif default != DEFAULT:
892 raise ValueError("Cannot get the first item - CnfList is empty")
894 def with_value(self, value):
895 """Shortcut method for filtering by value."""
896 return self.where(lambda c: c.value == value)
898 def with_name(self, name):
899 """Shortcut method for filtering by name."""
900 return self.where(lambda c: c.name == name)
902 def with_instance(self, instance):
903 """Shortcut method for filtering by instance."""
904 return self.where(lambda c: c.instance == instance)
906 def single_with_name(self, name, default=DEFAULT):
907 """Shortcut method for getting the single item with a given name."""
908 return self.with_name(name).single(default=default)
910 def single_with_value(self, value, default=DEFAULT):
911 """Shortcut method for getting the single item with a given value."""
912 return self.with_value(value).single(default=default)
914 def single_with_instance(self, instance, default=DEFAULT):
915 """Shortcut method for getting the single item with a given instance."""
916 return self.with_instance(instance).single(default=default)
918 def first_with_name(self, name, default=DEFAULT):
919 """Shortcut method for getting the first item with a given name."""
920 return self.with_name(name).first(default=default)
922 def first_with_value(self, value, default=DEFAULT):
923 """Shortcut method for getting the first item with a given value."""
924 return self.with_value(value).first(default=default)
926 def first_with_instance(self, instance, default=DEFAULT):
927 """Shortcut method for getting the first item with a given instance."""
928 return self.with_instance(instance).first(default=default)
931 ###############################################################################
933 ###############################################################################
935 # Set up the classes with the mixins we want to be available by default.
939 class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
940 """Collection of Cnf variables."""
945 class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
946 """Class representing a cnfvar."""
951 __all__ = ["CnfList", "Cnf"]