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