1"""Python module which parses and emits TOML.
2
3Released under the MIT license.
4"""
5import re
6import io
7import datetime
8from os import linesep
9
10__version__ = "0.9.4"
11__spec__ = "0.4.0"
12
13
14class TomlDecodeError(Exception):
15    """Base toml Exception / Error."""
16    pass
17
18
19class TomlTz(datetime.tzinfo):
20    def __init__(self, toml_offset):
21        if toml_offset == "Z":
22            self._raw_offset = "+00:00"
23        else:
24            self._raw_offset = toml_offset
25        self._sign = -1 if self._raw_offset[0] == '-' else 1
26        self._hours = int(self._raw_offset[1:3])
27        self._minutes = int(self._raw_offset[4:6])
28
29    def tzname(self, dt):
30        return "UTC" + self._raw_offset
31
32    def utcoffset(self, dt):
33        return self._sign * datetime.timedelta(hours=self._hours,
34                                               minutes=self._minutes)
35
36    def dst(self, dt):
37        return datetime.timedelta(0)
38
39
40class InlineTableDict(object):
41    """Sentinel subclass of dict for inline tables."""
42
43
44def _get_empty_inline_table(_dict):
45    class DynamicInlineTableDict(_dict, InlineTableDict):
46        """Concrete sentinel subclass for inline tables.
47        It is a subclass of _dict which is passed in dynamically at load time
48        It is also a subclass of InlineTableDict
49        """
50
51    return DynamicInlineTableDict()
52
53
54try:
55    _range = xrange
56except NameError:
57    unicode = str
58    _range = range
59    basestring = str
60    unichr = chr
61
62try:
63    FNFError = FileNotFoundError
64except NameError:
65    FNFError = IOError
66
67
68def load(f, _dict=dict):
69    """Parses named file or files as toml and returns a dictionary
70
71    Args:
72        f: Path to the file to open, array of files to read into single dict
73           or a file descriptor
74        _dict: (optional) Specifies the class of the returned toml dictionary
75
76    Returns:
77        Parsed toml file represented as a dictionary
78
79    Raises:
80        TypeError -- When f is invalid type
81        TomlDecodeError: Error while decoding toml
82        IOError / FileNotFoundError -- When an array with no valid (existing)
83        (Python 2 / Python 3)          file paths is passed
84    """
85
86    if isinstance(f, basestring):
87        with io.open(f, encoding='utf-8') as ffile:
88            return loads(ffile.read(), _dict)
89    elif isinstance(f, list):
90        from os import path as op
91        from warnings import warn
92        if not [path for path in f if op.exists(path)]:
93            error_msg = "Load expects a list to contain filenames only."
94            error_msg += linesep
95            error_msg += ("The list needs to contain the path of at least one "
96                          "existing file.")
97            raise FNFError(error_msg)
98        d = _dict()
99        for l in f:
100            if op.exists(l):
101                d.update(load(l))
102            else:
103                warn("Non-existent filename in list with at least one valid "
104                     "filename")
105        return d
106    else:
107        try:
108            return loads(f.read(), _dict)
109        except AttributeError:
110            raise TypeError("You can only load a file descriptor, filename or "
111                            "list")
112
113
114_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$')
115
116
117def loads(s, _dict=dict):
118    """Parses string as toml
119
120    Args:
121        s: String to be parsed
122        _dict: (optional) Specifies the class of the returned toml dictionary
123
124    Returns:
125        Parsed toml file represented as a dictionary
126
127    Raises:
128        TypeError: When a non-string is passed
129        TomlDecodeError: Error while decoding toml
130    """
131
132    implicitgroups = []
133    retval = _dict()
134    currentlevel = retval
135    if not isinstance(s, basestring):
136        raise TypeError("Expecting something like a string")
137
138    if not isinstance(s, unicode):
139        s = s.decode('utf8')
140
141    sl = list(s)
142    openarr = 0
143    openstring = False
144    openstrchar = ""
145    multilinestr = False
146    arrayoftables = False
147    beginline = True
148    keygroup = False
149    keyname = 0
150    for i, item in enumerate(sl):
151        if item == '\r' and sl[i + 1] == '\n':
152            sl[i] = ' '
153            continue
154        if keyname:
155            if item == '\n':
156                raise TomlDecodeError("Key name found without value."
157                                      " Reached end of line.")
158            if openstring:
159                if item == openstrchar:
160                    keyname = 2
161                    openstring = False
162                    openstrchar = ""
163                continue
164            elif keyname == 1:
165                if item.isspace():
166                    keyname = 2
167                    continue
168                elif item.isalnum() or item == '_' or item == '-':
169                    continue
170            elif keyname == 2 and item.isspace():
171                continue
172            if item == '=':
173                keyname = 0
174            else:
175                raise TomlDecodeError("Found invalid character in key name: '" +
176                                      item + "'. Try quoting the key name.")
177        if item == "'" and openstrchar != '"':
178            k = 1
179            try:
180                while sl[i - k] == "'":
181                    k += 1
182                    if k == 3:
183                        break
184            except IndexError:
185                pass
186            if k == 3:
187                multilinestr = not multilinestr
188                openstring = multilinestr
189            else:
190                openstring = not openstring
191            if openstring:
192                openstrchar = "'"
193            else:
194                openstrchar = ""
195        if item == '"' and openstrchar != "'":
196            oddbackslash = False
197            k = 1
198            tripquote = False
199            try:
200                while sl[i - k] == '"':
201                    k += 1
202                    if k == 3:
203                        tripquote = True
204                        break
205                if k == 1 or (k == 3 and tripquote):
206                    while sl[i - k] == '\\':
207                        oddbackslash = not oddbackslash
208                        k += 1
209            except IndexError:
210                pass
211            if not oddbackslash:
212                if tripquote:
213                    multilinestr = not multilinestr
214                    openstring = multilinestr
215                else:
216                    openstring = not openstring
217            if openstring:
218                openstrchar = '"'
219            else:
220                openstrchar = ""
221        if item == '#' and (not openstring and not keygroup and
222                            not arrayoftables):
223            j = i
224            try:
225                while sl[j] != '\n':
226                    sl[j] = ' '
227                    j += 1
228            except IndexError:
229                break
230        if item == '[' and (not openstring and not keygroup and
231                            not arrayoftables):
232            if beginline:
233                if sl[i + 1] == '[':
234                    arrayoftables = True
235                else:
236                    keygroup = True
237            else:
238                openarr += 1
239        if item == ']' and not openstring:
240            if keygroup:
241                keygroup = False
242            elif arrayoftables:
243                if sl[i - 1] == ']':
244                    arrayoftables = False
245            else:
246                openarr -= 1
247        if item == '\n':
248            if openstring or multilinestr:
249                if not multilinestr:
250                    raise TomlDecodeError("Unbalanced quotes")
251                if ((sl[i - 1] == "'" or sl[i - 1] == '"') and (
252                        sl[i - 2] == sl[i - 1])):
253                    sl[i] = sl[i - 1]
254                    if sl[i - 3] == sl[i - 1]:
255                        sl[i - 3] = ' '
256            elif openarr:
257                sl[i] = ' '
258            else:
259                beginline = True
260        elif beginline and sl[i] != ' ' and sl[i] != '\t':
261            beginline = False
262            if not keygroup and not arrayoftables:
263                if sl[i] == '=':
264                    raise TomlDecodeError("Found empty keyname. ")
265                keyname = 1
266    s = ''.join(sl)
267    s = s.split('\n')
268    multikey = None
269    multilinestr = ""
270    multibackslash = False
271    for line in s:
272        if not multilinestr or multibackslash or '\n' not in multilinestr:
273            line = line.strip()
274        if line == "" and (not multikey or multibackslash):
275            continue
276        if multikey:
277            if multibackslash:
278                multilinestr += line
279            else:
280                multilinestr += line
281            multibackslash = False
282            if len(line) > 2 and (line[-1] == multilinestr[0] and
283                                  line[-2] == multilinestr[0] and
284                                  line[-3] == multilinestr[0]):
285                value, vtype = _load_value(multilinestr, _dict)
286                currentlevel[multikey] = value
287                multikey = None
288                multilinestr = ""
289            else:
290                k = len(multilinestr) - 1
291                while k > -1 and multilinestr[k] == '\\':
292                    multibackslash = not multibackslash
293                    k -= 1
294                if multibackslash:
295                    multilinestr = multilinestr[:-1]
296                else:
297                    multilinestr += "\n"
298            continue
299        if line[0] == '[':
300            arrayoftables = False
301            if line[1] == '[':
302                arrayoftables = True
303                line = line[2:].split(']]', 1)
304            else:
305                line = line[1:].split(']', 1)
306            if line[1].strip() != "":
307                raise TomlDecodeError("Key group not on a line by itself.")
308            groups = line[0].split('.')
309            i = 0
310            while i < len(groups):
311                groups[i] = groups[i].strip()
312                if groups[i][0] == '"' or groups[i][0] == "'":
313                    groupstr = groups[i]
314                    j = i + 1
315                    while not groupstr[0] == groupstr[-1]:
316                        j += 1
317                        groupstr = '.'.join(groups[i:j])
318                    groups[i] = groupstr[1:-1]
319                    groups[i + 1:j] = []
320                else:
321                    if not _groupname_re.match(groups[i]):
322                        raise TomlDecodeError("Invalid group name '" +
323                                              groups[i] + "'. Try quoting it.")
324                i += 1
325            currentlevel = retval
326            for i in _range(len(groups)):
327                group = groups[i]
328                if group == "":
329                    raise TomlDecodeError("Can't have a keygroup with an empty "
330                                          "name")
331                try:
332                    currentlevel[group]
333                    if i == len(groups) - 1:
334                        if group in implicitgroups:
335                            implicitgroups.remove(group)
336                            if arrayoftables:
337                                raise TomlDecodeError("An implicitly defined "
338                                                      "table can't be an array")
339                        elif arrayoftables:
340                            currentlevel[group].append(_dict())
341                        else:
342                            raise TomlDecodeError("What? " + group +
343                                                  " already exists?" +
344                                                  str(currentlevel))
345                except TypeError:
346                    currentlevel = currentlevel[-1]
347                    try:
348                        currentlevel[group]
349                    except KeyError:
350                        currentlevel[group] = _dict()
351                        if i == len(groups) - 1 and arrayoftables:
352                            currentlevel[group] = [_dict()]
353                except KeyError:
354                    if i != len(groups) - 1:
355                        implicitgroups.append(group)
356                    currentlevel[group] = _dict()
357                    if i == len(groups) - 1 and arrayoftables:
358                        currentlevel[group] = [_dict()]
359                currentlevel = currentlevel[group]
360                if arrayoftables:
361                    try:
362                        currentlevel = currentlevel[-1]
363                    except KeyError:
364                        pass
365        elif line[0] == "{":
366            if line[-1] != "}":
367                raise TomlDecodeError("Line breaks are not allowed in inline"
368                                      "objects")
369            _load_inline_object(line, currentlevel, _dict, multikey,
370                                multibackslash)
371        elif "=" in line:
372            ret = _load_line(line, currentlevel, _dict, multikey,
373                             multibackslash)
374            if ret is not None:
375                multikey, multilinestr, multibackslash = ret
376    return retval
377
378
379def _load_inline_object(line, currentlevel, _dict, multikey=False,
380                        multibackslash=False):
381    candidate_groups = line[1:-1].split(",")
382    groups = []
383    if len(candidate_groups) == 1 and not candidate_groups[0].strip():
384        candidate_groups.pop()
385    while len(candidate_groups) > 0:
386        candidate_group = candidate_groups.pop(0)
387        try:
388            _, value = candidate_group.split('=', 1)
389        except ValueError:
390            raise TomlDecodeError("Invalid inline table encountered")
391        value = value.strip()
392        if ((value[0] == value[-1] and value[0] in ('"', "'")) or (
393                value[0] in '-0123456789' or
394                value in ('true', 'false') or
395                (value[0] == "[" and value[-1] == "]"))):
396            groups.append(candidate_group)
397        else:
398            candidate_groups[0] = candidate_group + "," + candidate_groups[0]
399    for group in groups:
400        status = _load_line(group, currentlevel, _dict, multikey,
401                            multibackslash)
402        if status is not None:
403            break
404
405
406# Matches a TOML number, which allows underscores for readability
407_number_with_underscores = re.compile('([0-9])(_([0-9]))*')
408
409
410def _strictly_valid_num(n):
411    n = n.strip()
412    if not n:
413        return False
414    if n[0] == '_':
415        return False
416    if n[-1] == '_':
417        return False
418    if "_." in n or "._" in n:
419        return False
420    if len(n) == 1:
421        return True
422    if n[0] == '0' and n[1] != '.':
423        return False
424    if n[0] == '+' or n[0] == '-':
425        n = n[1:]
426        if n[0] == '0' and n[1] != '.':
427            return False
428    if '__' in n:
429        return False
430    return True
431
432
433def _load_line(line, currentlevel, _dict, multikey, multibackslash):
434    i = 1
435    pair = line.split('=', i)
436    strictly_valid = _strictly_valid_num(pair[-1])
437    if _number_with_underscores.match(pair[-1]):
438        pair[-1] = pair[-1].replace('_', '')
439    while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and
440                             pair[-1][0] != "'" and pair[-1][0] != '"' and
441                             pair[-1][0] != '[' and pair[-1][0] != '{' and
442                             pair[-1] != 'true' and pair[-1] != 'false'):
443        try:
444            float(pair[-1])
445            break
446        except ValueError:
447            pass
448        if _load_date(pair[-1]) is not None:
449            break
450        i += 1
451        prev_val = pair[-1]
452        pair = line.split('=', i)
453        if prev_val == pair[-1]:
454            raise TomlDecodeError("Invalid date or number")
455        if strictly_valid:
456            strictly_valid = _strictly_valid_num(pair[-1])
457    pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()]
458    if (pair[0][0] == '"' or pair[0][0] == "'") and \
459            (pair[0][-1] == '"' or pair[0][-1] == "'"):
460        pair[0] = pair[0][1:-1]
461    if len(pair[1]) > 2 and ((pair[1][0] == '"' or pair[1][0] == "'") and
462                             pair[1][1] == pair[1][0] and
463                             pair[1][2] == pair[1][0] and
464                             not (len(pair[1]) > 5 and
465                                  pair[1][-1] == pair[1][0] and
466                                  pair[1][-2] == pair[1][0] and
467                                  pair[1][-3] == pair[1][0])):
468        k = len(pair[1]) - 1
469        while k > -1 and pair[1][k] == '\\':
470            multibackslash = not multibackslash
471            k -= 1
472        if multibackslash:
473            multilinestr = pair[1][:-1]
474        else:
475            multilinestr = pair[1] + "\n"
476        multikey = pair[0]
477    else:
478        value, vtype = _load_value(pair[1], _dict, strictly_valid)
479    try:
480        currentlevel[pair[0]]
481        raise TomlDecodeError("Duplicate keys!")
482    except KeyError:
483        if multikey:
484            return multikey, multilinestr, multibackslash
485        else:
486            currentlevel[pair[0]] = value
487
488
489def _load_date(val):
490    microsecond = 0
491    tz = None
492    try:
493        if len(val) > 19:
494            if val[19] == '.':
495                microsecond = int(val[20:26])
496                if len(val) > 26:
497                    tz = TomlTz(val[26:32])
498            else:
499                tz = TomlTz(val[19:25])
500    except ValueError:
501        tz = None
502    try:
503        d = datetime.datetime(
504            int(val[:4]), int(val[5:7]),
505            int(val[8:10]), int(val[11:13]),
506            int(val[14:16]), int(val[17:19]), microsecond, tz)
507    except ValueError:
508        return None
509    return d
510
511
512def _load_unicode_escapes(v, hexbytes, prefix):
513    hexchars = ['0', '1', '2', '3', '4', '5', '6', '7',
514                '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
515    skip = False
516    i = len(v) - 1
517    while i > -1 and v[i] == '\\':
518        skip = not skip
519        i -= 1
520    for hx in hexbytes:
521        if skip:
522            skip = False
523            i = len(hx) - 1
524            while i > -1 and hx[i] == '\\':
525                skip = not skip
526                i -= 1
527            v += prefix
528            v += hx
529            continue
530        hxb = ""
531        i = 0
532        hxblen = 4
533        if prefix == "\\U":
534            hxblen = 8
535        while i < hxblen:
536            try:
537                if not hx[i].lower() in hexchars:
538                    raise IndexError("This is a hack")
539            except IndexError:
540                raise TomlDecodeError("Invalid escape sequence")
541            hxb += hx[i].lower()
542            i += 1
543        v += unichr(int(hxb, 16))
544        v += unicode(hx[len(hxb):])
545    return v
546
547
548# Unescape TOML string values.
549
550# content after the \
551_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"']
552# What it should be replaced by
553_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"']
554# Used for substitution
555_escape_to_escapedchars = dict(zip(_escapes, _escapedchars))
556
557
558def _unescape(v):
559    """Unescape characters in a TOML string."""
560    i = 0
561    backslash = False
562    while i < len(v):
563        if backslash:
564            backslash = False
565            if v[i] in _escapes:
566                v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:]
567            elif v[i] == '\\':
568                v = v[:i - 1] + v[i:]
569            elif v[i] == 'u' or v[i] == 'U':
570                i += 1
571            else:
572                raise TomlDecodeError("Reserved escape sequence used")
573            continue
574        elif v[i] == '\\':
575            backslash = True
576        i += 1
577    return v
578
579
580def _load_value(v, _dict, strictly_valid=True):
581    if not v:
582        raise TomlDecodeError("Empty value is invalid")
583    if v == 'true':
584        return (True, "bool")
585    elif v == 'false':
586        return (False, "bool")
587    elif v[0] == '"':
588        testv = v[1:].split('"')
589        if testv[0] == '' and testv[1] == '':
590            testv = testv[2:-2]
591        closed = False
592        for tv in testv:
593            if tv == '':
594                closed = True
595            else:
596                oddbackslash = False
597                try:
598                    i = -1
599                    j = tv[i]
600                    while j == '\\':
601                        oddbackslash = not oddbackslash
602                        i -= 1
603                        j = tv[i]
604                except IndexError:
605                    pass
606                if not oddbackslash:
607                    if closed:
608                        raise TomlDecodeError("Stuff after closed string. WTF?")
609                    else:
610                        closed = True
611        escapeseqs = v.split('\\')[1:]
612        backslash = False
613        for i in escapeseqs:
614            if i == '':
615                backslash = not backslash
616            else:
617                if i[0] not in _escapes and (i[0] != 'u' and i[0] != 'U' and
618                                             not backslash):
619                    raise TomlDecodeError("Reserved escape sequence used")
620                if backslash:
621                    backslash = False
622        for prefix in ["\\u", "\\U"]:
623            if prefix in v:
624                hexbytes = v.split(prefix)
625                v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], prefix)
626        v = _unescape(v)
627        if v[1] == '"' and (len(v) < 3 or v[1] == v[2]):
628            v = v[2:-2]
629        return (v[1:-1], "str")
630    elif v[0] == "'":
631        if v[1] == "'" and (len(v) < 3 or v[1] == v[2]):
632            v = v[2:-2]
633        return (v[1:-1], "str")
634    elif v[0] == '[':
635        return (_load_array(v, _dict), "array")
636    elif v[0] == '{':
637        inline_object = _get_empty_inline_table(_dict)
638        _load_inline_object(v, inline_object, _dict)
639        return (inline_object, "inline_object")
640    else:
641        parsed_date = _load_date(v)
642        if parsed_date is not None:
643            return (parsed_date, "date")
644        if not strictly_valid:
645            raise TomlDecodeError("Weirdness with leading zeroes or underscores"
646                                  " in your number.")
647        itype = "int"
648        neg = False
649        if v[0] == '-':
650            neg = True
651            v = v[1:]
652        elif v[0] == '+':
653            v = v[1:]
654        v = v.replace('_', '')
655        if '.' in v or 'e' in v or 'E' in v:
656            if '.' in v and v.split('.', 1)[1] == '':
657                raise TomlDecodeError("This float is missing digits after "
658                                      "the point")
659            if v[0] not in '0123456789':
660                raise TomlDecodeError("This float doesn't have a leading digit")
661            v = float(v)
662            itype = "float"
663        else:
664            v = int(v)
665        if neg:
666            return (0 - v, itype)
667        return (v, itype)
668
669
670def _load_array(a, _dict):
671    atype = None
672    retval = []
673    a = a.strip()
674    if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip():
675        strarray = False
676        tmpa = a[1:-1].strip()
677        if tmpa != '' and (tmpa[0] == '"' or tmpa[0] == "'"):
678            strarray = True
679        if not a[1:-1].strip().startswith('{'):
680            a = a[1:-1].split(',')
681        else:
682            # a is an inline object, we must find the matching parenthesis
683            # to define groups
684            new_a = []
685            start_group_index = 1
686            end_group_index = 2
687            in_str = False
688            while end_group_index < len(a[1:]):
689                if a[end_group_index] == '"' or a[end_group_index] == "'":
690                    in_str = not in_str
691                if in_str or a[end_group_index] != '}':
692                    end_group_index += 1
693                    continue
694
695                # Increase end_group_index by 1 to get the closing bracket
696                end_group_index += 1
697                new_a.append(a[start_group_index:end_group_index])
698
699                # The next start index is at least after the closing bracket, a
700                # closing bracket can be followed by a comma since we are in
701                # an array.
702                start_group_index = end_group_index + 1
703                while (start_group_index < len(a[1:]) and
704                       a[start_group_index] != '{'):
705                    start_group_index += 1
706                end_group_index = start_group_index + 1
707            a = new_a
708        b = 0
709        if strarray:
710            while b < len(a) - 1:
711                ab = a[b].strip()
712                while ab[-1] != ab[0] or (len(ab) > 2 and
713                                          ab[0] == ab[1] == ab[2] and
714                                          ab[-2] != ab[0] and ab[-3] != ab[0]):
715                    a[b] = a[b] + ',' + a[b + 1]
716                    ab = a[b].strip()
717                    if b < len(a) - 2:
718                        a = a[:b + 1] + a[b + 2:]
719                    else:
720                        a = a[:b + 1]
721                b += 1
722    else:
723        al = list(a[1:-1])
724        a = []
725        openarr = 0
726        j = 0
727        for i in _range(len(al)):
728            if al[i] == '[':
729                openarr += 1
730            elif al[i] == ']':
731                openarr -= 1
732            elif al[i] == ',' and not openarr:
733                a.append(''.join(al[j:i]))
734                j = i + 1
735        a.append(''.join(al[j:]))
736    for i in _range(len(a)):
737        a[i] = a[i].strip()
738        if a[i] != '':
739            nval, ntype = _load_value(a[i], _dict)
740            if atype:
741                if ntype != atype:
742                    raise TomlDecodeError("Not a homogeneous array")
743            else:
744                atype = ntype
745            retval.append(nval)
746    return retval
747
748
749def dump(o, f):
750    """Writes out dict as toml to a file
751
752    Args:
753        o: Object to dump into toml
754        f: File descriptor where the toml should be stored
755
756    Returns:
757        String containing the toml corresponding to dictionary
758
759    Raises:
760        TypeError: When anything other than file descriptor is passed
761    """
762
763    if not f.write:
764        raise TypeError("You can only dump an object to a file descriptor")
765    d = dumps(o)
766    f.write(d)
767    return d
768
769
770def dumps(o, preserve=False):
771    """Stringifies input dict as toml
772
773    Args:
774        o: Object to dump into toml
775
776        preserve: Boolean parameter. If true, preserve inline tables.
777
778    Returns:
779        String containing the toml corresponding to dict
780    """
781
782    retval = ""
783    addtoretval, sections = _dump_sections(o, "")
784    retval += addtoretval
785    while sections != {}:
786        newsections = {}
787        for section in sections:
788            addtoretval, addtosections = _dump_sections(sections[section],
789                                                        section, preserve)
790            if addtoretval or (not addtoretval and not addtosections):
791                if retval and retval[-2:] != "\n\n":
792                    retval += "\n"
793                retval += "[" + section + "]\n"
794                if addtoretval:
795                    retval += addtoretval
796            for s in addtosections:
797                newsections[section + "." + s] = addtosections[s]
798        sections = newsections
799    return retval
800
801
802def _dump_sections(o, sup, preserve=False):
803    retstr = ""
804    if sup != "" and sup[-1] != ".":
805        sup += '.'
806    retdict = o.__class__()
807    arraystr = ""
808    for section in o:
809        section = unicode(section)
810        qsection = section
811        if not re.match(r'^[A-Za-z0-9_-]+$', section):
812            if '"' in section:
813                qsection = "'" + section + "'"
814            else:
815                qsection = '"' + section + '"'
816        if not isinstance(o[section], dict):
817            arrayoftables = False
818            if isinstance(o[section], list):
819                for a in o[section]:
820                    if isinstance(a, dict):
821                        arrayoftables = True
822            if arrayoftables:
823                for a in o[section]:
824                    arraytabstr = "\n"
825                    arraystr += "[[" + sup + qsection + "]]\n"
826                    s, d = _dump_sections(a, sup + qsection)
827                    if s:
828                        if s[0] == "[":
829                            arraytabstr += s
830                        else:
831                            arraystr += s
832                    while d != {}:
833                        newd = {}
834                        for dsec in d:
835                            s1, d1 = _dump_sections(d[dsec], sup + qsection +
836                                                    "." + dsec)
837                            if s1:
838                                arraytabstr += ("[" + sup + qsection + "." +
839                                                dsec + "]\n")
840                                arraytabstr += s1
841                            for s1 in d1:
842                                newd[dsec + "." + s1] = d1[s1]
843                        d = newd
844                    arraystr += arraytabstr
845            else:
846                if o[section] is not None:
847                    retstr += (qsection + " = " +
848                               unicode(_dump_value(o[section])) + '\n')
849        elif preserve and isinstance(o[section], InlineTableDict):
850            retstr += (qsection + " = " + _dump_inline_table(o[section]))
851        else:
852            retdict[qsection] = o[section]
853    retstr += arraystr
854    return (retstr, retdict)
855
856
857def _dump_inline_table(section):
858    """Preserve inline table in its compact syntax instead of expanding
859    into subsection.
860
861    https://github.com/toml-lang/toml#user-content-inline-table
862    """
863    retval = ""
864    if isinstance(section, dict):
865        val_list = []
866        for k, v in section.items():
867            val = _dump_inline_table(v)
868            val_list.append(k + " = " + val)
869        retval += "{ " + ", ".join(val_list) + " }\n"
870        return retval
871    else:
872        return unicode(_dump_value(section))
873
874
875def _dump_value(v):
876    dump_funcs = {
877        str: lambda: _dump_str(v),
878        unicode: lambda: _dump_str(v),
879        list: lambda: _dump_list(v),
880        bool: lambda: unicode(v).lower(),
881        float: lambda: _dump_float(v),
882        datetime.datetime: lambda: v.isoformat(),
883    }
884    # Lookup function corresponding to v's type
885    dump_fn = dump_funcs.get(type(v))
886    # Evaluate function (if it exists) else return v
887    return dump_fn() if dump_fn is not None else v
888
889
890def _dump_str(v):
891    v = "%r" % v
892    if v[0] == 'u':
893        v = v[1:]
894    singlequote = v.startswith("'")
895    v = v[1:-1]
896    if singlequote:
897        v = v.replace("\\'", "'")
898        v = v.replace('"', '\\"')
899    v = v.replace("\\x", "\\u00")
900    return unicode('"' + v + '"')
901
902
903def _dump_list(v):
904    t = []
905    retval = "["
906    for u in v:
907        t.append(_dump_value(u))
908    while t != []:
909        s = []
910        for u in t:
911            if isinstance(u, list):
912                for r in u:
913                    s.append(r)
914            else:
915                retval += " " + unicode(u) + ","
916        t = s
917    retval += "]"
918    return retval
919
920
921def _dump_float(v):
922    return "{0:.16}".format(v).replace("e+0", "e+").replace("e-0", "e-")
923