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