a8beb5caf2093e8fd0f509a93aa8b3f27c0152c6
[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 variable
26     - CnfList: a collection of `Cnf` instances
27
28 The classes above inherit from their base types with added mixins which
29 extend them with extra functionality.
30
31 .. seealso:: Overview Diagram linked to from doc main page
32
33 .. codeauthor:: Intra2net
34 """
35
36 import json
37
38 from .. import cnfvar_old, arnied_api
39
40 #: value used to detect unspecified arguments
41 DEFAULT = object()
42 #: encoding used by the get_cnf and set_cnf binaries
43 ENCODING = "latin1"
44
45
46 ###############################################################################
47 # HELPERS
48 ###############################################################################
49
50
51 class CnfName(str):
52     """
53     Custom string where comparisons are case-insensitive.
54
55     With this class we do not have to worry about case when comparing against
56     the name of cnfvars when filtering. The cnfvar backend is already case-
57     insensitive anyway.
58     """
59
60     def __eq__(self, other):
61         if not isinstance(other, str):
62             return False
63         return self.lower() == other.lower()
64
65     def __contains__(self, name):
66         return name.lower() in self.lower()
67
68     def startswith(self, prefix, *args, **kwargs):
69         return self.lower().startswith(prefix.lower(), *args, **kwargs)
70
71     def endswith(self, prefix, *args, **kwargs):
72         return self.lower().endswith(prefix.lower(), *args, **kwargs)
73
74
75 ###############################################################################
76 # BASE API
77 ###############################################################################
78
79
80 class BaseCnfList(list):
81     """Base class representing a CNF list with minimal functionality."""
82
83     def __init__(self, cnf_iter=None, renumber=False):
84         """
85         Class constructor.
86
87         :param cnf_iter: iterator producing CNF elements or arguments for the
88                          constructor of the :py:class:`Cnf` class
89         :type: :py:class:`collections.abc.Iterator` producing elements of type
90                :py:class:`Cnf`
91         :param bool renumber: whether to fix up the number/ids of the CNFs
92
93         Example::
94             cnf = Cnf("my_cnf", "value")
95             cnf_list = CnfList([
96                 cnf,
97                 ("other_cnf", "other value"),
98                 ("user", "john", instance=3)
99             ])
100         """
101         # Map the values of the iterator to support constructing this list
102         # from Cnf instances or arguments to the Cnf constructor
103         if cnf_iter is not None:
104             iter_ = map(lambda c: c if isinstance(c, Cnf) else Cnf(*c), cnf_iter)
105         else:
106             iter_ = []
107         super().__init__(iter_)
108         self._renumber_counter = None     # initialized and used in renumber
109         if renumber:
110             self.renumber()
111
112     def renumber(self):
113         """Fix line numbers of CNF variables from this list."""
114         # NOTE: we don't keep track of operations that change the list as this
115         # would require us to reimplement most of the methods. At least for now
116         # this method should be called again when serializing.
117         self._renumber_counter = 0
118
119         def renumber_fn(cnf):
120             self._renumber_counter += 1
121             cnf.lineno = self._renumber_counter
122
123         self.for_each_all(renumber_fn)
124
125     def where(self, where_filter):
126         """
127         Filter CNFs matching a given predicate.
128
129         :param where_filter: predicate to apply against CNFs
130         :type where_filter: function accepting a CNF and returning a boolean
131         :returns: an instance of this class with filtered members
132         :rtype: :py:class:`CnfList`
133         """
134         return CnfList(c for c in self if where_filter(c))
135
136     def where_child(self, where_filter):
137         """
138         Filter CNFs with children matching a given predicate.
139
140         :param where_filter: predicate to apply against children
141         :type where_filter: function accepting a CNF and returning a boolean
142         :returns: an instance of this class with filtered members
143         :rtype: :py:class:`CnfList`
144         """
145         def upper_filter(cnf):
146             return any(ch for ch in cnf.children if where_filter(ch))
147         return self.where(upper_filter)
148
149     def remove_where(self, where_filter):
150         """
151         Remove all CNFs from this list matching the given predicate.
152
153         :param where_filter: predicate to apply against children
154         :type where_filter: function accepting a CNF and returning a boolean
155         :returns: a list of the removed CNF variables
156         :rtype: [:py:class:`Cnf`]
157         """
158         r = []
159         # iterate by index for speed and in reverse to keep indexes valid
160         for i in range(len(self) - 1, -1, -1):
161             cnf = self[i]
162             if where_filter(cnf):
163                 del self[i]
164                 r.append(cnf)
165         return r
166
167     def for_each(self, fn):
168         """
169         Apply a function to each element of this list.
170
171         :param fn: function to apply to the elements
172         :type fn: function accepting a CNF (result value is ignored)
173         :returns: this same instance
174         :rtype: :py:class:`CnfList`
175
176         .. note:: this is mostly the same as the built-in map() function,
177         except that it changes the list in place.
178         """
179         for c in self:
180             try:
181                 fn(c)
182             except StopIteration:
183                 # support breaking
184                 break
185         return self
186
187     def for_each_child(self, fn):
188         """
189         Apply a function to each child of the elements of this list.
190
191         :param fn: function to apply to the elements
192         :type fn: function accepting a CNF (result value is ignored)
193         :returns: this same instance
194         :rtype: :py:class:`CnfList`
195
196         .. note:: if a CNF does not have children, it is ignored
197         """
198         for c in self:
199             children = c.children or CnfList()
200             for ch in children:
201                 try:
202                     fn(ch)
203                 except StopIteration:
204                     # support breaking
205                     break
206             # apply recursively, too
207             children.for_each_child(fn)
208         return self
209
210     def for_each_all(self, fn):
211         """
212         Apply a function to every CNF of this list, parent or child.
213
214         :param fn: function to apply to the elements
215         :type fn: function accepting a CNF (result value is ignored)
216         :returns: this same instance
217         :rtype: :py:class:`CnfList`
218         """
219         for c in self:
220             try:
221                 fn(c)
222             except StopIteration:
223                 # support breaking
224                 break
225             children = c.children or CnfList()
226             children.for_each_all(fn)
227         return self
228
229     def __str__(self):
230         """
231         Get a string representation of this instance.
232
233         :returns: a string in the cnfvar format
234         :rtype: str
235         """
236         return "\n".join((str(c) for c in self))
237
238     def __add__(self, other):
239         return CnfList(super().__add__(other))
240
241     def add(self, *args, **kwargs):
242         """
243         Add a CNF variable to the list.
244
245         Arguments can either be a single instance of the :py:class:`Cnf`
246         class or a list of arguments to be passed to the constructor of
247         that class. Similar to the :py:func:`add_child` method for a `Cnf`.
248
249         :returns: the instance that was created
250         :rtype: :py:class:`Cnf`
251         """
252         # support passing a Cnf instance
253         if len(args) == 1 and not kwargs:
254             cnf = args[0]
255             assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
256         else:
257             cnf = Cnf(*args, **kwargs)
258
259         self.append(cnf)
260         return cnf
261
262
263 class BaseCnf:
264     """Base class representing a CNF variable with minimal functionality."""
265
266     _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\""
267     _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\""
268     _NEST_INDENT = "  "
269
270     def __init__(self, name, value, instance=0, parent=None,
271                  lineno=None, comment=None):
272         """
273         Create this instance.
274
275         :param str name: name of the cnfvar (case does not matter)
276         :param str value: value for this cnfvar (will be converted to string
277                           if it is not of this type)
278         :param int instance: instance of this cnfvar
279         :param parent: a parent Cnf instance
280         :type parent: :py:class:`BaseCnf`
281         :param int lineno: line number
282         :param str comment: cnfvar comment
283         """
284         self.name = CnfName(name)
285         self.value = value
286         self.instance = int(instance)
287         self.parent = parent
288         self.lineno = int(lineno or 0)
289         self.comment = comment
290         self.__children = CnfList()
291
292     # Use getters and setters to keep internal consistency and fail-fast
293     # preventing invalid data from being sent to the cnfvar backend.
294
295     def _get_name(self):
296         return self.__name
297
298     def _set_name(self, value):
299         # convert Python strings passed as name to our custom string
300         self.__name = CnfName(value)
301     name = property(_get_name, _set_name)
302
303     def _get_instance(self):
304         return self.__instance
305
306     def _set_instance(self, value):
307         # fail-fast and make sure instance is a valid integer
308         self.__instance = int(value)
309     instance = property(_get_instance, _set_instance)
310
311     def _get_lineno(self):
312         return self.__lineno
313
314     def _set_lineno(self, value):
315         # fail-fast and make sure lineno is a valid integer
316         self.__lineno = int(value)
317     lineno = property(_get_lineno, _set_lineno)
318
319     def _get_children(self):
320         return self.__children
321     # No setter to sure that the children property will not
322     # be replaced by something other than a `CnfList`
323     children = property(_get_children)
324
325     def _get_value(self):
326         return self.__value
327
328     def _set_value(self, value):
329         # Make sure the value is always stored as a string
330         # (no other types make sense to the cnfvar backend)
331         self.__value = str(value)
332     value = property(_get_value, _set_value)
333
334     def add_child(self, *args, **kwargs):
335         """
336         Add a child CNF variable.
337
338         Arguments can either be a single instance of the :py:class:`Cnf`
339         class or a list of arguments to be passed to the constructor of
340         that class.
341
342         :returns: the instance that was created
343         :rtype: :py:class:`Cnf`
344
345         Example::
346
347             cnf = Cnf("my_parent_cnf", "parent")
348             cnf2 = Cnf("my_child_cnf", "john")
349
350             # adding a child as a CNF instance
351             cnf.add_child(cnf2)
352
353             # adding a child passing arguments of the Cnf constructor
354             cnf.add_child("my_child_cnf", "jane", instance=2)
355         """
356         # support passing a Cnf instance
357         if len(args) == 1 and not kwargs:
358             cnf = args[0]
359             assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
360         else:
361             cnf = Cnf(*args, **kwargs)
362
363         # store a reference to parent to easily access it
364         cnf.parent = self
365
366         # It seems the CNF backend (at least using set_cnf as opposed to the varlink
367         # API) only accepts instance with value of -1 for top-level variables, so
368         # just in case fix up instances when adding children with the default value.
369         if cnf.instance == -1:
370             cnf.instance = 0
371             for c in self.children:
372                 if c.name == cnf.name:
373                     cnf.instance += 1
374
375         self.children.append(cnf)
376         return cnf
377
378     def add_children(self, *children):
379         """
380         Add multiple child CNF variables.
381
382         Each argument must be either an instance of the :py:class:`Cnf` class
383         or a tuple/list to be expanded and passed to construct that instance.
384
385         :returns: a list of the instances that were created
386         :rtype: :py:class:`CnfList`
387
388         Example::
389             cnf = Cnf("my_parent_cnf", "parent")
390             cnf2 = Cnf("my_child_cnf", "john")
391
392             cnf.add_children(
393                 cnf2,                                  # cnf instance directly
394                 ("my_child_cnf", "jane", instance=2),  # pass a tuple with args
395                 ["my_child_cnf", "jack", instance=3])  # pass a list with args
396
397             # adding a child passing arguments of the Cnf constructor
398             cnf.add_child("my_child_cnf", "jane", instance=2)
399         """
400         added_children = CnfList()
401         for c in children:
402             if isinstance(c, Cnf):
403                 new_child = self.add_child(c)
404             elif isinstance(c, tuple) or isinstance(c, list):
405                 new_child = self.add_child(*c)
406             else:
407                 raise ValueError(f"Children item {c} must be either a Cnf, a tuple or a list")
408             added_children.append(new_child)
409         return added_children
410
411     def __eq__(self, other):
412         """
413         Equality implementation.
414
415         :param other: object to compare this instance against
416         :type other: any
417         :returns: whether `other` is equal to this instance
418         :rtype: bool
419
420         This is particularly useful when comparing instances of
421         :py:class:`CnfList`
422         """
423         if not isinstance(other, Cnf):
424             return False
425
426         # NOTE: we try to define two variables as equal in the same way as the
427         # set_cnf binary would if we were passing it an updated CNF variable.
428         # It does not take comments, children and lineno into account when we
429         # pass it a variable; it will rather compare the data we compare here,
430         # and if it finds a match it will update it with the changed children.
431         return self.name == other.name \
432             and self.value == other.value \
433             and self.instance == other.instance \
434             and self.parent == other.parent
435
436     def __str__(self):
437         """
438         Get a string representation of this instance.
439
440         :returns: a string in the cnfvar format
441         :rtype: str
442         """
443         if self.parent is None:
444             this_str = self._PARENT_TEMPLATE.format(
445                 lineno=self.lineno,
446                 name=self.name.upper(),
447                 instance=self.instance,
448                 value=self.value
449             )
450         else:
451             depth = 0
452             curr = self
453             while curr.parent is not None:
454                 depth += 1
455                 curr = curr.parent
456
457             this_str = self._CHILD_TEMPLATE.format(
458                 lineno=self.lineno,
459                 indent=self._NEST_INDENT * depth,
460                 parent=self.parent.lineno,
461                 name=self.name.upper(),
462                 instance=self.instance,
463                 value=self.value
464             )
465
466         if self.comment is not None:
467             this_str += f" # {self.comment}"
468
469         for child in self.children:
470             this_str += f"\n{child}"
471
472         return this_str
473
474     def __repr__(self):
475         """
476         Get a printable representation of this instance.
477
478         :returns: a string in the cnfvar format
479         :rtype: str
480         """
481         repr_ = self._PARENT_TEMPLATE.format(
482             lineno=self.lineno,
483             name=self.name.upper(),
484             instance=self.instance,
485             value=self.value
486         ) if self.parent is None else self._CHILD_TEMPLATE.format(
487             lineno=self.lineno,
488             indent="",
489             parent=self.parent.lineno,
490             name=self.name.upper(),
491             instance=self.instance,
492             value=self.value
493         )
494         return f"Cnf{{ {repr_} [children={len(self.children)}] }}"
495
496
497 ###############################################################################
498 # MIXINS
499 ###############################################################################
500 #
501 # These mixins add functionality to the base API without polluting it.
502 #
503
504 class CnfListSerializationMixin(BaseCnfList):
505     """Add serialization support to BaseCnfList."""
506
507     def to_cnf_structure(self, renumber=True):
508         """
509         Convert this list to an object meaningful to :py:mod:`cnfvar`.
510
511         :param bool renumber: whether to fix up the number/ids of the CNFs
512         :returns: a dictionary with the converted values
513         :rtype: {str, {str, str or int}}
514         """
515         if renumber:
516             self.renumber()
517         return {"cnf": [x.to_cnf_structure() for x in self]}
518
519     def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
520         """
521         Dump a string representation of this list in the cnfvar format to a file.
522
523         :param str path: path to the file to write to
524         :param bool renumber: whether to fix the lineno of the cnfvars
525         :param str encoding: encoding to use to save the file
526         """
527         if renumber:
528             self.renumber()
529         with open(path, "w", encoding=encoding) as fp:
530             fp.write(str(self))
531
532     def to_json_string(self, renumber=True):
533         """
534         Generate a JSON representation of this list in the cnfvar format.
535
536         :param bool renumber: whether to fix the lineno of the cnfvars
537         :returns: the JSON string
538         :rtype: str
539         """
540         def _to_dict(cnf):
541             d = {
542                 "number": cnf.lineno,
543                 "varname": cnf.name,
544                 "data": cnf.value,
545                 "instance": cnf.instance
546             }
547             if cnf.parent and cnf.parent.lineno:
548                 d["parent"] = cnf.parent.lineno
549             if cnf.comment is not None:
550                 d["comment"] = cnf.comment
551             if len(cnf.children) > 0:
552                 d["children"] = [_to_dict(c) for c in cnf.children]
553             return d
554         if renumber:
555             self.renumber()
556         json_list = [_to_dict(c) for c in self]
557         return json.dumps({"cnf": json_list})
558
559     def to_json_file(self, path, renumber=True):
560         """
561         Dump a JSON representation of this list to a file.
562
563         :param str path: path to the file to write to
564         :param bool renumber: whether to fix the lineno of the cnfvars
565         """
566         with open(path, "w", encoding="utf8") as fp:
567             fp.write(self.to_json_string(renumber=renumber))
568
569     @classmethod
570     def from_cnf_structure(cls, obj):
571         """
572         Create a list from a cnfvar object from the :py:mod:`cnfvar` module.
573
574         :param obj: an object as defined in the :py:mod:`cnfvar`
575         :type obj: {str, {str, str or int}}
576         :returns: a list of cnfvars
577         :rtype: :py:class:`CnfList`
578         """
579         return cls(map(Cnf.from_cnf_structure, obj["cnf"]))
580
581     @classmethod
582     def from_cnf_string(cls, data):
583         """
584         Create a list from a cnfvar string.
585
586         :param str data: string to generate the list from
587         :returns: a list of cnfvars
588         :rtype: :py:class:`CnfList`
589         """
590         cnf_obj = cnfvar_old.read_cnf(data)
591         return CnfList.from_cnf_structure(cnf_obj)
592
593     @classmethod
594     def from_json_string(cls, data):
595         """
596         Create a list from a json string.
597
598         :param str data: string to generate the list from
599         :returns: a list of cnfvars
600         :rtype: :py:class:`CnfList`
601         """
602         cnf_obj = json.loads(data)
603         return CnfList.from_cnf_structure(cnf_obj)
604
605     @classmethod
606     def from_cnf_file(cls, path, encoding=ENCODING):
607         """
608         Create a list from a cnfvar file.
609
610         :param str path: path to the file to read
611         :param str encoding: encoding to use to open the file (defaults to
612                              latin1 as this is the default encoding)
613         :returns: a list of cnfvars
614         :rtype: :py:class:`CnfList`
615         """
616         with open(path, "r", encoding=encoding) as fp:
617             return CnfList.from_cnf_string(fp.read())
618
619     @classmethod
620     def from_json_file(cls, path):
621         """
622         Create a list from a json file.
623
624         :param str path: path to the file to read
625         :returns: a list of cnfvars
626         :rtype: :py:class:`CnfList`
627         """
628         with open(path, "r", encoding="utf8") as fp:
629             return CnfList.from_json_string(fp.read())
630
631
632 class CnfSerializationMixin(BaseCnf):
633     """Add serialization support to BaseCnf."""
634
635     def to_cnf_structure(self):
636         """
637         Convert this instance to dictionary from the :py:mod:`cnfvar` module.
638
639         :returns: the dictionary created
640         :rtype: {str, str or int}
641
642         .. todo:: this method is still needed because dumping cnf variables
643         to strings (json or not) is still delegated to the old cnfvar module.
644         """
645         d = {
646             "number": self.lineno,
647             "varname": self.name,
648             "data": self.value,
649             "instance": self.instance
650         }
651         if self.parent and self.parent.lineno:
652             d["parent"] = self.parent.lineno
653         if self.comment is not None:
654             d["comment"] = self.comment
655         if len(self.children) > 0:
656             d["children"] = [c.to_cnf_structure() for c in self.children]
657         return d
658
659     def to_json_string(self, renumber=True):
660         """
661         Convert this instance to a JSON string.
662
663         :param bool renumber: whether to fix the lineno of the cnfvars
664         :returns: the JSON string
665         :rtype: str
666         """
667         return CnfList([self]).to_json_string(renumber=renumber)
668
669     def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
670         """
671         Dump a string representation of this instance to a file.
672
673         :param str path: path to the file to write to
674         :param bool renumber: whether to fix the lineno of this cnfvar and its children
675         :param str encoding: encoding to use to save the file
676         """
677         CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding)
678
679     def to_json_file(self, path, renumber=True):
680         """
681         Dump a JSON representation of this instance to a file.
682
683         :param str path: path to the file to write to
684         :param bool renumber: whether to fix the lineno of this cnfvar and its children
685         """
686         CnfList([self]).to_json_file(path, renumber=renumber)
687
688     @classmethod
689     def from_cnf_structure(cls, obj):
690         """
691         Create an instance from a dictionary from the :py:mod:`cnfvar` module.
692
693         :param obj: dictionary to convert to this instance
694         :type obj: {str, str or int}
695         :returns: the cnf instance created
696         :rtype: :py:class:`Cnf`
697         """
698         cnf = Cnf(obj["varname"], obj["data"],
699                   instance=obj["instance"], lineno=obj["number"],
700                   comment=obj.get("comment", None))
701         for ch_obj in obj.get("children", []):
702             child_cnf = Cnf.from_cnf_structure(ch_obj)
703             cnf.add_child(child_cnf)
704         return cnf
705
706     @classmethod
707     def from_cnf_string(cls, data):
708         """
709         Create an instance of this class from a cnfvar string.
710
711         :param str data: cnfvar string to convert
712         :returns: the cnf instance created
713         :rtype: :py:class:`Cnf`
714         """
715         return CnfListSerializationMixin.from_cnf_string(data).single()
716
717     @classmethod
718     def from_json_string(cls, data):
719         """
720         Create an instance of this class from a JSON string.
721
722         :param str data: JSON string to convert
723         :returns: the cnf instance created
724         :rtype: :py:class:`Cnf`
725         """
726         return CnfListSerializationMixin.from_json_string(data).single()
727
728     @classmethod
729     def from_cnf_file(cls, path, encoding=ENCODING):
730         """
731         Create an instance of this class from a cnfvar file.
732
733         :param str path: path to the file to read
734         :param str encoding: encoding to use to read the file
735         :returns: the cnf instance created
736         :rtype: :py:class:`Cnf`
737         """
738         return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single()
739
740     @classmethod
741     def from_json_file(cls, path):
742         """
743         Create an instance of this class from a json file.
744
745         :param str path: path to the file to read
746         :returns: the cnf instance created
747         :rtype: :py:class:`Cnf`
748         """
749         return CnfListSerializationMixin.from_json_file(path).single()
750
751
752 class CnfListArniedApiMixin(BaseCnfList):
753     """Add support for converting this class to and from Arnied API classes."""
754
755     def to_api_structure(self):
756         """
757         Convert this list to the corresponding object in the arnied API.
758
759         :returns: the converted object
760         :rtype: [:py:class:`arnied_api.CnfVar`]
761         """
762         return [c.to_api_structure() for c in self]
763
764     @classmethod
765     def from_api_structure(cls, cnfvar_list):
766         """
767         Convert a list from the arnied API into a list of this type.
768
769         :param cnfvar_list: list to convert
770         :type cnfvar_list: [:py:class:`arnied_api.CnfVar`]
771         :returns: the list created
772         :rtype: :py:class:`CnfList`
773         """
774         return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list),
775                        renumber=True)
776
777
778 class CnfArniedApiMixin(BaseCnf):
779     """Add support for converting this class to and from Arnied API classes."""
780
781     def to_api_structure(self):
782         """
783         Convert this instance to the corresponding object in the arnied API.
784
785         :returns: the converted object
786         :rtype: :py:class:`arnied_api.CnfVar`
787         """
788         return arnied_api.CnfVar(
789             self.name.upper(),
790             self.instance,
791             self.value,
792             False,  # default here to False
793             children=[c.to_api_structure() for c in self.children])
794
795     @classmethod
796     def from_api_structure(cls, cnfobj):
797         """
798         Convert an object from the arnied API into an instance of this type.
799
800         :param cnfobj: object to convert
801         :type cnfobj: :py:class:`arnied_api.CnfVar`
802         :returns: the instance created
803         :rtype: :py:class:`Cnf`
804         """
805         cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance)
806         children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children))
807         for c in children:
808             c.parent = cnf
809         cnf.children.extend(children)
810         return cnf
811
812
813 class CnfShortcutsMixin(BaseCnf):
814     """Extend the base CNF class with useful methods."""
815
816     def enable(self):
817         """Treat this variable as a boolean var and set its value to 1."""
818         self.value = "1"
819
820     def disable(self):
821         """Treat this variable as a boolean var and set its value to 0."""
822         self.value = "0"
823
824     def is_enabled(self):
825         """Treat this variable as a boolean var and check if its value is 1."""
826         return self.value == "1"
827
828     def enable_child_flag(self, name):
829         """
830         Set the value of the child CNF matching `name` to "1".
831
832         :param str name: name of the child whose value to enable
833
834         .. note:: child will be created if it does not exist.
835         """
836         cnf = self.children.first_with_name(name, default=None)
837         if cnf is None:
838             self.add_child(name, "1")
839         else:
840             cnf.enable()
841
842     def disable_child_flag(self, name):
843         """
844         Set the value of the child CNF matching `name` to "0".
845
846         :param str name: name of the child whose value to disable
847
848         .. note:: child will be created if it does not exist.
849         """
850         cnf = self.children.first_with_name(name, default=None)
851         if cnf is None:
852             self.add_child(name, "0")
853         else:
854             cnf.disable()
855
856     def child_flag_enabled(self, name):
857         """
858         Check if a given child has a value equal to `1`.
859
860         :param str name: name of the child to check
861         :returns: whether the value of the given child, if it exists, is 1
862         :rtype: bool
863         """
864         cnf = self.children.first_with_name(name, default=None)
865         return cnf.is_enabled() if cnf is not None else False
866
867
868 class CnfListQueryingMixin(BaseCnfList):
869     """Mixing adding shortcuts for common filter operations."""
870
871     def single(self, where_filter=None, default=DEFAULT):
872         """
873         Get the only CNF of this list or raise if none or more than one exist.
874
875         :param where_filter: predicate to apply against CNFs beforehand
876         :type where_filter: function accepting a CNF and returning a boolean
877         :param default: value to return in case the list is empty
878         :type default: any
879         :raises: :py:class:`ValueError` if a single value cannot be found and
880                  a default value was not specified
881         :returns: the first and only element of this list, or default if set
882                   and no element is present
883         :rtype: :py:class:`Cnf`
884         """
885         list_ = self.where(where_filter) if where_filter is not None else self
886
887         if len(list_) == 1:
888             return list_[0]
889         elif len(list_) == 0 and default != DEFAULT:
890             return default
891         else:
892             raise ValueError(f"CnfList does not contain a single item (len={len(list_)})")
893
894     def first(self, where_filter=None, default=DEFAULT):
895         """
896         Get the first element in this list or raise if the list is empty.
897
898         :param where_filter: predicate to apply against CNFs beforehand
899         :type where_filter: function accepting a CNF and returning a boolean
900         :param default: value to return in case the list is empty
901         :type default: any
902         :raises: :py:class:`ValueError` if a single value cannot be found and
903                  a default value was not specified
904         :returns: the first element of this list, or default if set and
905                   no element is present
906         :rtype: :py:class:`Cnf`
907         """
908         list_ = self.where(where_filter) if where_filter is not None else self
909         if len(list_) > 0:
910             return list_[0]
911         elif default != DEFAULT:
912             return default
913         else:
914             raise ValueError("Cannot get the first item - CnfList is empty")
915
916     def with_value(self, value):
917         """Shortcut method for filtering by value."""
918         return self.where(lambda c: c.value == value)
919
920     def with_name(self, name):
921         """Shortcut method for filtering by name."""
922         return self.where(lambda c: c.name == name)
923
924     def with_instance(self, instance):
925         """Shortcut method for filtering by instance."""
926         return self.where(lambda c: c.instance == instance)
927
928     def single_with_name(self, name, default=DEFAULT):
929         """Shortcut method for getting the single item with a given name."""
930         return self.with_name(name).single(default=default)
931
932     def single_with_value(self, value, default=DEFAULT):
933         """Shortcut method for getting the single item with a given value."""
934         return self.with_value(value).single(default=default)
935
936     def single_with_instance(self, instance, default=DEFAULT):
937         """Shortcut method for getting the single item with a given instance."""
938         return self.with_instance(instance).single(default=default)
939
940     def first_with_name(self, name, default=DEFAULT):
941         """Shortcut method for getting the first item with a given name."""
942         return self.with_name(name).first(default=default)
943
944     def first_with_value(self, value, default=DEFAULT):
945         """Shortcut method for getting the first item with a given value."""
946         return self.with_value(value).first(default=default)
947
948     def first_with_instance(self, instance, default=DEFAULT):
949         """Shortcut method for getting the first item with a given instance."""
950         return self.with_instance(instance).first(default=default)
951
952     def highest_instance(self):
953         """Shortcut method for getting the next instance in a list of items."""
954         return max([c.instance for c in self]) if len(self) > 0 else -1
955
956
957 ###############################################################################
958 # PUBLIC CLASSES
959 ###############################################################################
960 #
961 # Set up the classes with the mixins we want to be available by default.
962 #
963
964
965 class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
966     """Collection of Cnf variables."""
967
968     pass
969
970
971 class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
972     """Class representing a cnfvar."""
973
974     pass
975
976
977 __all__ = ["CnfList", "Cnf"]