Deprecate "to" and privatize "from" cnf structure methods
[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
b0075fa2
PD
38from . import string
39from .. import arnied_api
d31714a0
SA
40
41#: value used to detect unspecified arguments
42DEFAULT = object()
43#: encoding used by the get_cnf and set_cnf binaries
44ENCODING = "latin1"
45
46
47###############################################################################
48# HELPERS
49###############################################################################
50
51
52class 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
d5d2c1d7
CH
69 def startswith(self, prefix, *args, **kwargs):
70 return self.lower().startswith(prefix.lower(), *args, **kwargs)
d31714a0 71
6cee0cd4
PD
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)
d31714a0
SA
77
78
79###############################################################################
80# BASE API
81###############################################################################
82
83
84class 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_)
d5d2c1d7 112 self._renumber_counter = None # initialized and used in renumber
d31714a0
SA
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.
d5d2c1d7 121 self._renumber_counter = 0
d31714a0
SA
122
123 def renumber_fn(cnf):
d5d2c1d7
CH
124 self._renumber_counter += 1
125 cnf.lineno = self._renumber_counter
d31714a0
SA
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
d5d2c1d7 135 :returns: an instance of this class with filtered members
d31714a0
SA
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
d5d2c1d7 146 :returns: an instance of this class with filtered members
d31714a0
SA
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
bee1c853
PD
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
d31714a0
SA
266
267class 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
c89cc126 274 def __init__(self, name, value, instance=0, parent=None,
d31714a0
SA
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]
bee1c853 363 assert isinstance(cnf, Cnf), "A Cnf instance is mandatory with one argument"
d31714a0
SA
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
508class CnfListSerializationMixin(BaseCnfList):
509 """Add serialization support to BaseCnfList."""
510
d31714a0
SA
511 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
512 """
513 Dump a string representation of this list in the cnfvar format to a file.
514
515 :param str path: path to the file to write to
516 :param bool renumber: whether to fix the lineno of the cnfvars
517 :param str encoding: encoding to use to save the file
518 """
519 if renumber:
520 self.renumber()
521 with open(path, "w", encoding=encoding) as fp:
522 fp.write(str(self))
523
524 def to_json_string(self, renumber=True):
525 """
526 Generate a JSON representation of this list in the cnfvar format.
527
528 :param bool renumber: whether to fix the lineno of the cnfvars
529 :returns: the JSON string
530 :rtype: str
531 """
532 def _to_dict(cnf):
533 d = {
534 "number": cnf.lineno,
535 "varname": cnf.name,
536 "data": cnf.value,
537 "instance": cnf.instance
538 }
539 if cnf.parent and cnf.parent.lineno:
540 d["parent"] = cnf.parent.lineno
541 if cnf.comment is not None:
542 d["comment"] = cnf.comment
543 if len(cnf.children) > 0:
544 d["children"] = [_to_dict(c) for c in cnf.children]
545 return d
546 if renumber:
d5d2c1d7 547 self.renumber()
d31714a0
SA
548 json_list = [_to_dict(c) for c in self]
549 return json.dumps({"cnf": json_list})
550
551 def to_json_file(self, path, renumber=True):
552 """
553 Dump a JSON representation of this list to a file.
554
555 :param str path: path to the file to write to
556 :param bool renumber: whether to fix the lineno of the cnfvars
557 """
558 with open(path, "w", encoding="utf8") as fp:
559 fp.write(self.to_json_string(renumber=renumber))
560
561 @classmethod
37d7c300 562 def _from_cnf_structure(cls, obj):
d31714a0 563 """
37d7c300 564 Create a list from a JSON structure obtainable from `get_cnf --json`.
d31714a0
SA
565
566 :param obj: an object as defined in the :py:mod:`cnfvar`
567 :type obj: {str, {str, str or int}}
568 :returns: a list of cnfvars
569 :rtype: :py:class:`CnfList`
570 """
37d7c300 571 return cls(map(Cnf._from_cnf_structure, obj["cnf"]))
d31714a0
SA
572
573 @classmethod
574 def from_cnf_string(cls, data):
575 """
576 Create a list from a cnfvar string.
577
578 :param str data: string to generate the list from
579 :returns: a list of cnfvars
580 :rtype: :py:class:`CnfList`
581 """
b0075fa2 582 cnf_obj = string.read_cnf(data)
37d7c300 583 return CnfList._from_cnf_structure(cnf_obj)
d31714a0
SA
584
585 @classmethod
586 def from_json_string(cls, data):
587 """
588 Create a list from a json 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 = json.loads(data)
37d7c300 595 return CnfList._from_cnf_structure(cnf_obj)
d31714a0
SA
596
597 @classmethod
598 def from_cnf_file(cls, path, encoding=ENCODING):
599 """
600 Create a list from a cnfvar file.
601
602 :param str path: path to the file to read
603 :param str encoding: encoding to use to open the file (defaults to
604 latin1 as this is the default encoding)
605 :returns: a list of cnfvars
606 :rtype: :py:class:`CnfList`
607 """
608 with open(path, "r", encoding=encoding) as fp:
609 return CnfList.from_cnf_string(fp.read())
610
611 @classmethod
612 def from_json_file(cls, path):
613 """
614 Create a list from a json file.
615
616 :param str path: path to the file to read
617 :returns: a list of cnfvars
618 :rtype: :py:class:`CnfList`
619 """
620 with open(path, "r", encoding="utf8") as fp:
621 return CnfList.from_json_string(fp.read())
622
623
624class CnfSerializationMixin(BaseCnf):
625 """Add serialization support to BaseCnf."""
626
d31714a0
SA
627 def to_json_string(self, renumber=True):
628 """
629 Convert this instance to a JSON string.
630
631 :param bool renumber: whether to fix the lineno of the cnfvars
632 :returns: the JSON string
633 :rtype: str
634 """
635 return CnfList([self]).to_json_string(renumber=renumber)
636
637 def to_cnf_file(self, path, renumber=True, encoding=ENCODING):
638 """
639 Dump a string representation of this instance to a file.
640
641 :param str path: path to the file to write to
642 :param bool renumber: whether to fix the lineno of this cnfvar and its children
643 :param str encoding: encoding to use to save the file
644 """
645 CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding)
646
647 def to_json_file(self, path, renumber=True):
648 """
649 Dump a JSON representation of this instance to a file.
650
651 :param str path: path to the file to write to
652 :param bool renumber: whether to fix the lineno of this cnfvar and its children
653 """
654 CnfList([self]).to_json_file(path, renumber=renumber)
655
656 @classmethod
37d7c300 657 def _from_cnf_structure(cls, obj):
d31714a0 658 """
37d7c300 659 Create an instance from a JSON structure obtainable from `get_cnf --json`.
d31714a0
SA
660
661 :param obj: dictionary to convert to this instance
662 :type obj: {str, str or int}
663 :returns: the cnf instance created
664 :rtype: :py:class:`Cnf`
665 """
666 cnf = Cnf(obj["varname"], obj["data"],
667 instance=obj["instance"], lineno=obj["number"],
668 comment=obj.get("comment", None))
669 for ch_obj in obj.get("children", []):
37d7c300 670 child_cnf = Cnf._from_cnf_structure(ch_obj)
d31714a0
SA
671 cnf.add_child(child_cnf)
672 return cnf
673
674 @classmethod
675 def from_cnf_string(cls, data):
676 """
677 Create an instance of this class from a cnfvar string.
678
679 :param str data: cnfvar string to convert
680 :returns: the cnf instance created
681 :rtype: :py:class:`Cnf`
682 """
683 return CnfListSerializationMixin.from_cnf_string(data).single()
684
685 @classmethod
686 def from_json_string(cls, data):
687 """
688 Create an instance of this class from a JSON string.
689
690 :param str data: JSON string to convert
691 :returns: the cnf instance created
692 :rtype: :py:class:`Cnf`
693 """
694 return CnfListSerializationMixin.from_json_string(data).single()
695
696 @classmethod
697 def from_cnf_file(cls, path, encoding=ENCODING):
698 """
699 Create an instance of this class from a cnfvar file.
700
701 :param str path: path to the file to read
702 :param str encoding: encoding to use to read the file
703 :returns: the cnf instance created
704 :rtype: :py:class:`Cnf`
705 """
706 return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single()
707
708 @classmethod
709 def from_json_file(cls, path):
710 """
711 Create an instance of this class from a json file.
712
713 :param str path: path to the file to read
714 :returns: the cnf instance created
715 :rtype: :py:class:`Cnf`
716 """
717 return CnfListSerializationMixin.from_json_file(path).single()
718
719
720class CnfListArniedApiMixin(BaseCnfList):
721 """Add support for converting this class to and from Arnied API classes."""
722
723 def to_api_structure(self):
724 """
725 Convert this list to the corresponding object in the arnied API.
726
727 :returns: the converted object
728 :rtype: [:py:class:`arnied_api.CnfVar`]
729 """
730 return [c.to_api_structure() for c in self]
731
732 @classmethod
733 def from_api_structure(cls, cnfvar_list):
734 """
735 Convert a list from the arnied API into a list of this type.
736
737 :param cnfvar_list: list to convert
738 :type cnfvar_list: [:py:class:`arnied_api.CnfVar`]
739 :returns: the list created
740 :rtype: :py:class:`CnfList`
741 """
742 return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list),
743 renumber=True)
744
745
746class CnfArniedApiMixin(BaseCnf):
747 """Add support for converting this class to and from Arnied API classes."""
748
749 def to_api_structure(self):
750 """
751 Convert this instance to the corresponding object in the arnied API.
752
753 :returns: the converted object
754 :rtype: :py:class:`arnied_api.CnfVar`
755 """
756 return arnied_api.CnfVar(
757 self.name.upper(),
758 self.instance,
759 self.value,
760 False, # default here to False
761 children=[c.to_api_structure() for c in self.children])
762
763 @classmethod
764 def from_api_structure(cls, cnfobj):
765 """
766 Convert an object from the arnied API into an instance of this type.
767
768 :param cnfobj: object to convert
769 :type cnfobj: :py:class:`arnied_api.CnfVar`
770 :returns: the instance created
771 :rtype: :py:class:`Cnf`
772 """
773 cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance)
774 children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children))
775 for c in children:
776 c.parent = cnf
777 cnf.children.extend(children)
778 return cnf
779
780
781class CnfShortcutsMixin(BaseCnf):
782 """Extend the base CNF class with useful methods."""
783
784 def enable(self):
785 """Treat this variable as a boolean var and set its value to 1."""
786 self.value = "1"
787
788 def disable(self):
789 """Treat this variable as a boolean var and set its value to 0."""
790 self.value = "0"
791
792 def is_enabled(self):
793 """Treat this variable as a boolean var and check if its value is 1."""
794 return self.value == "1"
795
796 def enable_child_flag(self, name):
797 """
798 Set the value of the child CNF matching `name` to "1".
799
800 :param str name: name of the child whose value to enable
801
802 .. note:: child will be created if it does not exist.
803 """
804 cnf = self.children.first_with_name(name, default=None)
805 if cnf is None:
d5d2c1d7 806 self.add_child(name, "1")
d31714a0
SA
807 else:
808 cnf.enable()
809
810 def disable_child_flag(self, name):
811 """
812 Set the value of the child CNF matching `name` to "0".
813
814 :param str name: name of the child whose value to disable
815
816 .. note:: child will be created if it does not exist.
817 """
818 cnf = self.children.first_with_name(name, default=None)
819 if cnf is None:
d5d2c1d7 820 self.add_child(name, "0")
d31714a0
SA
821 else:
822 cnf.disable()
823
824 def child_flag_enabled(self, name):
825 """
826 Check if a given child has a value equal to `1`.
827
828 :param str name: name of the child to check
829 :returns: whether the value of the given child, if it exists, is 1
830 :rtype: bool
831 """
832 cnf = self.children.first_with_name(name, default=None)
833 return cnf.is_enabled() if cnf is not None else False
834
835
836class CnfListQueryingMixin(BaseCnfList):
837 """Mixing adding shortcuts for common filter operations."""
838
839 def single(self, where_filter=None, default=DEFAULT):
840 """
841 Get the only CNF of this list or raise if none or more than one exist.
842
843 :param where_filter: predicate to apply against CNFs beforehand
844 :type where_filter: function accepting a CNF and returning a boolean
845 :param default: value to return in case the list is empty
846 :type default: any
847 :raises: :py:class:`ValueError` if a single value cannot be found and
848 a default value was not specified
849 :returns: the first and only element of this list, or default if set
850 and no element is present
851 :rtype: :py:class:`Cnf`
852 """
853 list_ = self.where(where_filter) if where_filter is not None else self
854
855 if len(list_) == 1:
856 return list_[0]
857 elif len(list_) == 0 and default != DEFAULT:
858 return default
859 else:
860 raise ValueError(f"CnfList does not contain a single item (len={len(list_)})")
861
862 def first(self, where_filter=None, default=DEFAULT):
863 """
864 Get the first element in this list or raise if the list is empty.
865
866 :param where_filter: predicate to apply against CNFs beforehand
867 :type where_filter: function accepting a CNF and returning a boolean
868 :param default: value to return in case the list is empty
869 :type default: any
870 :raises: :py:class:`ValueError` if a single value cannot be found and
871 a default value was not specified
872 :returns: the first element of this list, or default if set and
873 no element is present
874 :rtype: :py:class:`Cnf`
875 """
876 list_ = self.where(where_filter) if where_filter is not None else self
877 if len(list_) > 0:
878 return list_[0]
879 elif default != DEFAULT:
880 return default
881 else:
882 raise ValueError("Cannot get the first item - CnfList is empty")
883
884 def with_value(self, value):
885 """Shortcut method for filtering by value."""
886 return self.where(lambda c: c.value == value)
887
888 def with_name(self, name):
889 """Shortcut method for filtering by name."""
890 return self.where(lambda c: c.name == name)
891
892 def with_instance(self, instance):
893 """Shortcut method for filtering by instance."""
894 return self.where(lambda c: c.instance == instance)
895
896 def single_with_name(self, name, default=DEFAULT):
897 """Shortcut method for getting the single item with a given name."""
898 return self.with_name(name).single(default=default)
899
900 def single_with_value(self, value, default=DEFAULT):
901 """Shortcut method for getting the single item with a given value."""
902 return self.with_value(value).single(default=default)
903
904 def single_with_instance(self, instance, default=DEFAULT):
905 """Shortcut method for getting the single item with a given instance."""
906 return self.with_instance(instance).single(default=default)
907
908 def first_with_name(self, name, default=DEFAULT):
909 """Shortcut method for getting the first item with a given name."""
910 return self.with_name(name).first(default=default)
911
912 def first_with_value(self, value, default=DEFAULT):
913 """Shortcut method for getting the first item with a given value."""
914 return self.with_value(value).first(default=default)
915
916 def first_with_instance(self, instance, default=DEFAULT):
917 """Shortcut method for getting the first item with a given instance."""
918 return self.with_instance(instance).first(default=default)
919
cdfd8f20
PD
920 def highest_instance(self):
921 """Shortcut method for getting the next instance in a list of items."""
922 return max([c.instance for c in self]) if len(self) > 0 else -1
923
d31714a0
SA
924
925###############################################################################
926# PUBLIC CLASSES
927###############################################################################
928#
929# Set up the classes with the mixins we want to be available by default.
930#
931
932
933class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
934 """Collection of Cnf variables."""
935
936 pass
937
938
939class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin):
940 """Class representing a cnfvar."""
941
942 pass
943
944
945__all__ = ["CnfList", "Cnf"]