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