#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-pytoml. Emits `key=value' lines, one per line, in file order; the suite reads them back with grep. A python3 start on this arm is tens of seconds and every measurement below is a `re.match' over a TOML document under 200 bytes, so the start is the entire cost of the suite and taking it once is the difference between a 30-second run and a twenty-minute one. THE FIXTURES ARE WRITTEN HERE, NOT SHIPPED. Every document this library parses is a few lines of text, so a fixture needs no file at all -- `loads' takes the string directly and `dumps' returns one. Two of the cases DO need a real file, because `load(fin)' reads `fin.name' to fill in `TomlError.filename', and those write into a private directory under $TMPDIR and remove it again. TWO RULES THE VALUES OBEY. The runner parses its input line by line, so no value may contain a newline. Every value is put through `canon' or `esc' below, both of which escape a newline rather than emitting one. And no value may depend on the encoding stdout happens to be in. `esc' escapes every codepoint above 0x7e as `\\uXXXX', so a case that is ABOUT non-ASCII handling -- and several are -- states its answer in ASCII. """ import datetime import io import os import shutil import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) try: import pytoml from pytoml.utils import (parse_rfc3339, format_rfc3339, rfc3339_re, _TimeZone) except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) def esc(s): """One line, pure ASCII, and reversible enough to read. A raw newline would end the value early and a raw non-ASCII character would make the answer depend on stdout's encoding, so both are escaped. `\\' is escaped first so the mapping stays unambiguous.""" out = [] for ch in s: o = ord(ch) if ch == '\\': out.append('\\\\') elif ch == '\n': out.append('\\n') elif ch == '\r': out.append('\\r') elif ch == '\t': out.append('\\t') elif 0x20 <= o <= 0x7e: out.append(ch) elif o <= 0xffff: out.append('\\u%04x' % o) else: out.append('\\U%08x' % o) return ''.join(out) def canon(v): """A deterministic, single-line, ASCII rendering of a parsed document. The TYPE is part of the rendering -- `i:1' and `f:1.0' are different answers, and TOML 0.4.0 says so too, which is what the array-type cases turn on. Dicts are sorted by key: this library parses into a plain dict and the suite is not making a statement about iteration order here (the `dumps-*' cases, where order IS observable in the output text, state it directly instead).""" if v is None: return 'null' if isinstance(v, bool): return 'true' if v else 'false' if isinstance(v, int): return 'i:%d' % v if isinstance(v, float): return 'f:%r' % v if isinstance(v, str): return 's:%s' % esc(v) if isinstance(v, datetime.datetime): return 'dt:%s' % v.isoformat() if isinstance(v, list): return '[%s]' % ','.join(canon(x) for x in v) if isinstance(v, dict): return '{%s}' % ','.join('%s=%s' % (esc(k), canon(v[k])) for k in sorted(v)) return 'other:%s' % type(v).__name__ def P(text, **kw): """`canon' of the parse, or `!TomlError(line,col,message)' on a refusal. A refusal that is NOT a TomlError is rendered as `!' with no message -- see the `err-bad-datetime' family, where the whole point is that the exception escaping is not the one the library documents.""" try: return canon(pytoml.loads(text, **kw)) except pytoml.TomlError as exc: return '!TomlError(%s,%s,%s)' % (exc.line, exc.col, esc(exc.message)) except Exception as exc: return '!%s' % type(exc).__name__ def D(obj, **kw): """`esc' of what `dumps' wrote, or `!'.""" try: return esc(pytoml.dumps(obj, **kw)) except Exception as exc: return '!%s' % type(exc).__name__ def RT(obj, **kw): """`yes' when `loads(dumps(x)) == x', else why not. THIS IS THE CASE FAMILY THAT PINS THE WRITER. Every other writer case asserts the text it produced, which a reformatting would move; this one asserts the property a caller actually relies on, and stays true across any reformatting that keeps the meaning.""" try: s = pytoml.dumps(obj, **kw) except Exception as exc: return '!dump-%s' % type(exc).__name__ try: back = pytoml.loads(s) except pytoml.TomlError as exc: return '!load-TomlError(%s)' % esc(exc.message) except Exception as exc: return '!load-%s' % type(exc).__name__ return 'yes' if back == obj else 'no:%s' % canon(back) # ============================================================================ # IDENTITY -- which copy answered. Every case below is about the package # named here. # ============================================================================ emit('package_file', pytoml.__file__) emit('is_package', 'yes' if hasattr(pytoml, '__path__') else 'no') emit('submodule_files', ','.join( os.path.basename(m.__file__) for m in (pytoml.core, pytoml.parser, pytoml.utils, pytoml.writer, pytoml.test))) emit('public_names', ','.join(sorted(n for n in dir(pytoml) if not n.startswith('_')))) emit('has___version__', 'yes' if hasattr(pytoml, '__version__') else 'no') emit('has___all__', 'yes' if hasattr(pytoml, '__all__') else 'no') # The dist-info is read as a FILE rather than through `importlib.metadata', # which is not in this image's python3-core split. Reading it beside # `pytoml.__file__' also makes it a statement about the copy that answered # above, which querying the metadata API by name would not be. _sitedir = os.path.dirname(os.path.dirname(os.path.abspath(pytoml.__file__))) _diname = 'pytoml-0.1.21.dist-info' try: with open(os.path.join(_sitedir, _diname, 'METADATA')) as _fh: _meta = _fh.read() emit('dist_version', [l.split(': ', 1)[1] for l in _meta.splitlines() if l.startswith('Version: ')][0]) emit('dist_name', [l.split(': ', 1)[1] for l in _meta.splitlines() if l.startswith('Name: ')][0]) except Exception as exc: emit('dist_version', '!%s' % type(exc).__name__) emit('dist_name', '!%s' % type(exc).__name__) # THE ARM'S FLOAT IS float64, WHICH EVERY FLOAT CASE BELOW ASSUMES. The # literals in `float_*' and `dumps_float' are the shortest repr of an IEEE # double; on a build whose Python used a narrower float they would all be wrong # together, and this case says which of the two happened. emit('float_mant_dig', str(sys.float_info.mant_dig)) emit('float_max_10_exp', str(sys.float_info.max_10_exp)) # TomlError's shape. It subclasses RuntimeError -- not ValueError, which is # what a caller writing `except ValueError' would assume -- and it passes all # four fields to RuntimeError.__init__, so they show up in .args as well as on # the attributes. emit('tomlerror_base', pytoml.TomlError.__mro__[1].__name__) try: pytoml.loads('a = @') except pytoml.TomlError as exc: emit('tomlerror_args', esc(repr(exc.args))) emit('tomlerror_str', esc(str(exc))) emit('tomlerror_repr', esc(repr(exc))) # ============================================================================ # SCALARS -- every type the 0.4.0 grammar defines. # ============================================================================ emit('str_basic', P('a = "hi"')) emit('str_empty', P('a = ""')) emit('str_literal', P("a = 'C:\\Users\\n'")) emit('str_ml_basic', P('a = """\nline1\nline2"""')) emit('str_ml_basic_keepnl', P('a = """\n\nfirst"""')) emit('str_ml_literal', P("a = '''\nx\\y'''")) emit('str_ml_quote_inside', P('a = """say "hi" ok"""')) emit('str_ml_lit_apos_inside', P("a = '''it's'''")) emit('str_escapes', P(r'a = "\t\n\r\f\b\\\""')) emit('str_line_continuation', P('a = """ab\\\n cd"""')) emit('str_esc_short_uni', P(r'a = "\u00e9"')) emit('str_esc_short_uni_upper', P(r'a = "\u00E9"')) emit('str_esc_long_uni', P(r'a = "\U0001F600"')) emit('str_raw_non_ascii', P('a = "h\u00e9llo \U0001f600"')) emit('int_plain', P('a = 42')) emit('int_zero', P('a = 0')) emit('int_plus', P('a = +99')) emit('int_minus', P('a = -17')) emit('int_underscores', P('a = 1_000_000')) emit('int_big', P('a = 1180591620717411303424')) emit('float_frac', P('a = 3.14')) emit('float_neg', P('a = -0.01')) emit('float_exp_plus', P('a = 5e+22')) emit('float_exp_bare', P('a = 1e6')) emit('float_exp_neg', P('a = 6.626e-34')) emit('float_underscores', P('a = 9_224_617.445_991_228_313')) emit('bool_true', P('a = true')) emit('bool_false', P('a = false')) # ============================================================================ # DATETIMES. # ============================================================================ emit('dt_zulu', P('a = 1979-05-27T07:32:00Z')) emit('dt_offset_neg', P('a = 1979-05-27T00:32:00-07:00')) emit('dt_offset_pos', P('a = 1979-05-27T00:32:00+05:30')) emit('dt_fractional', P('a = 1979-05-27T00:32:00.999999-07:00')) _dtz = pytoml.loads('a = 1979-05-27T07:32:00Z')['a'] _dto = pytoml.loads('a = 1979-05-27T00:32:00-07:00')['a'] _dth = pytoml.loads('a = 1979-05-27T00:32:00+05:30')['a'] emit('dt_tzinfo_type', type(_dtz.tzinfo).__name__) emit('dt_tzinfo_is_stdlib_utc', 'yes' if _dtz.tzinfo is datetime.timezone.utc else 'no') emit('dt_utcoffset_zulu', str(_dtz.utcoffset())) emit('dt_utcoffset_neg', str(_dto.utcoffset())) emit('dt_utcoffset_pos', str(_dth.utcoffset())) emit('dt_dst_is_none', 'yes' if _dtz.dst() is None else 'no') emit('dt_same_instant_equal', 'yes' if _dtz == _dto else 'no') emit('dt_equals_stdlib_utc', 'yes' if _dtz == datetime.datetime( 1979, 5, 27, 7, 32, tzinfo=datetime.timezone.utc) else 'no') # THE tzname BUG -- see the suite header. `_TimeZone.tzname' formats two # FLOATS with `{:.02}', which is a PRECISION of two significant digits, not a # width of two. A whole-hour offset survives it looking merely odd; a # half-hour one comes back in scientific notation. emit('tzname_zulu', esc(_dtz.tzname())) emit('tzname_neg', esc(_dto.tzname())) emit('tzname_half_hour', esc(_dth.tzname())) # ============================================================================ # ARRAYS. # ============================================================================ emit('arr_int', P('a = [1, 2, 3]')) emit('arr_str', P('a = ["x", "y"]')) emit('arr_empty', P('a = []')) emit('arr_nested', P('a = [[1, 2], [3]]')) emit('arr_deep', P('a = [[[1]]]')) emit('arr_trailing_comma', P('a = [1, 2, ]')) emit('arr_multiline_comment', P('a = [\n 1, # one\n 2,\n]')) # 0.4.0 REQUIRES A HOMOGENEOUS ARRAY and this parser enforces it -- but only at # the TOP level of the array, by comparing the parser's own kind strings. emit('arr_mixed_types', P('a = [1, "x"]')) emit('arr_int_and_float', P('a = [1, 1.0]')) emit('arr_nested_heterogeneous', P('a = [[1], ["x"]]')) # ============================================================================ # INLINE TABLES, TABLES, ARRAYS OF TABLES. # ============================================================================ emit('inline_table', P('a = {x = 1, y = "z"}')) emit('inline_empty', P('a = {}')) emit('inline_nested', P('a = {x = {y = 1}}')) emit('inline_with_array', P('a = {x = [1, 2]}')) emit('inline_trailing_comma', P('a = {x = 1,}')) emit('inline_newline_inside', P('a = {x = 1,\n y = 2}')) emit('table_simple', P('[a]\nx = 1')) emit('table_nested', P('[a]\nx = 1\n[a.b]\ny = 2')) emit('table_dotted_name', P('[a.b.c]\nx = 1')) emit('table_implicit_super', P('[a.b]\nx = 1')) emit('table_out_of_order', P('[a.b]\nx = 1\n[a]\ny = 2')) emit('table_quoted_name', P('["a.b"]\nx = 1')) emit('aot_simple', P('[[a]]\nx = 1\n[[a]]\nx = 2')) emit('aot_with_subtable', P('[[a]]\nx=1\n[a.b]\ny=2\n[[a]]\nx=2\n[a.b]\ny=3')) emit('aot_nested_aot', P('[[a]]\n[[a.b]]\nx=1\n[[a.b]]\nx=2')) emit('key_bare', P('a-b_c1 = 1')) emit('key_quoted_dot', P('"a.b" = 1')) emit('key_literal_space', P("'a b' = 1")) # DOTTED KEYS ARE A 0.5.0 FEATURE and this is a 0.4.0 parser: `a.b = 1' is a # syntax error, not a nested table. emit('key_dotted', P('a.b = 1')) emit('doc_empty', P('')) emit('doc_comment_only', P('# hi\n')) emit('doc_crlf', P('a = 1\r\nb = 2\r\n')) emit('doc_bytes_input', P(b'a = 1')) emit('doc_bom', P('\ufeffa = 1')) # ============================================================================ # ERRORS -- what a malformed document actually reports. # ============================================================================ # The five SEMANTIC errors are the only ones that carry a message a caller can # branch on. Everything else raises the literal placeholder `msg'. emit('err_dup_key', P('a = 1\na = 2')) emit('err_dup_key_later', P('x = 0\n\na = 1\na = 2')) emit('err_dup_table', P('[a]\nx = 1\n[a]\ny = 2')) emit('err_table_then_aot', P('[a]\nx=1\n[[a]]\ny=2')) emit('err_aot_then_table', P('[[a]]\nx=1\n[a]\ny=2')) emit('err_key_table_conflict', P('a = 1\n[a]\nx = 2')) emit('err_key_table_conflict_late', P('a = 1\nb = 2\nc = 3\n[a]\nx = 4')) emit('err_array_type_mismatch', P('a = [1, "x"]')) # ...and the placeholder family. THE COLUMN IS NOT THE ERROR SITE: the parser # backtracks to its last committed statement before reporting, so what is # reported is where the last GOOD statement ended. emit('err_unterminated_basic', P('a = "abc\nb = 1')) emit('err_unterminated_literal', P("a = 'abc\nb = 1")) emit('err_unterminated_ml', P('a = """abc\n')) emit('err_bad_escape', P(r'a = "\q"')) emit('err_escaped_solidus', P(r'a = "\/"')) emit('err_lone_surrogate', P(r'a = "\ud800"')) emit('err_control_in_string', P('a = "x\x01y"')) emit('err_bare_inf', P('a = inf')) emit('err_bare_nan', P('a = nan')) emit('err_leading_zero', P('a = 01')) emit('err_trailing_dot', P('a = 1.')) emit('err_missing_value', P('a = ')) emit('err_on_line_4', P('a = 1\nb = 2\nc = 3\nd = "unterminated\n')) emit('err_on_line_3', P('a = 1\nb = 2\nc = @\n')) emit('err_on_line_1', P('a = @\nb = 2\n')) # A DATE OUT OF RANGE DOES NOT RAISE TomlError. The RFC 3339 regex matches on # shape alone, so an impossible day reaches `datetime.datetime()' and its # ValueError escapes the parser -- with no line, no column and no filename. emit('err_bad_datetime_day', P('a = 1979-05-99T07:32:00Z')) emit('err_bad_datetime_month', P('a = 1979-13-27T07:32:00Z')) emit('err_bad_datetime_hour', P('a = 1979-05-27T99:32:00Z')) # LOCAL AND DATE-ONLY FORMS DO NOT EXIST IN 0.4.0. The offset is mandatory in # the regex, so a local datetime does not even reach the datetime branch: `1979' # parses as an integer and the parse dies on the `-' after it. emit('err_local_datetime', P('a = 1979-05-27T07:32:00')) emit('err_date_only', P('a = 1979-05-27')) emit('err_time_only', P('a = 07:32:00')) # FILENAME PLUMBING. `loads' defaults to `'; its `filename=' argument # and `load(fin)' reading `fin.name' are the two ways a caller gets a real path # into the message. try: pytoml.loads('a = @', filename='cfg.toml') except pytoml.TomlError as exc: emit('err_filename_arg', esc(str(exc))) try: pytoml.loads('a = @') except pytoml.TomlError as exc: emit('err_filename_default', esc(str(exc))) W = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'bst-pytoml-fx.%d' % os.getpid()) os.makedirs(W, exist_ok=True) _good = os.path.join(W, 'good.toml') _bad = os.path.join(W, 'bad.toml') with open(_good, 'w') as fh: fh.write('a = 1\n') with open(_bad, 'w') as fh: fh.write('a = @\n') with open(_good) as fh: emit('load_file', canon(pytoml.load(fh))) try: with open(_bad) as fh: pytoml.load(fh) except pytoml.TomlError as exc: emit('err_load_filename', esc(os.path.basename(exc.filename))) # A file object with no `.name' -- `load' falls back to `repr(fin)', so the # filename in the message is a Python object repr rather than a path. try: pytoml.load(io.StringIO('a = @')) except pytoml.TomlError as exc: emit('err_load_noname_is_repr', 'yes' if exc.filename.startswith('<') else 'no') emit('load_stringio', canon(pytoml.load(io.StringIO('a = 1')))) emit('load_bytesio', canon(pytoml.load(io.BytesIO(b'a = 1')))) # ============================================================================ # THE WRITER, AND THE ROUND TRIP. # ============================================================================ emit('dumps_scalars', D({'i': 1, 'f': 1.5, 'b': True, 's': 'hi'})) emit('dumps_empty_dict', D({})) emit('dumps_nested', D({'a': {'b': 1}, 'c': 2})) emit('dumps_deep', D({'a': {'b': {'c': 1}}})) emit('dumps_empty_table', D({'a': {}})) emit('dumps_aot', D({'a': [{'x': 1}, {'x': 2}]})) emit('dumps_aot_subtable', D({'a': [{'x': 1, 's': {'p': 1}}, {'x': 2, 's': {'p': 2}}]})) emit('dumps_list', D({'a': [1, 2]})) emit('dumps_empty_list', D({'a': []})) emit('dumps_key_escape_dot', D({'a.b': 1})) emit('dumps_key_escape_space', D({'a b': 1})) emit('dumps_key_non_ascii', D({'\u00e9': 1})) emit('dumps_str_escapes', D({'a': 'x\ty\nz\\"q\x01'})) emit('dumps_float', D({'a': 0.1, 'b': 1e300, 'c': 1e-300})) emit('dumps_int_big', D({'a': 2 ** 70})) emit('dumps_sort_keys', D({'z': 1, 'm': 2, 'a': 3}, sort_keys=True)) emit('dumps_no_sort_keys', D({'z': 1, 'm': 2, 'a': 3})) emit('dumps_datetime', D({'a': _dtz})) emit('dumps_datetime_offset', D({'a': _dto})) emit('dumps_to_stream', esc((lambda b: (pytoml.dump({'a': 1}, b), b.getvalue())[1])(io.StringIO()))) emit('rt_scalars', RT({'i': 1, 'f': 1.5, 'b': True, 's': 'hi'})) emit('rt_non_ascii', RT({'a': 'h\u00e9llo \U0001f600'})) emit('rt_control_chars', RT({'a': 'x\ty\nz\\"q\x01'})) emit('rt_nested', RT({'a': {'b': 1}, 'c': 2})) emit('rt_deep', RT({'a': {'b': {'c': 1}}})) emit('rt_empty_table', RT({'a': {}})) emit('rt_aot', RT({'a': [{'x': 1}, {'x': 2}]})) emit('rt_aot_subtable', RT({'a': [{'x': 1, 's': {'p': 1}}, {'x': 2, 's': {'p': 2}}]})) emit('rt_mixed_tables', RT({'t1': {'k': 1}, 't2': {'k': 2}, 'arr': [{'z': 1}, {'z': 2}]})) emit('rt_list', RT({'a': [1, 2]})) emit('rt_empty_list', RT({'a': []})) emit('rt_nested_list', RT({'a': [[1], [2]]})) emit('rt_dict_in_list', RT({'a': [[{'x': 1}]]})) emit('rt_key_dot', RT({'a.b': 1})) emit('rt_key_space', RT({'a b': 1})) emit('rt_key_non_ascii', RT({'\u00e9': 1})) emit('rt_float', RT({'a': 0.1, 'b': 1e300, 'c': 1e-300})) emit('rt_int_big', RT({'a': 2 ** 70})) emit('rt_datetime', RT({'a': _dtz})) emit('rt_datetime_offset', RT({'a': _dto})) emit('rt_datetime_fractional', RT({'a': pytoml.loads('a = 1979-05-27T00:32:00.999999-07:00')['a']})) # THE ROUND TRIP IS NOT TOTAL, and these are the four ways it breaks. Each is # asserted at what actually happens, so a release that repairs one turns the # case naming it red. # # 1. `None' is written as a COMMENT, so the key is silently gone. emit('rt_none', RT({'a': None})) emit('dumps_none', D({'a': None})) # 2. A heterogeneous list is WRITTEN but will not re-parse -- `dumps' has no # type check and `loads' enforces 0.4.0's homogeneous-array rule. emit('rt_mixed_list', RT({'a': [1, 'x']})) emit('dumps_mixed_list', D({'a': [1, 'x']})) emit('rt_int_float_list', RT({'a': [1, 1.0]})) emit('rt_dict_in_mixed_list', RT({'a': [1, {'x': 1}]})) emit('dumps_dict_in_mixed_list', D({'a': [1, {'x': 1}]})) # 3. An EMPTY key is written as a bare `= 1', which is not a document. emit('rt_empty_key', RT({'': 1})) emit('dumps_empty_key', D({'': 1})) # 4. A NAIVE datetime is written with a `Z' suffix, so a value with no # timezone comes back claiming UTC. `format_rfc3339' reads `utcoffset()' # as None and substitutes zero. emit('rt_naive_datetime', RT({'a': datetime.datetime(1979, 5, 27, 7, 32)})) emit('dumps_naive_datetime', D({'a': datetime.datetime(1979, 5, 27, 7, 32)})) # TYPES THE WRITER REFUSES. `inf' and `nan' get a ValueError naming the value, # which is deliberate -- 0.4.0 has no syntax for either. The rest fall off the # end of `_format_value' into a bare RuntimeError carrying only the value. emit('dumps_inf', D({'a': float('inf')})) emit('dumps_nan', D({'a': float('nan')})) emit('dumps_date_object', D({'a': datetime.date(1979, 5, 27)})) emit('dumps_time_object', D({'a': datetime.time(7, 32)})) emit('dumps_tuple', D({'a': (1, 2)})) emit('dumps_set', D({'a': set([1, 2])})) emit('dumps_list_toplevel', D([1, 2])) emit('dumps_none_toplevel', D(None)) # `_format_value' HAS A `bytes' BRANCH THAT CANNOT WORK ON PYTHON 3. It sends # bytes to `_escape_string', which indexes them into ints and tests `c in # '"\\\n\r\t\b\f'' -- a TypeError, not the RuntimeError the else-branch would # have given. The branch is Python 2 residue. emit('dumps_bytes_value', D({'a': b'raw'})) # ============================================================================ # utils, translate_to_test, AND THE TWO HOOKS. # ============================================================================ emit('parse_rfc3339_good', str(parse_rfc3339('1979-05-27T07:32:00Z'))) emit('parse_rfc3339_local', str(parse_rfc3339('1979-05-27T07:32:00'))) emit('parse_rfc3339_junk', str(parse_rfc3339('nope'))) emit('parse_rfc3339_trailing', str(parse_rfc3339('1979-05-27T07:32:00Zx'))) emit('format_rfc3339_naive', format_rfc3339(datetime.datetime(1979, 5, 27, 7, 32))) emit('format_rfc3339_micro', format_rfc3339(datetime.datetime(1979, 5, 27, 7, 32, 0, 5))) emit('format_rfc3339_offset', format_rfc3339(_dto)) emit('format_rfc3339_half_hour', format_rfc3339(_dth)) emit('rfc3339_re_is_compiled', 'yes' if hasattr(rfc3339_re, 'match') else 'no') emit('timezone_class', _TimeZone.__mro__[1].__name__) _tt = pytoml.translate_to_test({'i': 1, 'f': 1.5, 'b': True, 's': 'x', 'a': [1, 2], 'd': _dtz, 't': {'k': 'v'}, 'aot': [{'k': 1}]}) emit('tt_int', esc(repr(sorted(_tt['i'].items())))) emit('tt_float', esc(repr(sorted(_tt['f'].items())))) emit('tt_bool', esc(repr(sorted(_tt['b'].items())))) emit('tt_string', esc(repr(sorted(_tt['s'].items())))) emit('tt_datetime', esc(repr(sorted(_tt['d'].items())))) emit('tt_array_type', _tt['a']['type']) emit('tt_array_len', str(len(_tt['a']['value']))) emit('tt_table_is_bare', 'yes' if 'type' not in _tt['t'] else 'no') emit('tt_aot_is_bare_list', 'yes' if isinstance(_tt['aot'], list) else 'no') try: pytoml.translate_to_test({'x': None}) emit('tt_none', 'no-raise') except Exception as exc: emit('tt_none', type(exc).__name__) # THE `translate' HOOK sees the parser's own (kind, source-text, value) triple, # which is how a caller gets at the ORIGINAL spelling of a number -- the only # route to it, since the parsed value has already lost the underscores. emit('hook_translate', esc(repr(sorted( pytoml.loads('a = 1_0\nb = "x"\nc = 1.5', translate=lambda kind, text, value: (kind, text)).items())))) try: import collections emit('hook_object_pairs', type(pytoml.loads( 'b = 1\na = 2', object_pairs_hook=collections.OrderedDict)).__name__) except Exception as exc: emit('hook_object_pairs', '!%s' % type(exc).__name__) # ------------------------------------------------------------------ tail ------ shutil.rmtree(W, ignore_errors=True) emit('emitted_count', len(emitted)) print('probe_done=yes')