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 | ||
241 | ||
242 | class BaseCnf: | |
243 | """Base class representing a CNF variable with minimal functionality.""" | |
244 | ||
245 | _PARENT_TEMPLATE = "{lineno} {name},{instance}: \"{value}\"" | |
246 | _CHILD_TEMPLATE = "{lineno} {indent}({parent}) {name},{instance}: \"{value}\"" | |
247 | _NEST_INDENT = " " | |
248 | ||
c89cc126 | 249 | def __init__(self, name, value, instance=0, parent=None, |
d31714a0 SA |
250 | lineno=None, comment=None): |
251 | """ | |
252 | Create this instance. | |
253 | ||
254 | :param str name: name of the cnfvar (case does not matter) | |
255 | :param str value: value for this cnfvar (will be converted to string | |
256 | if it is not of this type) | |
257 | :param int instance: instance of this cnfvar | |
258 | :param parent: a parent Cnf instance | |
259 | :type parent: :py:class:`BaseCnf` | |
260 | :param int lineno: line number | |
261 | :param str comment: cnfvar comment | |
262 | """ | |
263 | self.name = CnfName(name) | |
264 | self.value = value | |
265 | self.instance = int(instance) | |
266 | self.parent = parent | |
267 | self.lineno = int(lineno or 0) | |
268 | self.comment = comment | |
269 | self.__children = CnfList() | |
270 | ||
271 | # Use getters and setters to keep internal consistency and fail-fast | |
272 | # preventing invalid data from being sent to the cnfvar backend. | |
273 | ||
274 | def _get_name(self): | |
275 | return self.__name | |
276 | ||
277 | def _set_name(self, value): | |
278 | # convert Python strings passed as name to our custom string | |
279 | self.__name = CnfName(value) | |
280 | name = property(_get_name, _set_name) | |
281 | ||
282 | def _get_instance(self): | |
283 | return self.__instance | |
284 | ||
285 | def _set_instance(self, value): | |
286 | # fail-fast and make sure instance is a valid integer | |
287 | self.__instance = int(value) | |
288 | instance = property(_get_instance, _set_instance) | |
289 | ||
290 | def _get_lineno(self): | |
291 | return self.__lineno | |
292 | ||
293 | def _set_lineno(self, value): | |
294 | # fail-fast and make sure lineno is a valid integer | |
295 | self.__lineno = int(value) | |
296 | lineno = property(_get_lineno, _set_lineno) | |
297 | ||
298 | def _get_children(self): | |
299 | return self.__children | |
300 | # No setter to sure that the children property will not | |
301 | # be replaced by something other than a `CnfList` | |
302 | children = property(_get_children) | |
303 | ||
304 | def _get_value(self): | |
305 | return self.__value | |
306 | ||
307 | def _set_value(self, value): | |
308 | # Make sure the value is always stored as a string | |
309 | # (no other types make sense to the cnfvar backend) | |
310 | self.__value = str(value) | |
311 | value = property(_get_value, _set_value) | |
312 | ||
313 | def add_child(self, *args, **kwargs): | |
314 | """ | |
315 | Add a child CNF variable. | |
316 | ||
317 | Arguments can either be a single instance of the :py:class:`Cnf` | |
318 | class or a list of arguments to be passed to the constructor of | |
319 | that class. | |
320 | ||
321 | :returns: the instance that was created | |
322 | :rtype: :py:class:`Cnf` | |
323 | ||
324 | Example:: | |
325 | ||
326 | cnf = Cnf("my_parent_cnf", "parent") | |
327 | cnf2 = Cnf("my_child_cnf", "john") | |
328 | ||
329 | # adding a child as a CNF instance | |
330 | cnf.add_child(cnf2) | |
331 | ||
332 | # adding a child passing arguments of the Cnf constructor | |
333 | cnf.add_child("my_child_cnf", "jane", instance=2) | |
334 | """ | |
335 | # support passing a Cnf instance | |
336 | if len(args) == 1 and not kwargs: | |
337 | cnf = args[0] | |
338 | assert isinstance(cnf, Cnf), \ | |
339 | "With one argument, a Cnf instance is mandatory" | |
340 | else: | |
341 | cnf = Cnf(*args, **kwargs) | |
342 | ||
343 | # store a reference to parent to easily access it | |
344 | cnf.parent = self | |
345 | ||
346 | # It seems the CNF backend (at least using set_cnf as opposed to the varlink | |
347 | # API) only accepts instance with value of -1 for top-level variables, so | |
348 | # just in case fix up instances when adding children with the default value. | |
349 | if cnf.instance == -1: | |
350 | cnf.instance = 0 | |
351 | for c in self.children: | |
352 | if c.name == cnf.name: | |
353 | cnf.instance += 1 | |
354 | ||
355 | self.children.append(cnf) | |
356 | return cnf | |
357 | ||
358 | def add_children(self, *children): | |
359 | """ | |
360 | Add multiple child CNF variables. | |
361 | ||
362 | Each argument must be either an instance of the :py:class:`Cnf` class | |
363 | or a tuple/list to be expanded and passed to construct that instance. | |
364 | ||
365 | :returns: a list of the instances that were created | |
366 | :rtype: :py:class:`CnfList` | |
367 | ||
368 | Example:: | |
369 | cnf = Cnf("my_parent_cnf", "parent") | |
370 | cnf2 = Cnf("my_child_cnf", "john") | |
371 | ||
372 | cnf.add_children( | |
373 | cnf2, # cnf instance directly | |
374 | ("my_child_cnf", "jane", instance=2), # pass a tuple with args | |
375 | ["my_child_cnf", "jack", instance=3]) # pass a list with args | |
376 | ||
377 | # adding a child passing arguments of the Cnf constructor | |
378 | cnf.add_child("my_child_cnf", "jane", instance=2) | |
379 | """ | |
380 | added_children = CnfList() | |
381 | for c in children: | |
382 | if isinstance(c, Cnf): | |
383 | new_child = self.add_child(c) | |
384 | elif isinstance(c, tuple) or isinstance(c, list): | |
385 | new_child = self.add_child(*c) | |
386 | else: | |
387 | raise ValueError(f"Children item {c} must be either a Cnf, a tuple or a list") | |
388 | added_children.append(new_child) | |
389 | return added_children | |
390 | ||
391 | def __eq__(self, other): | |
392 | """ | |
393 | Equality implementation. | |
394 | ||
395 | :param other: object to compare this instance against | |
396 | :type other: any | |
397 | :returns: whether `other` is equal to this instance | |
398 | :rtype: bool | |
399 | ||
400 | This is particularly useful when comparing instances of | |
401 | :py:class:`CnfList` | |
402 | """ | |
403 | if not isinstance(other, Cnf): | |
404 | return False | |
405 | ||
406 | # NOTE: we try to define two variables as equal in the same way as the | |
407 | # set_cnf binary would if we were passing it an updated CNF variable. | |
408 | # It does not take comments, children and lineno into account when we | |
409 | # pass it a variable; it will rather compare the data we compare here, | |
410 | # and if it finds a match it will update it with the changed children. | |
411 | return self.name == other.name \ | |
412 | and self.value == other.value \ | |
413 | and self.instance == other.instance \ | |
414 | and self.parent == other.parent | |
415 | ||
416 | def __str__(self): | |
417 | """ | |
418 | Get a string representation of this instance. | |
419 | ||
420 | :returns: a string in the cnfvar format | |
421 | :rtype: str | |
422 | """ | |
423 | if self.parent is None: | |
424 | this_str = self._PARENT_TEMPLATE.format( | |
425 | lineno=self.lineno, | |
426 | name=self.name.upper(), | |
427 | instance=self.instance, | |
428 | value=self.value | |
429 | ) | |
430 | else: | |
431 | depth = 0 | |
432 | curr = self | |
433 | while curr.parent is not None: | |
434 | depth += 1 | |
435 | curr = curr.parent | |
436 | ||
437 | this_str = self._CHILD_TEMPLATE.format( | |
438 | lineno=self.lineno, | |
439 | indent=self._NEST_INDENT * depth, | |
440 | parent=self.parent.lineno, | |
441 | name=self.name.upper(), | |
442 | instance=self.instance, | |
443 | value=self.value | |
444 | ) | |
445 | ||
446 | if self.comment is not None: | |
447 | this_str += f" # {self.comment}" | |
448 | ||
449 | for child in self.children: | |
450 | this_str += f"\n{child}" | |
451 | ||
452 | return this_str | |
453 | ||
454 | def __repr__(self): | |
455 | """ | |
456 | Get a printable representation of this instance. | |
457 | ||
458 | :returns: a string in the cnfvar format | |
459 | :rtype: str | |
460 | """ | |
461 | repr_ = self._PARENT_TEMPLATE.format( | |
462 | lineno=self.lineno, | |
463 | name=self.name.upper(), | |
464 | instance=self.instance, | |
465 | value=self.value | |
466 | ) if self.parent is None else self._CHILD_TEMPLATE.format( | |
467 | lineno=self.lineno, | |
468 | indent="", | |
469 | parent=self.parent.lineno, | |
470 | name=self.name.upper(), | |
471 | instance=self.instance, | |
472 | value=self.value | |
473 | ) | |
474 | return f"Cnf{{ {repr_} [children={len(self.children)}] }}" | |
475 | ||
476 | ||
477 | ############################################################################### | |
478 | # MIXINS | |
479 | ############################################################################### | |
480 | # | |
481 | # These mixins add functionality to the base API without polluting it. | |
482 | # | |
483 | ||
484 | class CnfListSerializationMixin(BaseCnfList): | |
485 | """Add serialization support to BaseCnfList.""" | |
486 | ||
487 | def to_cnf_structure(self, renumber=True): | |
488 | """ | |
489 | Convert this list to an object meaningful to :py:mod:`cnfvar`. | |
490 | ||
491 | :param bool renumber: whether to fix up the number/ids of the CNFs | |
492 | :returns: a dictionary with the converted values | |
493 | :rtype: {str, {str, str or int}} | |
494 | """ | |
495 | if renumber: | |
496 | self.renumber() | |
42c1b1d1 | 497 | return {"cnf": [x.to_cnf_structure() for x in self]} |
d31714a0 SA |
498 | |
499 | def to_cnf_file(self, path, renumber=True, encoding=ENCODING): | |
500 | """ | |
501 | Dump a string representation of this list in the cnfvar format to a file. | |
502 | ||
503 | :param str path: path to the file to write to | |
504 | :param bool renumber: whether to fix the lineno of the cnfvars | |
505 | :param str encoding: encoding to use to save the file | |
506 | """ | |
507 | if renumber: | |
508 | self.renumber() | |
509 | with open(path, "w", encoding=encoding) as fp: | |
510 | fp.write(str(self)) | |
511 | ||
512 | def to_json_string(self, renumber=True): | |
513 | """ | |
514 | Generate a JSON representation of this list in the cnfvar format. | |
515 | ||
516 | :param bool renumber: whether to fix the lineno of the cnfvars | |
517 | :returns: the JSON string | |
518 | :rtype: str | |
519 | """ | |
520 | def _to_dict(cnf): | |
521 | d = { | |
522 | "number": cnf.lineno, | |
523 | "varname": cnf.name, | |
524 | "data": cnf.value, | |
525 | "instance": cnf.instance | |
526 | } | |
527 | if cnf.parent and cnf.parent.lineno: | |
528 | d["parent"] = cnf.parent.lineno | |
529 | if cnf.comment is not None: | |
530 | d["comment"] = cnf.comment | |
531 | if len(cnf.children) > 0: | |
532 | d["children"] = [_to_dict(c) for c in cnf.children] | |
533 | return d | |
534 | if renumber: | |
d5d2c1d7 | 535 | self.renumber() |
d31714a0 SA |
536 | json_list = [_to_dict(c) for c in self] |
537 | return json.dumps({"cnf": json_list}) | |
538 | ||
539 | def to_json_file(self, path, renumber=True): | |
540 | """ | |
541 | Dump a JSON representation of this list to a file. | |
542 | ||
543 | :param str path: path to the file to write to | |
544 | :param bool renumber: whether to fix the lineno of the cnfvars | |
545 | """ | |
546 | with open(path, "w", encoding="utf8") as fp: | |
547 | fp.write(self.to_json_string(renumber=renumber)) | |
548 | ||
549 | @classmethod | |
550 | def from_cnf_structure(cls, obj): | |
551 | """ | |
552 | Create a list from a cnfvar object from the :py:mod:`cnfvar` module. | |
553 | ||
554 | :param obj: an object as defined in the :py:mod:`cnfvar` | |
555 | :type obj: {str, {str, str or int}} | |
556 | :returns: a list of cnfvars | |
557 | :rtype: :py:class:`CnfList` | |
558 | """ | |
559 | return cls(map(Cnf.from_cnf_structure, obj["cnf"])) | |
560 | ||
561 | @classmethod | |
562 | def from_cnf_string(cls, data): | |
563 | """ | |
564 | Create a list from a cnfvar string. | |
565 | ||
566 | :param str data: string to generate the list from | |
567 | :returns: a list of cnfvars | |
568 | :rtype: :py:class:`CnfList` | |
569 | """ | |
570 | cnf_obj = cnfvar_old.read_cnf(data) | |
571 | return CnfList.from_cnf_structure(cnf_obj) | |
572 | ||
573 | @classmethod | |
574 | def from_json_string(cls, data): | |
575 | """ | |
576 | Create a list from a json string. | |
577 | ||
578 | :param str data: string to generate the list from | |
579 | :returns: a list of cnfvars | |
580 | :rtype: :py:class:`CnfList` | |
581 | """ | |
582 | cnf_obj = json.loads(data) | |
583 | return CnfList.from_cnf_structure(cnf_obj) | |
584 | ||
585 | @classmethod | |
586 | def from_cnf_file(cls, path, encoding=ENCODING): | |
587 | """ | |
588 | Create a list from a cnfvar file. | |
589 | ||
590 | :param str path: path to the file to read | |
591 | :param str encoding: encoding to use to open the file (defaults to | |
592 | latin1 as this is the default encoding) | |
593 | :returns: a list of cnfvars | |
594 | :rtype: :py:class:`CnfList` | |
595 | """ | |
596 | with open(path, "r", encoding=encoding) as fp: | |
597 | return CnfList.from_cnf_string(fp.read()) | |
598 | ||
599 | @classmethod | |
600 | def from_json_file(cls, path): | |
601 | """ | |
602 | Create a list from a json file. | |
603 | ||
604 | :param str path: path to the file to read | |
605 | :returns: a list of cnfvars | |
606 | :rtype: :py:class:`CnfList` | |
607 | """ | |
608 | with open(path, "r", encoding="utf8") as fp: | |
609 | return CnfList.from_json_string(fp.read()) | |
610 | ||
611 | ||
612 | class CnfSerializationMixin(BaseCnf): | |
613 | """Add serialization support to BaseCnf.""" | |
614 | ||
42c1b1d1 | 615 | def to_cnf_structure(self): |
d31714a0 SA |
616 | """ |
617 | Convert this instance to dictionary from the :py:mod:`cnfvar` module. | |
618 | ||
619 | :returns: the dictionary created | |
620 | :rtype: {str, str or int} | |
621 | ||
622 | .. todo:: this method is still needed because dumping cnf variables | |
623 | to strings (json or not) is still delegated to the old cnfvar module. | |
624 | """ | |
625 | d = { | |
626 | "number": self.lineno, | |
627 | "varname": self.name, | |
628 | "data": self.value, | |
629 | "instance": self.instance | |
630 | } | |
631 | if self.parent and self.parent.lineno: | |
632 | d["parent"] = self.parent.lineno | |
633 | if self.comment is not None: | |
634 | d["comment"] = self.comment | |
635 | if len(self.children) > 0: | |
42c1b1d1 | 636 | d["children"] = [c.to_cnf_structure() for c in self.children] |
d31714a0 SA |
637 | return d |
638 | ||
639 | def to_json_string(self, renumber=True): | |
640 | """ | |
641 | Convert this instance to a JSON string. | |
642 | ||
643 | :param bool renumber: whether to fix the lineno of the cnfvars | |
644 | :returns: the JSON string | |
645 | :rtype: str | |
646 | """ | |
647 | return CnfList([self]).to_json_string(renumber=renumber) | |
648 | ||
649 | def to_cnf_file(self, path, renumber=True, encoding=ENCODING): | |
650 | """ | |
651 | Dump a string representation of this instance to a file. | |
652 | ||
653 | :param str path: path to the file to write to | |
654 | :param bool renumber: whether to fix the lineno of this cnfvar and its children | |
655 | :param str encoding: encoding to use to save the file | |
656 | """ | |
657 | CnfList([self]).to_cnf_file(path, renumber=renumber, encoding=encoding) | |
658 | ||
659 | def to_json_file(self, path, renumber=True): | |
660 | """ | |
661 | Dump a JSON representation of this instance to a file. | |
662 | ||
663 | :param str path: path to the file to write to | |
664 | :param bool renumber: whether to fix the lineno of this cnfvar and its children | |
665 | """ | |
666 | CnfList([self]).to_json_file(path, renumber=renumber) | |
667 | ||
668 | @classmethod | |
669 | def from_cnf_structure(cls, obj): | |
670 | """ | |
671 | Create an instance from a dictionary from the :py:mod:`cnfvar` module. | |
672 | ||
673 | :param obj: dictionary to convert to this instance | |
674 | :type obj: {str, str or int} | |
675 | :returns: the cnf instance created | |
676 | :rtype: :py:class:`Cnf` | |
677 | """ | |
678 | cnf = Cnf(obj["varname"], obj["data"], | |
679 | instance=obj["instance"], lineno=obj["number"], | |
680 | comment=obj.get("comment", None)) | |
681 | for ch_obj in obj.get("children", []): | |
682 | child_cnf = Cnf.from_cnf_structure(ch_obj) | |
683 | cnf.add_child(child_cnf) | |
684 | return cnf | |
685 | ||
686 | @classmethod | |
687 | def from_cnf_string(cls, data): | |
688 | """ | |
689 | Create an instance of this class from a cnfvar string. | |
690 | ||
691 | :param str data: cnfvar string to convert | |
692 | :returns: the cnf instance created | |
693 | :rtype: :py:class:`Cnf` | |
694 | """ | |
695 | return CnfListSerializationMixin.from_cnf_string(data).single() | |
696 | ||
697 | @classmethod | |
698 | def from_json_string(cls, data): | |
699 | """ | |
700 | Create an instance of this class from a JSON string. | |
701 | ||
702 | :param str data: JSON string to convert | |
703 | :returns: the cnf instance created | |
704 | :rtype: :py:class:`Cnf` | |
705 | """ | |
706 | return CnfListSerializationMixin.from_json_string(data).single() | |
707 | ||
708 | @classmethod | |
709 | def from_cnf_file(cls, path, encoding=ENCODING): | |
710 | """ | |
711 | Create an instance of this class from a cnfvar file. | |
712 | ||
713 | :param str path: path to the file to read | |
714 | :param str encoding: encoding to use to read the file | |
715 | :returns: the cnf instance created | |
716 | :rtype: :py:class:`Cnf` | |
717 | """ | |
718 | return CnfListSerializationMixin.from_cnf_file(path, encoding=encoding).single() | |
719 | ||
720 | @classmethod | |
721 | def from_json_file(cls, path): | |
722 | """ | |
723 | Create an instance of this class from a json file. | |
724 | ||
725 | :param str path: path to the file to read | |
726 | :returns: the cnf instance created | |
727 | :rtype: :py:class:`Cnf` | |
728 | """ | |
729 | return CnfListSerializationMixin.from_json_file(path).single() | |
730 | ||
731 | ||
732 | class CnfListArniedApiMixin(BaseCnfList): | |
733 | """Add support for converting this class to and from Arnied API classes.""" | |
734 | ||
735 | def to_api_structure(self): | |
736 | """ | |
737 | Convert this list to the corresponding object in the arnied API. | |
738 | ||
739 | :returns: the converted object | |
740 | :rtype: [:py:class:`arnied_api.CnfVar`] | |
741 | """ | |
742 | return [c.to_api_structure() for c in self] | |
743 | ||
744 | @classmethod | |
745 | def from_api_structure(cls, cnfvar_list): | |
746 | """ | |
747 | Convert a list from the arnied API into a list of this type. | |
748 | ||
749 | :param cnfvar_list: list to convert | |
750 | :type cnfvar_list: [:py:class:`arnied_api.CnfVar`] | |
751 | :returns: the list created | |
752 | :rtype: :py:class:`CnfList` | |
753 | """ | |
754 | return CnfList((Cnf.from_api_structure(c) for c in cnfvar_list), | |
755 | renumber=True) | |
756 | ||
757 | ||
758 | class CnfArniedApiMixin(BaseCnf): | |
759 | """Add support for converting this class to and from Arnied API classes.""" | |
760 | ||
761 | def to_api_structure(self): | |
762 | """ | |
763 | Convert this instance to the corresponding object in the arnied API. | |
764 | ||
765 | :returns: the converted object | |
766 | :rtype: :py:class:`arnied_api.CnfVar` | |
767 | """ | |
768 | return arnied_api.CnfVar( | |
769 | self.name.upper(), | |
770 | self.instance, | |
771 | self.value, | |
772 | False, # default here to False | |
773 | children=[c.to_api_structure() for c in self.children]) | |
774 | ||
775 | @classmethod | |
776 | def from_api_structure(cls, cnfobj): | |
777 | """ | |
778 | Convert an object from the arnied API into an instance of this type. | |
779 | ||
780 | :param cnfobj: object to convert | |
781 | :type cnfobj: :py:class:`arnied_api.CnfVar` | |
782 | :returns: the instance created | |
783 | :rtype: :py:class:`Cnf` | |
784 | """ | |
785 | cnf = Cnf(cnfobj.name, cnfobj.data, cnfobj.instance) | |
786 | children = CnfList((Cnf.from_api_structure(c) for c in cnfobj.children)) | |
787 | for c in children: | |
788 | c.parent = cnf | |
789 | cnf.children.extend(children) | |
790 | return cnf | |
791 | ||
792 | ||
793 | class CnfShortcutsMixin(BaseCnf): | |
794 | """Extend the base CNF class with useful methods.""" | |
795 | ||
796 | def enable(self): | |
797 | """Treat this variable as a boolean var and set its value to 1.""" | |
798 | self.value = "1" | |
799 | ||
800 | def disable(self): | |
801 | """Treat this variable as a boolean var and set its value to 0.""" | |
802 | self.value = "0" | |
803 | ||
804 | def is_enabled(self): | |
805 | """Treat this variable as a boolean var and check if its value is 1.""" | |
806 | return self.value == "1" | |
807 | ||
808 | def enable_child_flag(self, name): | |
809 | """ | |
810 | Set the value of the child CNF matching `name` to "1". | |
811 | ||
812 | :param str name: name of the child whose value to enable | |
813 | ||
814 | .. note:: child will be created if it does not exist. | |
815 | """ | |
816 | cnf = self.children.first_with_name(name, default=None) | |
817 | if cnf is None: | |
d5d2c1d7 | 818 | self.add_child(name, "1") |
d31714a0 SA |
819 | else: |
820 | cnf.enable() | |
821 | ||
822 | def disable_child_flag(self, name): | |
823 | """ | |
824 | Set the value of the child CNF matching `name` to "0". | |
825 | ||
826 | :param str name: name of the child whose value to disable | |
827 | ||
828 | .. note:: child will be created if it does not exist. | |
829 | """ | |
830 | cnf = self.children.first_with_name(name, default=None) | |
831 | if cnf is None: | |
d5d2c1d7 | 832 | self.add_child(name, "0") |
d31714a0 SA |
833 | else: |
834 | cnf.disable() | |
835 | ||
836 | def child_flag_enabled(self, name): | |
837 | """ | |
838 | Check if a given child has a value equal to `1`. | |
839 | ||
840 | :param str name: name of the child to check | |
841 | :returns: whether the value of the given child, if it exists, is 1 | |
842 | :rtype: bool | |
843 | """ | |
844 | cnf = self.children.first_with_name(name, default=None) | |
845 | return cnf.is_enabled() if cnf is not None else False | |
846 | ||
847 | ||
848 | class CnfListQueryingMixin(BaseCnfList): | |
849 | """Mixing adding shortcuts for common filter operations.""" | |
850 | ||
851 | def single(self, where_filter=None, default=DEFAULT): | |
852 | """ | |
853 | Get the only CNF of this list or raise if none or more than one exist. | |
854 | ||
855 | :param where_filter: predicate to apply against CNFs beforehand | |
856 | :type where_filter: function accepting a CNF and returning a boolean | |
857 | :param default: value to return in case the list is empty | |
858 | :type default: any | |
859 | :raises: :py:class:`ValueError` if a single value cannot be found and | |
860 | a default value was not specified | |
861 | :returns: the first and only element of this list, or default if set | |
862 | and no element is present | |
863 | :rtype: :py:class:`Cnf` | |
864 | """ | |
865 | list_ = self.where(where_filter) if where_filter is not None else self | |
866 | ||
867 | if len(list_) == 1: | |
868 | return list_[0] | |
869 | elif len(list_) == 0 and default != DEFAULT: | |
870 | return default | |
871 | else: | |
872 | raise ValueError(f"CnfList does not contain a single item (len={len(list_)})") | |
873 | ||
874 | def first(self, where_filter=None, default=DEFAULT): | |
875 | """ | |
876 | Get the first element in this list or raise if the list is empty. | |
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 element of this list, or default if set and | |
885 | 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 | if len(list_) > 0: | |
890 | return list_[0] | |
891 | elif default != DEFAULT: | |
892 | return default | |
893 | else: | |
894 | raise ValueError("Cannot get the first item - CnfList is empty") | |
895 | ||
896 | def with_value(self, value): | |
897 | """Shortcut method for filtering by value.""" | |
898 | return self.where(lambda c: c.value == value) | |
899 | ||
900 | def with_name(self, name): | |
901 | """Shortcut method for filtering by name.""" | |
902 | return self.where(lambda c: c.name == name) | |
903 | ||
904 | def with_instance(self, instance): | |
905 | """Shortcut method for filtering by instance.""" | |
906 | return self.where(lambda c: c.instance == instance) | |
907 | ||
908 | def single_with_name(self, name, default=DEFAULT): | |
909 | """Shortcut method for getting the single item with a given name.""" | |
910 | return self.with_name(name).single(default=default) | |
911 | ||
912 | def single_with_value(self, value, default=DEFAULT): | |
913 | """Shortcut method for getting the single item with a given value.""" | |
914 | return self.with_value(value).single(default=default) | |
915 | ||
916 | def single_with_instance(self, instance, default=DEFAULT): | |
917 | """Shortcut method for getting the single item with a given instance.""" | |
918 | return self.with_instance(instance).single(default=default) | |
919 | ||
920 | def first_with_name(self, name, default=DEFAULT): | |
921 | """Shortcut method for getting the first item with a given name.""" | |
922 | return self.with_name(name).first(default=default) | |
923 | ||
924 | def first_with_value(self, value, default=DEFAULT): | |
925 | """Shortcut method for getting the first item with a given value.""" | |
926 | return self.with_value(value).first(default=default) | |
927 | ||
928 | def first_with_instance(self, instance, default=DEFAULT): | |
929 | """Shortcut method for getting the first item with a given instance.""" | |
930 | return self.with_instance(instance).first(default=default) | |
931 | ||
cdfd8f20 PD |
932 | def highest_instance(self): |
933 | """Shortcut method for getting the next instance in a list of items.""" | |
934 | return max([c.instance for c in self]) if len(self) > 0 else -1 | |
935 | ||
d31714a0 SA |
936 | |
937 | ############################################################################### | |
938 | # PUBLIC CLASSES | |
939 | ############################################################################### | |
940 | # | |
941 | # Set up the classes with the mixins we want to be available by default. | |
942 | # | |
943 | ||
944 | ||
945 | class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin): | |
946 | """Collection of Cnf variables.""" | |
947 | ||
948 | pass | |
949 | ||
950 | ||
951 | class Cnf(CnfSerializationMixin, CnfArniedApiMixin, CnfShortcutsMixin): | |
952 | """Class representing a cnfvar.""" | |
953 | ||
954 | pass | |
955 | ||
956 | ||
957 | __all__ = ["CnfList", "Cnf"] |