Commit | Line | Data |
---|---|---|
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 | """ | |
22 | model: Cnf classes, collection of Cnf classes and multiple filtering methods. | |
23 | ||
d5d2c1d7 CH |
24 | Featuring: |
25 | - Cnf: class representing a CNF variable | |
26 | - CnfList: a collection of `Cnf` instances | |
d31714a0 SA |
27 | |
28 | The classes above inherit from their base types with added mixins which | |
29 | extend 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 | ||
36 | import json | |
37 | ||
b0075fa2 PD |
38 | from . import string |
39 | from .. import arnied_api | |
d31714a0 SA |
40 | |
41 | #: value used to detect unspecified arguments | |
42 | DEFAULT = object() | |
43 | #: encoding used by the get_cnf and set_cnf binaries | |
44 | ENCODING = "latin1" | |
45 | ||
46 | ||
47 | ############################################################################### | |
48 | # HELPERS | |
49 | ############################################################################### | |
50 | ||
51 | ||
52 | class CnfName(str): | |
53 | """ | |
54 | Custom string where comparisons are case-insensitive. | |
55 | ||
56 | With this class we do not have to worry about case when comparing against | |
57 | the name of cnfvars when filtering. The cnfvar backend is already case- | |
58 | insensitive anyway. | |
59 | """ | |
60 | ||
61 | def __eq__(self, other): | |
62 | if not isinstance(other, str): | |
63 | return False | |
64 | return self.lower() == other.lower() | |
65 | ||
66 | def __contains__(self, name): | |
67 | return name.lower() in self.lower() | |
68 | ||
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 | ||
84 | class BaseCnfList(list): | |
85 | """Base class representing a CNF list with minimal functionality.""" | |
86 | ||
87 | def __init__(self, cnf_iter=None, renumber=False): | |
88 | """ | |
89 | Class constructor. | |
90 | ||
91 | :param cnf_iter: iterator producing CNF elements or arguments for the | |
92 | constructor of the :py:class:`Cnf` class | |
93 | :type: :py:class:`collections.abc.Iterator` producing elements of type | |
94 | :py:class:`Cnf` | |
95 | :param bool renumber: whether to fix up the number/ids of the CNFs | |
96 | ||
97 | Example:: | |
98 | cnf = Cnf("my_cnf", "value") | |
99 | cnf_list = CnfList([ | |
100 | cnf, | |
101 | ("other_cnf", "other value"), | |
102 | ("user", "john", instance=3) | |
103 | ]) | |
104 | """ | |
105 | # Map the values of the iterator to support constructing this list | |
106 | # from Cnf instances or arguments to the Cnf constructor | |
107 | if cnf_iter is not None: | |
108 | iter_ = map(lambda c: c if isinstance(c, Cnf) else Cnf(*c), cnf_iter) | |
109 | else: | |
110 | iter_ = [] | |
111 | super().__init__(iter_) | |
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 | |
267 | class BaseCnf: | |
268 | """Base class representing a CNF variable with minimal functionality.""" | |
269 | ||
270 | _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\"" | |
271 | _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\"" | |
272 | _NEST_INDENT = " " | |
273 | ||
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 | ||
508 | class 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 | ||
624 | class 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 | ||
720 | class 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 | ||
746 | class 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 | ||
781 | class 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 | ||
836 | class 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 | ||
933 | class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin): | |
934 | """Collection of Cnf variables.""" | |
935 | ||
936 | pass | |
937 | ||
938 | ||
939 | class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin): | |
940 | """Class representing a cnfvar.""" | |
941 | ||
942 | pass | |
943 | ||
944 | ||
945 | __all__ = ["CnfList", "Cnf"] |