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