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