Implement add() to add cnfvars to cnfvar lists directly from tuples
[pyi2ncommon] / src / cnfvar / model.py
CommitLineData
d31714a0
SA
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"""
22model: Cnf classes, collection of Cnf classes and multiple filtering methods.
23
d5d2c1d7
CH
24Featuring:
25 - Cnf: class representing a CNF variable
26 - CnfList: a collection of `Cnf` instances
d31714a0
SA
27
28The classes above inherit from their base types with added mixins which
29extend them with extra functionality.
30
d5d2c1d7
CH
31.. seealso:: Overview Diagram linked to from doc main page
32
d31714a0
SA
33.. codeauthor:: Intra2net
34"""
35
36import json
37
38from .. import cnfvar_old, arnied_api
39
40#: value used to detect unspecified arguments
41DEFAULT = object()
42#: encoding used by the get_cnf and set_cnf binaries
43ENCODING = "latin1"
44
45
46###############################################################################
47# HELPERS
48###############################################################################
49
50
51class 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
d5d2c1d7
CH
68 def startswith(self, prefix, *args, **kwargs):
69 return self.lower().startswith(prefix.lower(), *args, **kwargs)
d31714a0 70
d5d2c1d7
CH
71 def endswith(self, prefix, *args, **kwargs):
72 return self.lower().endswith(prefix.lower(), *args, **kwargs)
d31714a0
SA
73
74
75###############################################################################
76# BASE API
77###############################################################################
78
79
80class 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_)
d5d2c1d7 108 self._renumber_counter = None # initialized and used in renumber
d31714a0
SA
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.
d5d2c1d7 117 self._renumber_counter = 0
d31714a0
SA
118
119 def renumber_fn(cnf):
d5d2c1d7
CH
120 self._renumber_counter += 1
121 cnf.lineno = self._renumber_counter
d31714a0
SA
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
d5d2c1d7 131 :returns: an instance of this class with filtered members
d31714a0
SA
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
d5d2c1d7 142 :returns: an instance of this class with filtered members
d31714a0
SA
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
bee1c853
PD
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
d31714a0
SA
262
263class 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
c89cc126 270 def __init__(self, name, value, instance=0, parent=None,
d31714a0
SA
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]
bee1c853 359 assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
d31714a0
SA
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
504class 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()
42c1b1d1 517 return {"cnf": [x.to_cnf_structure() for x in self]}
d31714a0
SA
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:
d5d2c1d7 555 self.renumber()
d31714a0
SA
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
632class CnfSerializationMixin(BaseCnf):
633 """Add serialization support to BaseCnf."""
634
42c1b1d1 635 def to_cnf_structure(self):
d31714a0
SA
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:
42c1b1d1 656 d["children"] = [c.to_cnf_structure() for c in self.children]
d31714a0
SA
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
752class 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
778class 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
813class 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:
d5d2c1d7 838 self.add_child(name, "1")
d31714a0
SA
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:
d5d2c1d7 852 self.add_child(name, "0")
d31714a0
SA
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
868class 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
cdfd8f20
PD
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
d31714a0
SA
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
965class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
966 """Collection of Cnf variables."""
967
968 pass
969
970
971class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
972 """Class representing a cnfvar."""
973
974 pass
975
976
977__all__ = ["CnfList", "Cnf"]