Add a new cnfvar2 API
[pyi2ncommon] / src / cnfvar / model.py
1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
3 #
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
6 #
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.
12 #
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.
15 #
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.
18 #
19 # Copyright (c) 2016-2022 Intra2net AG <info@intra2net.com>
20
21 """
22 model: Cnf classes, collection of Cnf classes and multiple filtering methods.
23
24 Featuring
25 - Cnf: class representing a CNF variabe
26
27 - CnfList: a collection of `Cnf` instances
28
29 The classes above inherit from their base types with added mixins which
30 extend them with extra functionality.
31
32 .. codeauthor:: Intra2net
33 """
34
35 import json
36
37 from .. import cnfvar_old, arnied_api
38
39 #: value used to detect unspecified arguments
40 DEFAULT = object()
41 #: encoding used by the get_cnf and set_cnf binaries
42 ENCODING = "latin1"
43
44
45 ###############################################################################
46 # HELPERS
47 ###############################################################################
48
49
50 class CnfName(str):
51     """
52     Custom string where comparisons are case-insensitive.
53
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-
56     insensitive anyway.
57     """
58
59     def __eq__(self, other):
60         if not isinstance(other, str):
61             return False
62         return self.lower() == other.lower()
63
64     def __contains__(self, name):
65         return name.lower() in self.lower()
66
67     def startswith(self, prefix):
68         return self.lower().startswith(prefix.lower())
69
70     def endswith(self, prefix):
71         return self.lower().endswith(prefix.lower())
72
73
74 ###############################################################################
75 # BASE API
76 ###############################################################################
77
78
79 class BaseCnfList(list):
80     """Base class representing a CNF list with minimal functionality."""
81
82     def __init__(self, cnf_iter=None, renumber=False):
83         """
84         Class constructor.
85
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
89                :py:class:`Cnf`
90         :param bool renumber: whether to fix up the number/ids of the CNFs
91
92         Example::
93             cnf = Cnf("my_cnf", "value")
94             cnf_list = CnfList([
95                 cnf,
96                 ("other_cnf", "other value"),
97                 ("user", "john", instance=3)
98             ])
99         """
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)
104         else:
105             iter_ = []
106         super().__init__(iter_)
107         if renumber:
108             self.renumber()
109
110     def renumber(self):
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.
115         self._counter = 0
116
117         def renumber_fn(cnf):
118             self._counter += 1
119             cnf.lineno = self._counter
120
121         self.for_each_all(renumber_fn)
122
123     def where(self, where_filter):
124         """
125         Filter CNFs matching a given predicate.
126
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`
131         """
132         return CnfList(c for c in self if where_filter(c))
133
134     def where_child(self, where_filter):
135         """
136         Filter CNFs with children matching a given predicate.
137
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`
142         """
143         def upper_filter(cnf):
144             return any(ch for ch in cnf.children if where_filter(ch))
145         return self.where(upper_filter)
146
147     def remove_where(self, where_filter):
148         """
149         Remove all CNFs from this list matching the given predicate.
150
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`]
155         """
156         r = []
157         # iterate by index for speed and in reverse to keep indexes valid
158         for i in range(len(self) - 1, -1, -1):
159             cnf = self[i]
160             if where_filter(cnf):
161                 del self[i]
162                 r.append(cnf)
163         return r
164
165     def for_each(self, fn):
166         """
167         Apply a function to each element of this list.
168
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`
173
174         .. note:: this is mostly the same as the built-in map() function,
175         except that it changes the list in place.
176         """
177         for c in self:
178             try:
179                 fn(c)
180             except StopIteration:
181                 # support breaking
182                 break
183         return self
184
185     def for_each_child(self, fn):
186         """
187         Apply a function to each child of the elements of this list.
188
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`
193
194         .. note:: if a CNF does not have children, it is ignored
195         """
196         for c in self:
197             children = c.children or CnfList()
198             for ch in children:
199                 try:
200                     fn(ch)
201                 except StopIteration:
202                     # support breaking
203                     break
204             # apply recursively, too
205             children.for_each_child(fn)
206         return self
207
208     def for_each_all(self, fn):
209         """
210         Apply a function to every CNF of this list, parent or child.
211
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`
216         """
217         for c in self:
218             try:
219                 fn(c)
220             except StopIteration:
221                 # support breaking
222                 break
223             children = c.children or CnfList()
224             children.for_each_all(fn)
225         return self
226
227     def __str__(self):
228         """
229         Get a string representation of this instance.
230
231         :returns: a string in the cnfvar format
232         :rtype: str
233         """
234         return "\n".join((str(c) for c in self))
235
236     def __add__(self, other):
237         return CnfList(super().__add__(other))
238
239
240 class BaseCnf:
241     """Base class representing a CNF variable with minimal functionality."""
242
243     _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\""
244     _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\""
245     _NEST_INDENT = "  "
246
247     def __init__(self, name, value, instance=-1, parent=None,
248                  lineno=None, comment=None):
249         """
250         Create this instance.
251
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
260         """
261         self.name = CnfName(name)
262         self.value = value
263         self.instance = int(instance)
264         self.parent = parent
265         self.lineno = int(lineno or 0)
266         self.comment = comment
267         self.__children = CnfList()
268
269     # Use getters and setters to keep internal consistency and fail-fast
270     # preventing invalid data from being sent to the cnfvar backend.
271
272     def _get_name(self):
273         return self.__name
274
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)
279
280     def _get_instance(self):
281         return self.__instance
282
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)
287
288     def _get_lineno(self):
289         return self.__lineno
290
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)
295
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)
301
302     def _get_value(self):
303         return self.__value
304
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)
310
311     def add_child(self, *args, **kwargs):
312         """
313         Add a child CNF variable.
314
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
317         that class.
318
319         :returns: the instance that was created
320         :rtype: :py:class:`Cnf`
321
322         Example::
323
324             cnf = Cnf("my_parent_cnf", "parent")
325             cnf2 = Cnf("my_child_cnf", "john")
326
327             # adding a child as a CNF instance
328             cnf.add_child(cnf2)
329
330             # adding a child passing arguments of the Cnf constructor
331             cnf.add_child("my_child_cnf", "jane", instance=2)
332         """
333         # support passing a Cnf instance
334         if len(args) == 1 and not kwargs:
335             cnf = args[0]
336             assert isinstance(cnf, Cnf), \
337                    "With one argument, a Cnf instance is mandatory"
338         else:
339             cnf = Cnf(*args, **kwargs)
340
341         # store a reference to parent to easily access it
342         cnf.parent = self
343
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:
348             cnf.instance = 0
349             for c in self.children:
350                 if c.name == cnf.name:
351                     cnf.instance += 1
352
353         self.children.append(cnf)
354         return cnf
355
356     def add_children(self, *children):
357         """
358         Add multiple child CNF variables.
359
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.
362
363         :returns: a list of the instances that were created
364         :rtype: :py:class:`CnfList`
365
366         Example::
367             cnf = Cnf("my_parent_cnf", "parent")
368             cnf2 = Cnf("my_child_cnf", "john")
369
370             cnf.add_children(
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
374
375             # adding a child passing arguments of the Cnf constructor
376             cnf.add_child("my_child_cnf", "jane", instance=2)
377         """
378         added_children = CnfList()
379         for c in children:
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)
384             else:
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
388
389     def __eq__(self, other):
390         """
391         Equality implementation.
392
393         :param other: object to compare this instance against
394         :type other: any
395         :returns: whether `other` is equal to this instance
396         :rtype: bool
397
398         This is particularly useful when comparing instances of
399         :py:class:`CnfList`
400         """
401         if not isinstance(other, Cnf):
402             return False
403
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
413
414     def __str__(self):
415         """
416         Get a string representation of this instance.
417
418         :returns: a string in the cnfvar format
419         :rtype: str
420         """
421         if self.parent is None:
422             this_str = self._PARENT_TEMPLATE.format(
423                 lineno=self.lineno,
424                 name=self.name.upper(),
425                 instance=self.instance,
426                 value=self.value
427             )
428         else:
429             depth = 0
430             curr = self
431             while curr.parent is not None:
432                 depth += 1
433                 curr = curr.parent
434
435             this_str = self._CHILD_TEMPLATE.format(
436                 lineno=self.lineno,
437                 indent=self._NEST_INDENT * depth,
438                 parent=self.parent.lineno,
439                 name=self.name.upper(),
440                 instance=self.instance,
441                 value=self.value
442             )
443
444         if self.comment is not None:
445             this_str += f" # {self.comment}"
446
447         for child in self.children:
448             this_str += f"\n{child}"
449
450         return this_str
451
452     def __repr__(self):
453         """
454         Get a printable representation of this instance.
455
456         :returns: a string in the cnfvar format
457         :rtype: str
458         """
459         repr_ = self._PARENT_TEMPLATE.format(
460             lineno=self.lineno,
461             name=self.name.upper(),
462             instance=self.instance,
463             value=self.value
464         ) if self.parent is None else self._CHILD_TEMPLATE.format(
465             lineno=self.lineno,
466             indent="",
467             parent=self.parent.lineno,
468             name=self.name.upper(),
469             instance=self.instance,
470             value=self.value
471         )
472         return f"Cnf{{ {repr_} [children={len(self.children)}] }}"
473
474
475 ###############################################################################
476 # MIXINS
477 ###############################################################################
478 #
479 # These mixins add functionality to the base API without polluting it.
480 #
481
482 class CnfListSerializationMixin(BaseCnfList):
483     """Add serialization support to BaseCnfList."""
484
485     def to_cnf_structure(self, renumber=True):
486         """
487         Convert this list to an object meaningful to :py:mod:`cnfvar`.
488
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}}
492         """
493         if renumber:
494             self.renumber()
495         return {"cnf": [x.to_cnfvar_dict() for x in self]}
496
497     def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
498         """
499         Dump a string representation of this list in the cnfvar format to a file.
500
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
504         """
505         if renumber:
506             self.renumber()
507         with open(path, "w", encoding=encoding) as fp:
508             fp.write(str(self))
509
510     def to_json_string(self, renumber=True):
511         """
512         Generate a JSON representation of this list in the cnfvar format.
513
514         :param bool renumber: whether to fix the lineno of the cnfvars
515         :returns: the JSON string
516         :rtype: str
517         """
518         def _to_dict(cnf):
519             d = {
520                 "number": cnf.lineno,
521                 "varname": cnf.name,
522                 "data": cnf.value,
523                 "instance": cnf.instance
524             }
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]
531             return d
532         if renumber:
533             renumber = True
534         json_list = [_to_dict(c) for c in self]
535         return json.dumps({"cnf": json_list})
536
537     def to_json_file(self, path, renumber=True):
538         """
539         Dump a JSON representation of this list to a file.
540
541         :param str path: path to the file to write to
542         :param bool renumber: whether to fix the lineno of the cnfvars
543         """
544         with open(path, "w", encoding="utf8") as fp:
545             fp.write(self.to_json_string(renumber=renumber))
546
547     @classmethod
548     def from_cnf_structure(cls, obj):
549         """
550         Create a list from a cnfvar object from the :py:mod:`cnfvar` module.
551
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`
556         """
557         return cls(map(Cnf.from_cnf_structure, obj["cnf"]))
558
559     @classmethod
560     def from_cnf_string(cls, data):
561         """
562         Create a list from a cnfvar string.
563
564         :param str data: string to generate the list from
565         :returns: a list of cnfvars
566         :rtype: :py:class:`CnfList`
567         """
568         cnf_obj = cnfvar_old.read_cnf(data)
569         return CnfList.from_cnf_structure(cnf_obj)
570
571     @classmethod
572     def from_json_string(cls, data):
573         """
574         Create a list from a json string.
575
576         :param str data: string to generate the list from
577         :returns: a list of cnfvars
578         :rtype: :py:class:`CnfList`
579         """
580         cnf_obj = json.loads(data)
581         return CnfList.from_cnf_structure(cnf_obj)
582
583     @classmethod
584     def from_cnf_file(cls, path, encoding=ENCODING):
585         """
586         Create a list from a cnfvar file.
587
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`
593         """
594         with open(path, "r", encoding=encoding) as fp:
595             return CnfList.from_cnf_string(fp.read())
596
597     @classmethod
598     def from_json_file(cls, path):
599         """
600         Create a list from a json file.
601
602         :param str path: path to the file to read
603         :returns: a list of cnfvars
604         :rtype: :py:class:`CnfList`
605         """
606         with open(path, "r", encoding="utf8") as fp:
607             return CnfList.from_json_string(fp.read())
608
609
610 class CnfSerializationMixin(BaseCnf):
611     """Add serialization support to BaseCnf."""
612
613     def to_cnfvar_dict(self):
614         """
615         Convert this instance to dictionary from the :py:mod:`cnfvar` module.
616
617         :returns: the dictionary created
618         :rtype: {str, str or int}
619
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.
622         """
623         d = {
624             "number": self.lineno,
625             "varname": self.name,
626             "data": self.value,
627             "instance": self.instance
628         }
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]
635         return d
636
637     def to_json_string(self, renumber=True):
638         """
639         Convert this instance to a JSON string.
640
641         :param bool renumber: whether to fix the lineno of the cnfvars
642         :returns: the JSON string
643         :rtype: str
644         """
645         return CnfList([self]).to_json_string(renumber=renumber)
646
647     def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
648         """
649         Dump a string representation of this instance to a file.
650
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
654         """
655         CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding)
656
657     def to_json_file(self, path, renumber=True):
658         """
659         Dump a JSON representation of this instance to a file.
660
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
663         """
664         CnfList([self]).to_json_file(path, renumber=renumber)
665
666     @classmethod
667     def from_cnf_structure(cls, obj):
668         """
669         Create an instance from a dictionary from the :py:mod:`cnfvar` module.
670
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`
675         """
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)
682         return cnf
683
684     @classmethod
685     def from_cnf_string(cls, data):
686         """
687         Create an instance of this class from a cnfvar string.
688
689         :param str data: cnfvar string to convert
690         :returns: the cnf instance created
691         :rtype: :py:class:`Cnf`
692         """
693         return CnfListSerializationMixin.from_cnf_string(data).single()
694
695     @classmethod
696     def from_json_string(cls, data):
697         """
698         Create an instance of this class from a JSON string.
699
700         :param str data: JSON string to convert
701         :returns: the cnf instance created
702         :rtype: :py:class:`Cnf`
703         """
704         return CnfListSerializationMixin.from_json_string(data).single()
705
706     @classmethod
707     def from_cnf_file(cls, path, encoding=ENCODING):
708         """
709         Create an instance of this class from a cnfvar file.
710
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`
715         """
716         return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single()
717
718     @classmethod
719     def from_json_file(cls, path):
720         """
721         Create an instance of this class from a json file.
722
723         :param str path: path to the file to read
724         :returns: the cnf instance created
725         :rtype: :py:class:`Cnf`
726         """
727         return CnfListSerializationMixin.from_json_file(path).single()
728
729
730 class CnfListArniedApiMixin(BaseCnfList):
731     """Add support for converting this class to and from Arnied API classes."""
732
733     def to_api_structure(self):
734         """
735         Convert this list to the corresponding object in the arnied API.
736
737         :returns: the converted object
738         :rtype: [:py:class:`arnied_api.CnfVar`]
739         """
740         return [c.to_api_structure() for c in self]
741
742     @classmethod
743     def from_api_structure(cls, cnfvar_list):
744         """
745         Convert a list from the arnied API into a list of this type.
746
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`
751         """
752         return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list),
753                        renumber=True)
754
755
756 class CnfArniedApiMixin(BaseCnf):
757     """Add support for converting this class to and from Arnied API classes."""
758
759     def to_api_structure(self):
760         """
761         Convert this instance to the corresponding object in the arnied API.
762
763         :returns: the converted object
764         :rtype: :py:class:`arnied_api.CnfVar`
765         """
766         return arnied_api.CnfVar(
767             self.name.upper(),
768             self.instance,
769             self.value,
770             False,  # default here to False
771             children=[c.to_api_structure() for c in self.children])
772
773     @classmethod
774     def from_api_structure(cls, cnfobj):
775         """
776         Convert an object from the arnied API into an instance of this type.
777
778         :param cnfobj: object to convert
779         :type cnfobj: :py:class:`arnied_api.CnfVar`
780         :returns: the instance created
781         :rtype: :py:class:`Cnf`
782         """
783         cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance)
784         children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children))
785         for c in children:
786             c.parent = cnf
787         cnf.children.extend(children)
788         return cnf
789
790
791 class CnfShortcutsMixin(BaseCnf):
792     """Extend the base CNF class with useful methods."""
793
794     def enable(self):
795         """Treat this variable as a boolean var and set its value to 1."""
796         self.value = "1"
797
798     def disable(self):
799         """Treat this variable as a boolean var and set its value to 0."""
800         self.value = "0"
801
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"
805
806     def enable_child_flag(self, name):
807         """
808         Set the value of the child CNF matching `name` to "1".
809
810         :param str name: name of the child whose value to enable
811
812         .. note:: child will be created if it does not exist.
813         """
814         cnf = self.children.first_with_name(name, default=None)
815         if cnf is None:
816             cnf = self.add_child(name, "1")
817         else:
818             cnf.enable()
819
820     def disable_child_flag(self, name):
821         """
822         Set the value of the child CNF matching `name` to "0".
823
824         :param str name: name of the child whose value to disable
825
826         .. note:: child will be created if it does not exist.
827         """
828         cnf = self.children.first_with_name(name, default=None)
829         if cnf is None:
830             cnf = self.add_child(name, "0")
831         else:
832             cnf.disable()
833
834     def child_flag_enabled(self, name):
835         """
836         Check if a given child has a value equal to `1`.
837
838         :param str name: name of the child to check
839         :returns: whether the value of the given child, if it exists, is 1
840         :rtype: bool
841         """
842         cnf = self.children.first_with_name(name, default=None)
843         return cnf.is_enabled() if cnf is not None else False
844
845
846 class CnfListQueryingMixin(BaseCnfList):
847     """Mixing adding shortcuts for common filter operations."""
848
849     def single(self, where_filter=None, default=DEFAULT):
850         """
851         Get the only CNF of this list or raise if none or more than one exist.
852
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
856         :type default: any
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`
862         """
863         list_ = self.where(where_filter) if where_filter is not None else self
864
865         if len(list_) == 1:
866             return list_[0]
867         elif len(list_) == 0 and default != DEFAULT:
868             return default
869         else:
870             raise ValueError(f"CnfList does not contain a single item (len={len(list_)})")
871
872     def first(self, where_filter=None, default=DEFAULT):
873         """
874         Get the first element in this list or raise if the list is empty.
875
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
879         :type default: any
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`
885         """
886         list_ = self.where(where_filter) if where_filter is not None else self
887         if len(list_) > 0:
888             return list_[0]
889         elif default != DEFAULT:
890             return default
891         else:
892             raise ValueError("Cannot get the first item - CnfList is empty")
893
894     def with_value(self, value):
895         """Shortcut method for filtering by value."""
896         return self.where(lambda c: c.value == value)
897
898     def with_name(self, name):
899         """Shortcut method for filtering by name."""
900         return self.where(lambda c: c.name == name)
901
902     def with_instance(self, instance):
903         """Shortcut method for filtering by instance."""
904         return self.where(lambda c: c.instance == instance)
905
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)
909
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)
913
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)
917
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)
921
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)
925
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)
929
930
931 ###############################################################################
932 # PUBLIC CLASSES
933 ###############################################################################
934 #
935 # Set up the classes with the mixins we want to be available by default.
936 #
937
938
939 class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
940     """Collection of Cnf variables."""
941
942     pass
943
944
945 class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
946     """Class representing a cnfvar."""
947
948     pass
949
950
951 __all__ = ["CnfList", "Cnf"]