3 # The software in this package is distributed under the GNU General
4 # Public License version 2 (with a special exception described below).
6 # A copy of GNU General Public License (GPL) is included in this distribution,
7 # in the file COPYING.GPL.
9 # As a special exception, if other files instantiate templates or use macros
10 # or inline functions from this file, or you compile this file and link it
11 # with other works to produce a work based on this file, this file
12 # does not by itself cause the resulting work to be covered
13 # by the GNU General Public License.
15 # However the source code for this file must still be made available
16 # in accordance with section (3) of the GNU General Public License.
18 # This exception does not invalidate any other reasons why a work based
19 # on this file might be covered by the GNU General Public License.
21 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
26 -------------------------------------------------------------------------------
27 Represent CNF_VARs as recursive structures.
29 Copyright: 2014-2017 Intra2net AG
34 -------------------------------------------------------------------------------
36 This module provides read and write functionality for the Intra2net *CNF*
37 format. Two different syntaxes are available: classical *CNF* and a JSON
38 representation. Both versions are commonly understood by Intra2net software.
40 On the command line, raw CNF is accepted if the option ``-`` is given: ::
42 $ get_cnf routing 2 |python3 cnfvar.py - <<ENOUGH
43 1 ROUTING,2: "192.168.55.0"
44 2 (1) ROUTING_COMMENT,0: ""
45 3 (1) ROUTING_DNS_RELAYING_ALLOWED,0: "1"
46 4 (1) ROUTING_EMAIL_RELAYING_ALLOWED,0: "1"
47 5 (1) ROUTING_FIREWALL_RULESET_REF,0: "9"
48 6 (1) ROUTING_GATEWAY,0: "10.0.254.1"
49 7 (1) ROUTING_NAT_INTO,0: "0"
50 8 (1) ROUTING_NETMASK,0: "255.255.255.0"
51 9 (1) ROUTING_PROXY_PROFILE_REF,0: "2"
54 1 ROUTING,2: "192.168.55.0"
55 2 (1) ROUTING_COMMENT,0: ""
56 3 (1) ROUTING_DNS_RELAYING_ALLOWED,0: "1"
57 4 (1) ROUTING_EMAIL_RELAYING_ALLOWED,0: "1"
58 5 (1) ROUTING_FIREWALL_RULESET_REF,0: "9"
59 6 (1) ROUTING_GATEWAY,0: "10.0.254.1"
60 7 (1) ROUTING_NAT_INTO,0: "0"
61 8 (1) ROUTING_NETMASK,0: "255.255.255.0"
62 9 (1) ROUTING_PROXY_PROFILE_REF,0: "2"
64 The input takes one round-trip through the parsers and will error out on
65 problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data.
67 Note that line numbers may be arbitrarily reassigned in the process. Of course,
68 parent references and the relative ordering of lines will be preserved in this
72 Decide on some facility for automatic fixup of line number values. The
73 internal representation is recursive so line numbers are not needed to
74 establish a variable hierarchy. They might as well be omitted from the json
75 input and only added when writing cnf. For the time being a lack of the
76 "number" field is interpreted as an error. Though it should at least
77 optionally be possible to omit the numbers entirely and have a function add
78 them after the fact. This might as well be added to :py:func:`is_cnf`
79 though it would be counterintuitive to have a predicate mutate its
80 argument. So maybe it could return a second argument to indicate a valid
81 structure that needs fixup or something like that.
84 The variable values of get_cnf seems to be encoded in latin1, and set_cnf
85 seems to assume latin1-encoded values (not var names). Function
86 :py:func:`read_cnf` converts this to unicode and functions
87 :py:func:`dump_cnf_string`, :py:func:`print_cnf`, and :py:func:`write_cnf`
88 convert unicode back to latin1.
91 notes on Python 3 conversion
92 -------------------------------------------------------------------------------
94 Since the original *CNF* format assumes latin-1 encoded data pretty much
95 exclusively, we preserve the original encoding while parsing the file.
96 When assembling the data structures returned to the user, values are then
97 converted to strings so they can be used naturally at the Python end.
100 -------------------------------------------------------------------------------
110 ###############################################################################
112 ###############################################################################
115 CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ])
116 CNF_FIELD_OPTIONAL = set ([ "parent", "children", "comment", "number" ])
117 CNF_FIELD_KNOWN = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL
119 grab_parent_pattern = re.compile(b"""
121 \s* # optional spaces
128 base_line_pattern = re.compile(b"""
130 \s* # optional spaces
133 ([A-Z][A-Z0-9_]*) # varname
134 \s* # optional spaces
136 \s* # optional spaces
138 \s* # optional spaces
140 \s* # optional spaces
141 \"( # quoted string (data)
142 (?: \\\" # (of escaped dquote
143 |[^\"])* # or anything not a
145 \s* # optional spaces
148 \s* # optional spaces
149 .* # string (comment)
150 )? # egroup, optional
155 child_line_pattern = re.compile(b"""
157 \s* # optional spaces
162 ([A-Z][A-Z0-9_]*) # varname
163 \s* # optional spaces
165 \s* # optional spaces
167 \s* # optional spaces
169 \s* # optional spaces
170 \"([^\"]*)\" # quoted string (data)
171 \s* # optional spaces
174 \s* # optional spaces
175 .* # string (comment)
176 )? # egroup, optional
182 ###############################################################################
184 ###############################################################################
188 # Sadly, the Intranator is still stuck with one leg in the 90s.
191 """Take given unicode str and convert it to a latin1-encoded `bytes`."""
192 return s.encode("latin-1")
196 """Take given latin1-encoded `bytes` value and convert it to `str`."""
197 return s.decode("latin-1")
201 # Conversion functions
204 def marshal_in_number(number):
208 def marshal_in_parent(parent):
212 def marshal_in_instance(instance):
216 def marshal_in_varname(varname):
217 return from_latin1(varname).lower()
220 def marshal_in_data(data):
221 return from_latin1(data) if data is not None else ""
224 def marshal_in_comment(comment):
225 return comment and from_latin1(comment[1:].strip()) or None
233 return isinstance(s, str)
236 ###############################################################################
238 ###############################################################################
241 class InvalidCNF(Exception):
243 def __init__(self, msg):
247 return "Malformed CNF_VAR: \"%s\"" % self.msg
250 class MalformedCNF(Exception):
252 def __init__(self, msg):
256 return "Malformed CNF file: \"%s\"" % self.msg
259 ###############################################################################
261 ###############################################################################
273 raise InvalidCNF("CNF_VAR lacks a name.")
274 elif not is_string(varname):
275 raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string."
278 raise InvalidCNF("Varname field of CNF_VAR is the empty string.")
280 if comment is not None:
281 if not is_string(comment):
282 raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string."
286 raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty."
288 elif not is_string(data):
289 raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string."
293 raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty."
295 elif not isinstance(instance, int):
296 raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer."
300 raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty."
302 elif not isinstance(number, int):
303 raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer."
306 raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d."
309 other = acc.get(number, None)
310 if other is not None: # already in use
311 raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s."
313 acc[number] = varname
317 raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty."
319 elif not isinstance(parent, int):
320 raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer."
323 if parent is not None:
324 raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"."
331 is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid`
332 predicate over the argument which can be either a well-formed CNF
333 dictionary or a list of CNF_VARs.
335 :type root: cnfvar or cnf list
338 Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non
339 well-formed member of the argument will cause the predicate to bail out
340 with an exception during traversal.
344 raise InvalidCNF(root)
345 return walk_cnf(cnf, False, is_valid, {}) is not None
350 Check whether a dictionary is a valid CNF.
352 :param dict obj: dictionary to check
353 :returns: True if the dictionary has all the mandatory fields and no
354 unknown fields, False otherwise
357 assert isinstance (obj, dict)
359 for f in CNF_FIELD_MANDATORY:
360 if obj.get(f, None) is None:
364 if f not in CNF_FIELD_KNOWN:
370 ###############################################################################
372 ###############################################################################
376 # JSON reader for get_cnf -j (the easy part)
379 def make_varname_lowercase(cnfvar):
381 Custom hook for json decoder: convert variable name to lowercase.
383 Since variable names are case insensitive, :py:func:`read_cnf` converts
384 them all to lower case. Downstream users of :py:func:`read_cnf_json` (e.g.
385 :py:class:`simple_cnf.SimpleCnf`) rely on lowercase variable names.
387 :param dict cnfvar: JSON "object" converted into a dict
388 :returns: same as input but if field `varname` is present, its value is
389 converted to lower case
390 :rtype: dict with str keys
393 cnfvar['varname'] = cnfvar['varname'].lower()
394 except KeyError: # there is no "varname" field
396 except AttributeError: # cnfvar['varname'] is not a string
401 def read_cnf_json(cnfdata):
403 Read json data from cnf data bytes.
405 :param bytes cnfdata: config data
406 :return: the parsed json data
409 .. note:: The JSON module does not decode data for all versions
410 of Python 3 so we handle the decoding ourselves.
412 if isinstance (cnfdata, bytes) is True:
413 cnfdata = from_latin1 (cnfdata)
414 cnf_json = json.loads(cnfdata, object_hook=make_varname_lowercase)
415 if is_cnf(cnf_json) is False:
416 raise TypeError("Invalid CNF_VAR.")
421 # CNF reader (the moderately more complicated part)
423 # Parsing usually starts from the `read_cnf`, which accepts a string containing
424 # the variables to parse in the same structure as returned by `get_cnf`.
426 # In the `prepare` function the string is split into lines, and a 3-element
427 # tuple is built. The first (named `current`) and second (named `next`)
428 # elements of this tuple are respectively the first and second non-empty lines
429 # of the input, while the third is a list of the remaining lines. This tuple is
430 # named `state` in the implementation below, and it is passed around during
431 # parsing. The `get` and `peek` functions are used to easily retrieve the
432 # `current` and `next` items from the "state".
434 # When we "advance" the state, we actually drop the "current" element,
435 # replacing it with the "next", while a new "next" is popped from the list of
436 # remaining lines. Parsing is done this way because we need to look ahead at
437 # the next line -- if it is a child it needs to be appended to the `children`
438 # property of the current line.
440 # Regular expressions are used to extract important information from the CNF
441 # lines. Finally, once parsing is completed, a dictionary is returned. The dict
442 # has the same structure as the serialized JSON output returned by
449 Read cnf data from data bytes.
451 :param bytes data: raw data
452 :return: the parsed cnf data
453 :rtype: {str, {str, str or int}}
455 if isinstance(data, str):
456 data = to_latin1(data)
457 state = prepare(data)
459 raise InvalidCNF("Empty input string.")
461 cnf = parse_cnf_root(state)
462 if is_cnf(cnf) is False:
463 raise TypeError("Invalid CNF_VAR.")
469 Build 3-element iterable from a CNF string dump.
471 :param raw: string content as returned by `get_cnf`
473 :returns: 3-element tuple, where the first two elements are the first two
474 lines of the output and the third is a list containing the rest
475 of the lines in reverse.
476 :rtype: (str * str option * str list) option
478 lines = raw.splitlines()
490 first = first.strip()
492 return advance((first, second, lines))
494 return (first, second, lines)
499 Pop the next line from the stream, advancing the tuple.
501 :param cns: a 3-element tuple containing two CNF lines and a list of the
503 :type cnd: (str, str, [str])
504 :returns: a new tuple with a new item popped from the list of lines
505 :rtype cnd: (str, str, [str])
507 current, next, stream = cns
508 if next is None: # reached end of stream
518 if current == "": # skip blank lines
519 return advance((current, next, stream))
520 return (current, next, stream)
525 Get the current line from the state without advancing it.
527 :param cns: a 3-element tuple containing two CNF lines and a list of the
529 :type cnd: (str, str, [str])
530 :returns: the CNF line stored as `current`
539 Get the next line from the state without advancing it.
541 :param cns: a 3-element tuple containing two CNF lines and a list of the
543 :type cnd: (str, str, [str])
544 :returns: the CNF line stored as `next`
551 def parse_cnf_root(state):
553 Iterate over and parse a list of CNF lines.
555 :param state: a 3-element tuple containing two lines and a list of the
557 :type state: (str, str, [str])
558 :returns: a list of parsed CNF variables
561 The function will parse the first element from the `state` tuple, then read
562 the next line to see if it is a child variable. If it is, it will be
563 appended to the last parsed CNF, otherwise top-level parsing is done
569 cnf_line = read_base_line(current)
570 if cnf_line is not None:
571 lines.append(cnf_line)
572 state = advance(state)
573 if state is None: # -> nothing left to do
576 parent = get_parent(current) # peek at next line
577 if parent is not None: # -> recurse into children
578 (state, children, _parent) = parse_cnf_children(state, parent)
579 cnf_line["children"] = children
584 state = advance(state)
591 def parse_cnf_children(state, parent):
593 Read and parse child CNFs of a given parent until there is none left.
595 :param state: a 3-element tuple containing two lines and a list of the
597 :type state: (str, str, [str])
598 :param parent: id of the parent whose children we are looking for
600 :returns: a 3-element tuple with the current state, a list of children of
601 the given parent and the parent ID
602 :rtype: (tuple, [str], int)
604 The function will recursively parse child lines from the `state` tuple
605 until one of these conditions is satisfied:
607 1. the input is exhausted
609 2.1. is a toplevel line
610 2.2. is a child line whose parent has a lower parent number
612 Conceptually, 2.1 is a very similar to 2.2 but due to the special status of
613 toplevel lines in CNF we need to handle them separately.
615 Note that since nesting of CNF vars is achieved via parent line numbers,
616 lines with different parents could appear out of order. libcnffile will
617 happily parse those and still assign children to the specified parent:
621 1 USER,1337: "l33t_h4x0r"
622 2 (1) USER_GROUP_MEMBER_REF,0: "2"
623 4 USER,1701: "picard"
624 5 (4) USER_GROUP_MEMBER_REF,0: "2"
625 6 (4) USER_PASSWORD,0: "engage"
626 3 (1) USER_PASSWORD,0: "hacktheplanet"
629 1 USER,1337: "l33t_h4x0r"
630 2 (1) USER_GROUP_MEMBER_REF,0: "2"
631 3 (1) USER_PASSWORD,0: "hacktheplanet"
633 1 USER,1701: "picard"
634 2 (1) USER_GROUP_MEMBER_REF,0: "2"
635 3 (1) USER_PASSWORD,0: "engage"
637 It is a limitation of ``cnfvar.py`` that it cannot parse CNF data
638 structured like the above example: child lists are only populated from
639 subsequent CNF vars using the parent number solely to track nesting levels.
640 The parser does not keep track of line numbers while traversing the input
641 so it doesn’t support retroactively assigning a child to anything else but
642 the immediate parent.
647 cnf_line = read_child_line(current)
648 if cnf_line is not None:
649 lines.append(cnf_line)
650 state = advance(state)
654 new_parent = get_parent(current)
655 if new_parent is None:
657 return (state, lines, None)
658 if new_parent > parent:
659 # parent is further down in hierarchy -> new level
660 (state, children, new_parent) = \
661 parse_cnf_children (state, new_parent)
664 cnf_line["children"] = children
666 new_parent = get_parent(current)
667 if new_parent is None:
669 return (state, lines, None)
670 if new_parent < parent:
671 # parent is further up in hierarchy -> pop level
672 return (state, lines, new_parent)
673 # new_parent == parent -> continue parsing on same level
674 return (state, lines, parent)
677 def get_parent(line):
679 Extract the ID of the parent for a given CNF line.
681 :param str line: CNF line
682 :returns: parent ID or None if no parent is found
685 match = re.match(grab_parent_pattern, line)
686 if match is None: # -> no parent
688 return int(match.groups()[0])
691 def read_base_line(line):
693 Turn one top-level CNF line into a dictionary.
695 :param str line: CNF line
698 This performs the necessary decoding on values to obtain proper Python
699 strings from 8-bit encoded CNF data.
701 The function only operates on individual lines. Argument strings that
702 contain data for multiple lines – this includes child lines of the current
703 CNF var! – will trigger a parsing exception.
705 if len(line.strip()) == 0:
706 return None # ignore empty lines
708 return None # ignore comments
710 match = re.match(base_line_pattern, line)
712 raise MalformedCNF("Syntax error in line \"\"\"%s\"\"\"" % line)
713 number, varname, instance, data, comment = match.groups()
715 "number" : marshal_in_number (number),
716 "varname" : marshal_in_varname (varname),
717 "instance" : marshal_in_instance (instance),
718 "data" : marshal_in_data (data),
719 "comment" : marshal_in_comment (comment),
723 def read_child_line(line):
725 Turn one child CNF line into a dictionary.
727 :param str line: CNF line
730 This function only operates on individual lines. If the argument string is
731 syntactically valid but contains input representing multiple CNF vars, a
732 parse error will be thrown.
734 if len(line.strip()) == 0:
735 return None # ignore empty lines
737 return None # ignore comments
739 match = re.match(child_line_pattern, line)
741 raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\""
742 % from_latin1 (line))
743 number, parent, varname, instance, data, comment = match.groups()
745 "number" : marshal_in_number (number),
746 "parent" : marshal_in_parent (parent),
747 "varname" : marshal_in_varname (varname),
748 "instance" : marshal_in_instance (instance),
749 "data" : marshal_in_data (data),
750 "comment" : marshal_in_comment (comment),
754 ###############################################################################
756 ###############################################################################
759 cnf_line_nest_indent = " "
760 cnf_line_base_fmt = "%d %s,%d: \"%s\""
761 cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\""
764 def format_cnf_vars(da, var):
766 Return a list of formatted cnf_line byte strings.
768 :param da: a tuple where the first element is the depth (0 = top-level,
769 >1 = child CNF) and the second is the string being built.
771 :param var: the CNF element to convert to string in the current iteration
773 :returns: a tuple like `da`, where the second element should contain all
777 This function is meant to be passed to the :py:func:`functools.reduce`
780 The variable names are uppercased unconditionally because while ``get_cnf``
781 is case-indifferent for variable names, ``set_cnf`` isn’t.
786 line = cnf_line_child_fmt \
788 cnf_line_nest_indent * depth,
790 var["varname"].upper(),
794 line = cnf_line_base_fmt \
796 var["varname"].upper(),
800 comment = var.get("comment", None)
801 if comment and len(comment) != 0:
802 line = line + (" # %s" % comment)
804 acc.append(to_latin1(line))
806 children = var.get("children", None)
807 if children is not None:
808 (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
815 Extract a list of CNFs from a given structure.
817 :param root: list of CNFs or a CNF dictionary
818 :type root: [dict] or dict
819 :raises: :py:class:`TypeError` if no CNFs can be extracted
820 :returns: list with one or more CNF objects
823 Output varies depending on a few conditions:
824 - If `root` is a list, return it right away
825 - If `root` is a dict corresponding to a valid CNF value, return it wrapped
827 - If `root` is a dict with a `cnf` key containg a list (as the JSON
828 returned by `get_cnf -j`), return the value
829 - Otherwise, raise an error
831 if isinstance(root, list):
833 if not isinstance(root, dict):
835 "Expected dictionary of CNF_VARs, got %s." % type(root))
838 cnf = root.get("cnf", None)
839 if not isinstance(cnf, list):
840 raise TypeError("Expected list of CNF_VARs, got %s." % type(cnf))
844 def normalize_cnf(cnf):
846 Ensure the output conforms to set_cnf()’s expectations.
848 :param cnf: list of CNF objects to normalize
850 :returns: normalized list
853 if isinstance(cnf, list) is False:
854 raise MalformedCNF("expected list of CNF_VARs, got [%s]" % type(cnf))
857 { "number" : var ["number"]
858 , "varname" : var ["varname"].upper()
859 , "instance" : var ["instance"]
860 , "data" : var ["data"]
863 children = var.get("children", None)
864 if children is not None:
865 vvar ["children"] = normalize_cnf(children)
867 parent = var.get("parent", None)
868 if parent is not None:
869 vvar ["parent"] = var["parent"]
871 comment = var.get("comment", None)
872 if comment is not None:
873 vvar ["comment"] = var["comment"]
877 return [norm(var) for var in cnf]
880 ###############################################################################
882 ###############################################################################
885 def walk_cnf(cnf, nested, fun, acc):
887 Depth-first traversal of a CNF tree.
891 :type fun: 'a -> bool -> (cnf stuff) -> 'a
895 Executes ``fun`` recursively for each node in the tree. The function
896 receives the accumulator ``acc`` which can be of an arbitrary type as first
897 argument. The second argument is a flag indicating whether the current
898 CNF var is a child (if ``True``) or a parent var. CNF member fields are
899 passed via named optional arguments.
904 comment=var.get("comment", None),
905 data=var.get("data", None),
906 instance=var.get("instance", None),
907 number=var.get("number", None),
908 parent=var.get("parent", None),
909 varname=var.get("varname", None))
910 children = var.get("children", None)
911 if children is not None:
912 acc = walk_cnf(children, True, fun, acc)
916 def renumber_vars(root, parent=None, toplevel=False):
918 Number cnfvars linearly.
920 If *parent* is specified, numbering will start at this offset. Also, the
921 VAR *root* will be assigned this number as a parent lineno unless
922 *toplevel* is set (the root var in a CNF tree obviously can’t have a
925 The *toplevel* parameter is useful when renumbering an existing variable
926 starting at a given offset without at the same time having that offset
927 assigned as a parent.
929 root = cnf_root (root)
934 if toplevel is False and parent is not None:
935 var["parent"] = parent
936 children = var.get("children", None)
937 if children is not None:
938 i = renumber_vars(children, parent=i, toplevel=False)
942 def count_vars(root):
944 Traverse the cnf structure recursively, counting VAR objects (CNF lines).
948 raise InvalidCNF(root)
949 return walk_cnf(cnf, True, lambda n, _nested, **_kwa: n + 1, 0)
956 def get_vars(cnf, data=None, instance=None):
958 get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR
959 structure for entries with a matching `data` or `instance` field.
965 :returns: The structure containing only references to the
966 matching variables. Containing an empty list of
967 variables in case there is no match.
969 Values are compared literally. If both ``instance`` and ``data`` are
970 specified, vars will be compared against both.
974 criterion = lambda _var: False
977 criterion = lambda var: var[
978 "data"] == data and var["instance"] == instance
980 criterion = lambda var: var["data"] == data
982 criterion = lambda var: var["instance"] == instance
984 return {"cnf": [var for var in cnf if criterion(var) is True]}
989 ###############################################################################
991 ###############################################################################
995 # Print/dump raw CNF values
998 def output_cnf(root, out, renumber=False):
1000 Dump a textual representation of given CNF VAR structure to given stream.
1002 Runs :py:func:`format_cnf_vars` on the input (`root`) and then writes that
1003 to the given file-like object (out).
1005 :param root: a CNF_VAR structure
1006 :type root: dict or list or anything that :py:func:`cnf_root` accepts
1007 :param out: file-like object or something with a `write(str)` function
1008 :param bool renumber: Whether to renumber cnfvars first
1010 Files are converted to the 8-bit format expected by CNF so they can be fed
1011 directly into libcnffile.
1013 cnf = cnf_root(root)
1014 if renumber is True:
1015 _count = renumber_vars(root)
1016 if is_cnf(cnf) is True:
1017 (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
1018 if isinstance(out, (io.RawIOBase, io.BufferedIOBase)):
1019 out.write (b"\n".join (lines))
1021 else: # either subclass of io.TextIOBase or unknown
1022 out.write ("\n".join (map (from_latin1, lines)))
1026 def dump_cnf_bytes (root, renumber=False):
1028 Serialize CNF var structure, returning the result as a byte sequence.
1030 cnf = cnf_root(root)
1032 output_cnf(root, out, renumber=renumber)
1033 res = out.getvalue()
1038 def dump_cnf_string(root, renumber=False):
1040 Serialize CNF var structure, returning a latin1-encode byte string.
1042 .. todo::this is identical to :py:func:`dump_cnf_bytes`!
1044 cnf = cnf_root(root)
1046 output_cnf(root, out, renumber=renumber)
1047 res = out.getvalue()
1052 def print_cnf(root, out=None, renumber=False):
1054 Print given CNF_VAR structure to stdout (or other file-like object).
1056 Note that per default the config is printed to sys.stdout using the shell's
1057 preferred encoding. If the shell cannot handle unicode this might raise
1060 All params forwarded to :py:func:`output_cnf`. See args there.
1062 if root is not None:
1063 output_cnf(root, out or sys.stdout, renumber=renumber)
1066 def write_cnf(*argv, **kw_argv):
1067 """Alias for :py:func:`print_cnf`."""
1068 print_cnf(*argv, **kw_argv)
1071 def print_cnf_raw(root, out=None):
1072 """`if root is not None: out.write(root)`."""
1073 if root is not None:
1077 def write_cnf_raw(*argv, **kw_argv):
1078 """Alias for :py:func:`print_cnf_raw`."""
1079 print_cnf_raw(*argv, **kw_argv)
1083 # Print/dump CNF values in JSON format
1087 def output_json(root, out, renumber=False):
1089 Dump CNF_VAR structure to file-like object in json format.
1091 :param root: CNF_VAR structure
1092 :type root: dict or list or anything that :py:func:`cnf_root` accepts
1093 :param out: file-like object, used as argument to :py:func:`json.dumps` so
1094 probably has to accept `str` (as opposed to `bytes`).
1095 :param bool renumber: whether to renumber variables before dupming.
1097 # if not isinstance(out, file):
1098 #raise TypeError("%s (%s) is not a stream." % (out, type(out)))
1099 if renumber is True:
1100 _count = renumber_vars(root)
1101 if is_cnf(root) is True:
1102 root ["cnf"] = normalize_cnf(cnf_root (root))
1103 data = json.dumps(root)
1106 # TODO: else raise value error?
1109 def dump_json_string(root, renumber=False):
1111 Serialize CNF var structure as JSON, returning the result as a string.
1114 output_json(root, out, renumber=renumber)
1115 res = out.getvalue()
1120 def print_cnf_json(root, out=None, renumber=False):
1122 Print CNF_VAR structure in json format to stdout.
1124 Calls :py:func:`output_json` with `sys.stdout` if `out` is not given or
1127 if root is not None:
1128 output_json(root, out or sys.stdout, renumber=renumber)
1131 def write_cnf_json(*argv, **kw_argv):
1132 """Alias for :py:func:`print_cnf_json`."""
1133 print_cnf_json(*argv, **kw_argv)
1137 ###############################################################################
1138 # ENTRY POINT FOR DEVELOPMENT
1139 ###############################################################################
1143 print("usage: cnfvar.py -" , file=sys.stderr)
1144 print("" , file=sys.stderr)
1145 print(" Read CNF from stdin.", file=sys.stderr)
1146 print("" , file=sys.stderr)
1153 cnf = read_cnf(sys.stdin.buffer.read())
1156 elif first == "test":
1157 cnf = read_cnf(sys.stdin.buffer.read())
1158 cnff = get_vars(cnf, instance=2, data="FAX")
1164 if __name__ == "__main__":
1165 sys.exit(main(sys.argv))