From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- python/pytoml/PKG-INFO | 10 ++ python/pytoml/pytoml/__init__.py | 3 + python/pytoml/pytoml/core.py | 13 ++ python/pytoml/pytoml/parser.py | 366 +++++++++++++++++++++++++++++++++++++++ python/pytoml/pytoml/writer.py | 120 +++++++++++++ python/pytoml/setup.cfg | 5 + python/pytoml/setup.py | 17 ++ python/pytoml/test/test.py | 100 +++++++++++ 8 files changed, 634 insertions(+) create mode 100644 python/pytoml/PKG-INFO create mode 100644 python/pytoml/pytoml/__init__.py create mode 100644 python/pytoml/pytoml/core.py create mode 100644 python/pytoml/pytoml/parser.py create mode 100644 python/pytoml/pytoml/writer.py create mode 100644 python/pytoml/setup.cfg create mode 100644 python/pytoml/setup.py create mode 100644 python/pytoml/test/test.py (limited to 'python/pytoml') diff --git a/python/pytoml/PKG-INFO b/python/pytoml/PKG-INFO new file mode 100644 index 000000000..844436f95 --- /dev/null +++ b/python/pytoml/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: pytoml +Version: 0.1.10 +Summary: A parser for TOML-0.4.0 +Home-page: https://github.com/avakar/pytoml +Author: Martin Vejnár +Author-email: avakar@ratatanek.cz +License: MIT +Description: UNKNOWN +Platform: UNKNOWN diff --git a/python/pytoml/pytoml/__init__.py b/python/pytoml/pytoml/__init__.py new file mode 100644 index 000000000..222a1967f --- /dev/null +++ b/python/pytoml/pytoml/__init__.py @@ -0,0 +1,3 @@ +from .core import TomlError +from .parser import load, loads +from .writer import dump, dumps diff --git a/python/pytoml/pytoml/core.py b/python/pytoml/pytoml/core.py new file mode 100644 index 000000000..0fcada48c --- /dev/null +++ b/python/pytoml/pytoml/core.py @@ -0,0 +1,13 @@ +class TomlError(RuntimeError): + def __init__(self, message, line, col, filename): + RuntimeError.__init__(self, message, line, col, filename) + self.message = message + self.line = line + self.col = col + self.filename = filename + + def __str__(self): + return '{}({}, {}): {}'.format(self.filename, self.line, self.col, self.message) + + def __repr__(self): + return 'TomlError({!r}, {!r}, {!r}, {!r})'.format(self.message, self.line, self.col, self.filename) diff --git a/python/pytoml/pytoml/parser.py b/python/pytoml/pytoml/parser.py new file mode 100644 index 000000000..d4c4ac187 --- /dev/null +++ b/python/pytoml/pytoml/parser.py @@ -0,0 +1,366 @@ +import string, re, sys, datetime +from .core import TomlError + +if sys.version_info[0] == 2: + _chr = unichr +else: + _chr = chr + +def load(fin, translate=lambda t, x, v: v): + return loads(fin.read(), translate=translate, filename=fin.name) + +def loads(s, filename='', translate=lambda t, x, v: v): + if isinstance(s, bytes): + s = s.decode('utf-8') + + s = s.replace('\r\n', '\n') + + root = {} + tables = {} + scope = root + + src = _Source(s, filename=filename) + ast = _p_toml(src) + + def error(msg): + raise TomlError(msg, pos[0], pos[1], filename) + + def process_value(v): + kind, text, value, pos = v + if kind == 'str' and value.startswith('\n'): + value = value[1:] + if kind == 'array': + if value and any(k != value[0][0] for k, t, v, p in value[1:]): + error('array-type-mismatch') + value = [process_value(item) for item in value] + elif kind == 'table': + value = dict([(k, process_value(value[k])) for k in value]) + return translate(kind, text, value) + + for kind, value, pos in ast: + if kind == 'kv': + k, v = value + if k in scope: + error('duplicate_keys. Key "{0}" was used more than once.'.format(k)) + scope[k] = process_value(v) + else: + is_table_array = (kind == 'table_array') + cur = tables + for name in value[:-1]: + if isinstance(cur.get(name), list): + d, cur = cur[name][-1] + else: + d, cur = cur.setdefault(name, (None, {})) + + scope = {} + name = value[-1] + if name not in cur: + if is_table_array: + cur[name] = [(scope, {})] + else: + cur[name] = (scope, {}) + elif isinstance(cur[name], list): + if not is_table_array: + error('table_type_mismatch') + cur[name].append((scope, {})) + else: + if is_table_array: + error('table_type_mismatch') + old_scope, next_table = cur[name] + if old_scope is not None: + error('duplicate_tables') + cur[name] = (scope, next_table) + + def merge_tables(scope, tables): + if scope is None: + scope = {} + for k in tables: + if k in scope: + error('key_table_conflict') + v = tables[k] + if isinstance(v, list): + scope[k] = [merge_tables(sc, tbl) for sc, tbl in v] + else: + scope[k] = merge_tables(v[0], v[1]) + return scope + + return merge_tables(root, tables) + +class _Source: + def __init__(self, s, filename=None): + self.s = s + self._pos = (1, 1) + self._last = None + self._filename = filename + self.backtrack_stack = [] + + def last(self): + return self._last + + def pos(self): + return self._pos + + def fail(self): + return self._expect(None) + + def consume_dot(self): + if self.s: + self._last = self.s[0] + self.s = self[1:] + self._advance(self._last) + return self._last + return None + + def expect_dot(self): + return self._expect(self.consume_dot()) + + def consume_eof(self): + if not self.s: + self._last = '' + return True + return False + + def expect_eof(self): + return self._expect(self.consume_eof()) + + def consume(self, s): + if self.s.startswith(s): + self.s = self.s[len(s):] + self._last = s + self._advance(s) + return True + return False + + def expect(self, s): + return self._expect(self.consume(s)) + + def consume_re(self, re): + m = re.match(self.s) + if m: + self.s = self.s[len(m.group(0)):] + self._last = m + self._advance(m.group(0)) + return m + return None + + def expect_re(self, re): + return self._expect(self.consume_re(re)) + + def __enter__(self): + self.backtrack_stack.append((self.s, self._pos)) + + def __exit__(self, type, value, traceback): + if type is None: + self.backtrack_stack.pop() + else: + self.s, self._pos = self.backtrack_stack.pop() + return type == TomlError + + def commit(self): + self.backtrack_stack[-1] = (self.s, self._pos) + + def _expect(self, r): + if not r: + raise TomlError('msg', self._pos[0], self._pos[1], self._filename) + return r + + def _advance(self, s): + suffix_pos = s.rfind('\n') + if suffix_pos == -1: + self._pos = (self._pos[0], self._pos[1] + len(s)) + else: + self._pos = (self._pos[0] + s.count('\n'), len(s) - suffix_pos) + +_ews_re = re.compile(r'(?:[ \t]|#[^\n]*\n|#[^\n]*\Z|\n)*') +def _p_ews(s): + s.expect_re(_ews_re) + +_ws_re = re.compile(r'[ \t]*') +def _p_ws(s): + s.expect_re(_ws_re) + +_escapes = { 'b': '\b', 'n': '\n', 'r': '\r', 't': '\t', '"': '"', '\'': '\'', + '\\': '\\', '/': '/', 'f': '\f' } + +_basicstr_re = re.compile(r'[^"\\\000-\037]*') +_short_uni_re = re.compile(r'u([0-9a-fA-F]{4})') +_long_uni_re = re.compile(r'U([0-9a-fA-F]{8})') +_escapes_re = re.compile('[bnrt"\'\\\\/f]') +_newline_esc_re = re.compile('\n[ \t\n]*') +def _p_basicstr_content(s, content=_basicstr_re): + res = [] + while True: + res.append(s.expect_re(content).group(0)) + if not s.consume('\\'): + break + if s.consume_re(_newline_esc_re): + pass + elif s.consume_re(_short_uni_re) or s.consume_re(_long_uni_re): + res.append(_chr(int(s.last().group(1), 16))) + else: + s.expect_re(_escapes_re) + res.append(_escapes[s.last().group(0)]) + return ''.join(res) + +_key_re = re.compile(r'[0-9a-zA-Z-_]+') +def _p_key(s): + with s: + s.expect('"') + r = _p_basicstr_content(s, _basicstr_re) + s.expect('"') + return r + return s.expect_re(_key_re).group(0) + +_float_re = re.compile(r'[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?(?:\d(?:_?\d)*))?') +_datetime_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:Z|([+-]\d{2}):(\d{2}))') + +_basicstr_ml_re = re.compile(r'(?:(?:|"|"")[^"\\\000-\011\013-\037])*') +_litstr_re = re.compile(r"[^'\000-\037]*") +_litstr_ml_re = re.compile(r"(?:(?:|'|'')(?:[^'\000-\011\013-\037]))*") +def _p_value(s): + pos = s.pos() + + if s.consume('true'): + return 'bool', s.last(), True, pos + if s.consume('false'): + return 'bool', s.last(), False, pos + + if s.consume('"'): + if s.consume('""'): + r = _p_basicstr_content(s, _basicstr_ml_re) + s.expect('"""') + else: + r = _p_basicstr_content(s, _basicstr_re) + s.expect('"') + return 'str', r, r, pos + + if s.consume('\''): + if s.consume('\'\''): + r = s.expect_re(_litstr_ml_re).group(0) + s.expect('\'\'\'') + else: + r = s.expect_re(_litstr_re).group(0) + s.expect('\'') + return 'str', r, r, pos + + if s.consume_re(_datetime_re): + m = s.last() + s0 = m.group(0) + r = map(int, m.groups()[:6]) + if m.group(7): + micro = float(m.group(7)) + else: + micro = 0 + + if m.group(8): + g = int(m.group(8), 10) * 60 + int(m.group(9), 10) + tz = _TimeZone(datetime.timedelta(0, g * 60)) + else: + tz = _TimeZone(datetime.timedelta(0, 0)) + + y, m, d, H, M, S = r + dt = datetime.datetime(y, m, d, H, M, S, int(micro * 1000000), tz) + return 'datetime', s0, dt, pos + + if s.consume_re(_float_re): + m = s.last().group(0) + r = m.replace('_','') + if '.' in m or 'e' in m or 'E' in m: + return 'float', m, float(r), pos + else: + return 'int', m, int(r, 10), pos + + if s.consume('['): + items = [] + with s: + while True: + _p_ews(s) + items.append(_p_value(s)) + s.commit() + _p_ews(s) + s.expect(',') + s.commit() + _p_ews(s) + s.expect(']') + return 'array', None, items, pos + + if s.consume('{'): + _p_ws(s) + items = {} + if not s.consume('}'): + k = _p_key(s) + _p_ws(s) + s.expect('=') + _p_ws(s) + items[k] = _p_value(s) + _p_ws(s) + while s.consume(','): + _p_ws(s) + k = _p_key(s) + _p_ws(s) + s.expect('=') + _p_ws(s) + items[k] = _p_value(s) + _p_ws(s) + s.expect('}') + return 'table', None, items, pos + + s.fail() + +def _p_stmt(s): + pos = s.pos() + if s.consume( '['): + is_array = s.consume('[') + _p_ws(s) + keys = [_p_key(s)] + _p_ws(s) + while s.consume('.'): + _p_ws(s) + keys.append(_p_key(s)) + _p_ws(s) + s.expect(']') + if is_array: + s.expect(']') + return 'table_array' if is_array else 'table', keys, pos + + key = _p_key(s) + _p_ws(s) + s.expect('=') + _p_ws(s) + value = _p_value(s) + return 'kv', (key, value), pos + +_stmtsep_re = re.compile(r'(?:[ \t]*(?:#[^\n]*)?\n)+[ \t]*') +def _p_toml(s): + stmts = [] + _p_ews(s) + with s: + stmts.append(_p_stmt(s)) + while True: + s.commit() + s.expect_re(_stmtsep_re) + stmts.append(_p_stmt(s)) + _p_ews(s) + s.expect_eof() + return stmts + +class _TimeZone(datetime.tzinfo): + def __init__(self, offset): + self._offset = offset + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return None + + def tzname(self, dt): + m = self._offset.total_seconds() // 60 + if m < 0: + res = '-' + m = -m + else: + res = '+' + h = m // 60 + m = m - h * 60 + return '{}{:.02}{:.02}'.format(res, h, m) diff --git a/python/pytoml/pytoml/writer.py b/python/pytoml/pytoml/writer.py new file mode 100644 index 000000000..2c9f7c69d --- /dev/null +++ b/python/pytoml/pytoml/writer.py @@ -0,0 +1,120 @@ +from __future__ import unicode_literals +import io, datetime, sys + +if sys.version_info[0] == 3: + long = int + unicode = str + + +def dumps(obj, sort_keys=False): + fout = io.StringIO() + dump(fout, obj, sort_keys=sort_keys) + return fout.getvalue() + + +_escapes = {'\n': 'n', '\r': 'r', '\\': '\\', '\t': 't', '\b': 'b', '\f': 'f', '"': '"'} + + +def _escape_string(s): + res = [] + start = 0 + + def flush(): + if start != i: + res.append(s[start:i]) + return i + 1 + + i = 0 + while i < len(s): + c = s[i] + if c in '"\\\n\r\t\b\f': + start = flush() + res.append('\\' + _escapes[c]) + elif ord(c) < 0x20: + start = flush() + res.append('\\u%04x' % ord(c)) + i += 1 + + flush() + return '"' + ''.join(res) + '"' + + +def _escape_id(s): + if any(not c.isalnum() and c not in '-_' for c in s): + return _escape_string(s) + return s + + +def _format_list(v): + return '[{0}]'.format(', '.join(_format_value(obj) for obj in v)) + +# Formula from: +# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds +# Once support for py26 is dropped, this can be replaced by td.total_seconds() +def _total_seconds(td): + return ((td.microseconds + + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.0**6) + +def _format_value(v): + if isinstance(v, bool): + return 'true' if v else 'false' + if isinstance(v, int) or isinstance(v, long): + return unicode(v) + if isinstance(v, float): + return '{0:.17f}'.format(v) + elif isinstance(v, unicode) or isinstance(v, bytes): + return _escape_string(v) + elif isinstance(v, datetime.datetime): + offs = v.utcoffset() + offs = _total_seconds(offs) // 60 if offs is not None else 0 + + if offs == 0: + suffix = 'Z' + else: + if offs > 0: + suffix = '+' + else: + suffix = '-' + offs = -offs + suffix = '{0}{1:.02}{2:.02}'.format(suffix, offs // 60, offs % 60) + + if v.microsecond: + return v.strftime('%Y-%m-%dT%H:%M:%S.%f') + suffix + else: + return v.strftime('%Y-%m-%dT%H:%M:%S') + suffix + elif isinstance(v, list): + return _format_list(v) + else: + raise RuntimeError(v) + + +def dump(fout, obj, sort_keys=False): + tables = [((), obj, False)] + + while tables: + if sort_keys: + tables.sort(key=lambda tup: tup[0], reverse=True) + name, table, is_array = tables.pop() + if name: + section_name = '.'.join(_escape_id(c) for c in name) + if is_array: + fout.write('[[{0}]]\n'.format(section_name)) + else: + fout.write('[{0}]\n'.format(section_name)) + + table_keys = sorted(table.keys()) if sort_keys else table.keys() + for k in table_keys: + v = table[k] + if isinstance(v, dict): + tables.append((name + (k,), v, False)) + elif isinstance(v, list) and v and all(isinstance(o, dict) for o in v): + tables.extend((name + (k,), d, True) for d in reversed(v)) + elif v is None: + # based on mojombo's comment: https://github.com/toml-lang/toml/issues/146#issuecomment-25019344 + fout.write( + '#{} = null # To use: uncomment and replace null with value\n'.format(_escape_id(k))) + else: + fout.write('{0} = {1}\n'.format(_escape_id(k), _format_value(v))) + + if tables: + fout.write('\n') diff --git a/python/pytoml/setup.cfg b/python/pytoml/setup.cfg new file mode 100644 index 000000000..b14b0bc3d --- /dev/null +++ b/python/pytoml/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/python/pytoml/setup.py b/python/pytoml/setup.py new file mode 100644 index 000000000..98c08a540 --- /dev/null +++ b/python/pytoml/setup.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# coding: utf-8 + +from setuptools import setup + +setup( + name='pytoml', + version='0.1.10', + + description='A parser for TOML-0.4.0', + author='Martin Vejnár', + author_email='avakar@ratatanek.cz', + url='https://github.com/avakar/pytoml', + license='MIT', + + packages=['pytoml'], + ) diff --git a/python/pytoml/test/test.py b/python/pytoml/test/test.py new file mode 100644 index 000000000..53fcd229d --- /dev/null +++ b/python/pytoml/test/test.py @@ -0,0 +1,100 @@ +import os, json, sys, io, traceback, argparse +import pytoml as toml + +# Formula from: +# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds +# Once support for py26 is dropped, this can be replaced by td.total_seconds() +def _total_seconds(td): + return ((td.microseconds + + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.0**6) + +def _testbench_literal(type, text, value): + if type == 'table': + return value + if type == 'array': + return { 'type': 'array', 'value': value } + if type == 'datetime': + offs = _total_seconds(value.tzinfo.utcoffset(value)) // 60 + offs = 'Z' if offs == 0 else '{}{}:{}'.format('-' if offs < 0 else '-', abs(offs) // 60, abs(offs) % 60) + v = '{0:04}-{1:02}-{2:02}T{3:02}:{4:02}:{5:02}{6}'.format(value.year, value.month, value.day, value.hour, value.minute, value.second, offs) + return { 'type': 'datetime', 'value': v } + if type == 'bool': + return { 'type': 'bool', 'value': 'true' if value else 'false' } + if type == 'float': + return { 'type': 'float', 'value': value } + if type == 'str': + return { 'type': 'string', 'value': value } + if type == 'int': + return { 'type': 'integer', 'value': str(value) } + +def adjust_bench(v): + if isinstance(v, dict): + if v.get('type') == 'float': + v['value'] = float(v['value']) + return v + return dict([(k, adjust_bench(v[k])) for k in v]) + if isinstance(v, list): + return [adjust_bench(v) for v in v] + return v + +def _main(): + ap = argparse.ArgumentParser() + ap.add_argument('-d', '--dir', action='append') + ap.add_argument('testcase', nargs='*') + args = ap.parse_args() + + if not args.dir: + args.dir = [os.path.join(os.path.split(__file__)[0], 'toml-test/tests')] + + succeeded = [] + failed = [] + + for path in args.dir: + if not os.path.isdir(path): + print('error: not a dir: {0}'.format(path)) + return 2 + for top, dirnames, fnames in os.walk(path): + for fname in fnames: + if not fname.endswith('.toml'): + continue + + if args.testcase and not any(arg in fname for arg in args.testcase): + continue + + parse_error = None + try: + with open(os.path.join(top, fname), 'rb') as fin: + parsed = toml.load(fin) + except toml.TomlError: + parsed = None + parse_error = sys.exc_info() + else: + dumped = toml.dumps(parsed) + parsed2 = toml.loads(dumped) + if parsed != parsed2: + failed.append((fname, None)) + continue + + with open(os.path.join(top, fname), 'rb') as fin: + parsed = toml.load(fin, translate=_testbench_literal) + + try: + with io.open(os.path.join(top, fname[:-5] + '.json'), 'rt', encoding='utf-8') as fin: + bench = json.load(fin) + except IOError: + bench = None + + if parsed != adjust_bench(bench): + failed.append((fname, parsed, bench, parse_error)) + else: + succeeded.append(fname) + + for f, parsed, bench, e in failed: + print('failed: {}\n{}\n{}'.format(f, json.dumps(parsed, indent=4), json.dumps(bench, indent=4))) + if e: + traceback.print_exception(*e) + print('succeeded: {0}'.format(len(succeeded))) + return 1 if failed or not succeeded else 0 + +if __name__ == '__main__': + sys.exit(_main()) -- cgit v1.2.3