From bdef10a92b6da97644eeef46475e00d34c551291 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 10 Nov 2025 17:15:23 +0000 Subject: [PATCH] tests: Add full feature and coverage tests for PEP 750 template strings. Includes corresponding .exp files because this feature is only available in Python 3.14+. Tests for `!a` conversion specifier and space after `!` are not included because they are not supported by MicroPython. Signed-off-by: Koudai Aono Signed-off-by: Damien George --- pyproject.toml | 14 + tests/basics/string_module_tstring.py | 6 + tests/basics/string_module_tstring.py.exp | 1 + tests/basics/string_tstring_basic.py | 338 +++++++++++++ tests/basics/string_tstring_basic.py.exp | 141 ++++++ tests/basics/string_tstring_basic1.py | 131 +++++ tests/basics/string_tstring_basic1.py.exp | 41 ++ tests/basics/string_tstring_constructor.py | 112 +++++ .../basics/string_tstring_constructor.py.exp | 40 ++ tests/basics/string_tstring_constructor1.py | 24 + .../basics/string_tstring_constructor1.py.exp | 6 + tests/basics/string_tstring_errors1.py | 246 +++++++++ tests/basics/string_tstring_errors1.py.exp | 60 +++ tests/basics/string_tstring_format1.py | 186 +++++++ tests/basics/string_tstring_format1.py.exp | 54 ++ tests/basics/string_tstring_interpolation1.py | 72 +++ .../string_tstring_interpolation1.py.exp | 20 + tests/basics/string_tstring_operations.py | 55 ++ tests/basics/string_tstring_operations.py.exp | 23 + tests/basics/string_tstring_parser1.py | 288 +++++++++++ tests/basics/string_tstring_parser1.py.exp | 60 +++ tests/basics/string_tstring_whitespace.py | 25 + tests/basics/string_tstring_whitespace.py.exp | 7 + tests/feature_check/tstring.py | 8 + tests/feature_check/tstring.py.exp | 1 + tests/micropython/builtin_tstring.py | 24 + tests/micropython/builtin_tstring.py.exp | 9 + tests/micropython/heapalloc_fail_tstring.py | 475 ++++++++++++++++++ .../micropython/heapalloc_fail_tstring.py.exp | 42 ++ tests/run-tests.py | 8 + 30 files changed, 2517 insertions(+) create mode 100644 tests/basics/string_module_tstring.py create mode 100644 tests/basics/string_module_tstring.py.exp create mode 100644 tests/basics/string_tstring_basic.py create mode 100644 tests/basics/string_tstring_basic.py.exp create mode 100644 tests/basics/string_tstring_basic1.py create mode 100644 tests/basics/string_tstring_basic1.py.exp create mode 100644 tests/basics/string_tstring_constructor.py create mode 100644 tests/basics/string_tstring_constructor.py.exp create mode 100644 tests/basics/string_tstring_constructor1.py create mode 100644 tests/basics/string_tstring_constructor1.py.exp create mode 100644 tests/basics/string_tstring_errors1.py create mode 100644 tests/basics/string_tstring_errors1.py.exp create mode 100644 tests/basics/string_tstring_format1.py create mode 100644 tests/basics/string_tstring_format1.py.exp create mode 100644 tests/basics/string_tstring_interpolation1.py create mode 100644 tests/basics/string_tstring_interpolation1.py.exp create mode 100644 tests/basics/string_tstring_operations.py create mode 100644 tests/basics/string_tstring_operations.py.exp create mode 100644 tests/basics/string_tstring_parser1.py create mode 100644 tests/basics/string_tstring_parser1.py.exp create mode 100644 tests/basics/string_tstring_whitespace.py create mode 100644 tests/basics/string_tstring_whitespace.py.exp create mode 100644 tests/feature_check/tstring.py create mode 100644 tests/feature_check/tstring.py.exp create mode 100644 tests/micropython/builtin_tstring.py create mode 100644 tests/micropython/builtin_tstring.py.exp create mode 100644 tests/micropython/heapalloc_fail_tstring.py create mode 100644 tests/micropython/heapalloc_fail_tstring.py.exp diff --git a/pyproject.toml b/pyproject.toml index 2f71058f60..55763552fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,15 @@ target-version = "py38" [tool.ruff.lint] exclude = [ # Ruff finds Python SyntaxError in these files + "tests/basics/string_module_tstring.py", + "tests/basics/string_tstring_basic.py", + "tests/basics/string_tstring_basic1.py", + "tests/basics/string_tstring_constructor.py", + "tests/basics/string_tstring_errors1.py", + "tests/basics/string_tstring_format1.py", + "tests/basics/string_tstring_interpolation1.py", + "tests/basics/string_tstring_operations.py", + "tests/basics/string_tstring_parser1.py", "tests/cmdline/cmd_compile_only_error.py", "tests/cmdline/repl_autocomplete.py", "tests/cmdline/repl_autocomplete_underscore.py", @@ -47,6 +56,8 @@ exclude = [ # Ruff finds Python SyntaxError in these files "tests/cmdline/repl_words_move.py", "tests/feature_check/repl_emacs_check.py", "tests/feature_check/repl_words_move_check.py", + "tests/feature_check/tstring.py", + "tests/micropython/heapalloc_fail_tstring.py", "tests/micropython/viper_args.py", ] extend-select = ["C9", "PLC"] @@ -78,11 +89,14 @@ mccabe.max-complexity = 40 # Exclude third-party code, and exclude the following tests: # basics: needs careful attention before applying automatic formatting # repl_: not real python files +# tstring: ruff does not support template strings # viper_args: uses f(*) exclude = [ "tests/basics/*.py", "tests/*/repl_*.py", "tests/cmdline/cmd_compile_only_error.py", + "tests/feature_check/tstring.py", + "tests/micropython/heapalloc_fail_tstring.py", "tests/micropython/test_normalize_newlines.py", "tests/micropython/viper_args.py", ] diff --git a/tests/basics/string_module_tstring.py b/tests/basics/string_module_tstring.py new file mode 100644 index 0000000000..8380992692 --- /dev/null +++ b/tests/basics/string_module_tstring.py @@ -0,0 +1,6 @@ +# Test basic templatelib functionality. +# This test requires t-strings support. + +from string.templatelib import Template + +print("templatelib", isinstance(t"hi", Template)) diff --git a/tests/basics/string_module_tstring.py.exp b/tests/basics/string_module_tstring.py.exp new file mode 100644 index 0000000000..26dba98f40 --- /dev/null +++ b/tests/basics/string_module_tstring.py.exp @@ -0,0 +1 @@ +templatelib True diff --git a/tests/basics/string_tstring_basic.py b/tests/basics/string_tstring_basic.py new file mode 100644 index 0000000000..e23a3f0659 --- /dev/null +++ b/tests/basics/string_tstring_basic.py @@ -0,0 +1,338 @@ +from string.templatelib import Template, Interpolation + +print("=== Basic functionality ===") +t = t"Hello World" +print(type(t).__name__) + +name = "World" +t2 = t"Hello {name}" +print(f"Strings: {t2.strings}") +print(f"Value: {t2.interpolations[0].value}") +print(f"str(): {str(t2)}") + +t_raw = rt"Path: C:\test\{name}" +print(f"Raw: '{t_raw.strings[0]}'") + +t_tr = tr"Path: C:\test\{name}" +print(f"tr: '{t_tr.strings[0]}'") + +print("\n=== Parser tests ===") +data = {"a": {"b": [1, 2, 3]}} +t_complex = t"{data['a']['b'][0]}" +print(f"Complex: {str(t_complex)}") + +print(f"None: {str(t'{None}')}") +print(f"True: {str(t'{True}')}") +print(f"False: {str(t'{False}')}") +print(f"Ellipsis: {str(t'{...}')}") + +obj = type('Obj', (), { + '__getattr__': lambda s, n: s, + '__getitem__': lambda s, k: s, + '__call__': lambda s, *a: 42, + '__str__': lambda s: "42" +})() +print(f"Deep nest: {str(t'{obj.a.b[0].c()}')}") + +print("\n=== Conversions and formatting ===") +val = {"key": "value"} +print(f"repr: {str(t'{val!r}')}") +print(f"str: {str(t'{val!s}')}") +# print(f"ascii: {str(t'{val!a}')}") + +print(f"Width: '{str(t'{42:10d}')}'") +print(f"Precision: {str(t'{3.14159:.2f}')}") + +x = 42 +t_debug = t"{x=}" +assert t_debug.strings == ("x=", "") +assert t_debug.interpolations[0].expression == "x" +assert t_debug.interpolations[0].conversion == "r" +assert t_debug.interpolations[0].value == 42 +print(f"Debug: {str(t_debug)}") + +y = 10 +t_debug2 = t"{x + y=}" +assert t_debug2.strings == ("x + y=", "") +assert t_debug2.interpolations[0].expression == "x + y" +assert t_debug2.interpolations[0].conversion == "r" +assert t_debug2.interpolations[0].value == 52 + +pi = 3.14159 +t_debug3 = t"{pi=:.2f}" +assert t_debug3.strings == ("pi=", "") +assert t_debug3.interpolations[0].expression == "pi" +assert t_debug3.interpolations[0].conversion is None +assert t_debug3.interpolations[0].format_spec == ".2f" +assert t_debug3.interpolations[0].value == 3.14159 + +print("\n=== Constructor tests ===") +t_empty = Template() +print(f"Empty: {t_empty.strings}") + +t_single = Template("single") +print(f"Single: {t_single.strings}") + +t_multi = Template("a", "b", "c") +print(f"Multi: {t_multi.strings}") + +i1 = Interpolation(1, "x") +i2 = Interpolation(2, "y") +t_mixed = Template("start", i1, "middle", i2, "end") +print(f"Mixed: strings={t_mixed.strings}, values={t_mixed.values}") + +print("\n=== Operations ===") +t1 = t"Hello" +t2 = t" World" +t_concat = t1 + t2 +print(f"Concat: '{str(t_concat)}'") + +items = list(t"a{1}b{2}c") +print(f"Iterator: {[type(x).__name__ for x in items]}") + +t_attr = t"test{42}" +print(f"strings attr: {t_attr.strings}") +print(f"interpolations attr: {len(t_attr.interpolations)}") +print(f"values attr: {t_attr.values}") + +print(f"repr: {repr(t2)[:50]}...") + +print("\n=== Escaped braces evaluation ===") +t_escaped = Template("{", Interpolation(42, "x"), "}", Interpolation(42, "x"), "{{", Interpolation(42, "x"), "}}") +print(f"Escaped eval: '{str(t_escaped)}'") + +t_braces = Template("{{hello}}", Interpolation(1, "a"), " {", Interpolation(2, "b"), "} ", Interpolation(3, "c"), "{{world}}") +print(f"Braces in strings: '{str(t_braces)}'") + +print("\n=== Memory stress test ===") +for n in [10, 20, 30]: + args = [] + for i in range(n): + args.append("s") + args.append(Interpolation(i, f"var{i}")) + args.append("s") + t_mem = Template(*args) + result = str(t_mem) + print(f"Memory test [{n}]: {len(result)} chars") + +large_args = [] +for i in range(20): + large_args.append("") + large_args.append(Interpolation(i, f"v{i}")) +large_args.append("") +t_large = Template(*large_args) +print(f"Large values: {len(t_large.values)} values") + +print("\n=== Nested quotes and braces tests ===") +t_nested1 = t"{"{}"}" +print(f"Nested quotes 1: {str(t_nested1)}") +print(f" Value: {t_nested1.interpolations[0].value}") +print(f" Expression: {t_nested1.interpolations[0].expression}") + +t_nested2 = t"{'hello'}" +print(f"Nested quotes 2: {str(t_nested2)}") +print(f" Value: {t_nested2.interpolations[0].value}") + +t_nested3 = t'{"world"}' +print(f"Nested quotes 3: {str(t_nested3)}") +print(f" Value: {t_nested3.interpolations[0].value}") + +t_nested4 = t"{['a', 'b', 'c'][1]}" +print(f"Nested quotes 4: {str(t_nested4)}") +print(f" Value: {t_nested4.interpolations[0].value}") + +d = {"key": "value", "x": 123} +t_nested5 = t"{d['key']}" +print(f"Nested quotes 5: {str(t_nested5)}") +print(f" Value: {t_nested5.interpolations[0].value}") + +t_escaped1 = t'{{""}}' +print(f"Escaped braces 1: {str(t_escaped1)}") +print(f" Strings: {t_escaped1.strings}") + +t_escaped2 = t"{{}}}}" +print(f"Escaped braces 2: {str(t_escaped2)}") +print(f" Strings: {t_escaped2.strings}") + +x = "test" +t_mixed = t"{{before}} {x} {{after}}" +print(f"Mixed escaped: {str(t_mixed)}") +print(f" Strings: {t_mixed.strings}") +print(f" Value: {t_mixed.interpolations[0].value}") + +t_escape = t"{'\n\t'}" +print(f"Escape sequences: {str(t_escape)}") +print(f" Value repr: {repr(t_escape.interpolations[0].value)}") + +inner = "{}" +t_nested_expr = t"{inner}" +print(f"Nested expr: {str(t_nested_expr)}") +print(f" Value: {t_nested_expr.interpolations[0].value}") + +print("\n=== Interpolation attribute tests ===") +i_basic = Interpolation(42, "x") +print(f"Basic conversion: {i_basic.conversion}") +print(f"Basic format_spec: {i_basic.format_spec}") + +i_with_conv = Interpolation(42, "x", "s") +print(f"With conversion: {i_with_conv.conversion}") + +i_with_fmt = Interpolation(42, "x", None, ":>10") +print(f"With format_spec: {i_with_fmt.format_spec}") + +i_full = Interpolation(42, "x", "r", ":>10") +print(f"Full conversion: {i_full.conversion}") +print(f"Full format_spec: {i_full.format_spec}") + +t_conv = t"{42!s}" +print(f"Template conversion: {t_conv.interpolations[0].conversion}") + +t_fmt = t"{42:>10}" +print(f"Template format_spec: {t_fmt.interpolations[0].format_spec}") + +t_both = t"{42!r:>10}" +print(f"Both conversion: {t_both.interpolations[0].conversion}") +print(f"Both format_spec: {t_both.interpolations[0].format_spec}") + +print("\n=== Escape sequence tests ===") +print(repr(t"Line1\nLine2")) +print(repr(t"Path\\to\\file")) +print(repr(t"It\'s working")) +print(repr(t"She said \"Hello\"")) +print(repr(t"Bell\a")) +print(repr(t"Back\bspace")) +print(repr(t"Tab\there")) +print(repr(t"Vertical\vtab")) +print(repr(t"Form\ffeed")) +print(repr(t"Carriage\rreturn")) + +# Valid hex escapes +print(repr(t"\x41")) +print(repr(t"\x7F")) + +# Valid octal escapes +print(repr(t"\0")) +print(repr(t"\7")) +print(repr(t"\77")) +print(repr(t"\101")) + +print(repr(t"\1a")) +print(repr(t"\123c")) + +# Line continuation +print(repr(t"First \ +second")) +print(repr(t"One\ +Two\ +Three")) + +# Multiple escapes +print(repr(t"\n\t\r")) + +# Raw strings +print(repr(rt"\n\t\x41")) +print(repr(rt"C:\new\test")) + +# Triple quoted strings +print(repr(t"""\n\t\x41""")) +print(repr(rt"""\n\t\x41""")) + +print("\n=== Triple quote edge cases ===") +doc = '''This is a +multi-line +docstring''' +result1 = t"""Documentation: +{doc} +End of doc""" +print(f"Triple in interpolation: {str(result1)}") +print(f" Value: {repr(result1.interpolations[0].value)}") + +data = { + "users": [ + {"name": "Alice", "msg": 'Say "Hello"'}, + {"name": "Bob", "msg": "It's great!"} + ] +} +complex_nested = t"""Users: +{data['users'][0]['name']}: {data['users'][0]['msg']} +{data['users'][1]['name']}: {data['users'][1]['msg']}""" +print(f"Complex nested: {str(complex_nested)}") +print(f" Values: {complex_nested.values}") + +import os +path_sep = os.sep +raw_with_interp = rt"""Path: C:\Users\{path_sep}Documents +Raw newline: \n +Raw tab: \t""" +print(f"Raw with interpolation: {str(raw_with_interp)}") +print(f" String 0 repr: {repr(raw_with_interp.strings[0])}") + +long_line = "x" * 50 +long_triple = t"""Start +{long_line} +End""" +print(f"Long line: strings={long_triple.strings}") +print(f" Value length: {len(long_triple.values[0])}") + +inner = t"inner {42}" +outer = t"""Outer template: +{inner} +End""" +print(f"Nested templates: {str(outer)}") +print(f" Inner value: {outer.values[0]}") + +formatted = t"""Math constants: +Pi: {314:.2f} +E: {271:.1e} +Sqrt(2): {141:.0f}""" +print(f"Formatted values: {repr(formatted)}") +print(f" Interpolation count: {len(formatted.interpolations)}") +print(f" Format specs: {[i.format_spec for i in formatted.interpolations]}") + +x, y, z = 1, 2, 3 +debug_complex = t"""Debug info: +{x + y=} {x * y * z=} {x < y < z=}""" +print(f"Debug complex: {repr(debug_complex)}") +print(f" Expressions: {[i.expression for i in debug_complex.interpolations]}") + +part1 = t"""Part 1 +with newline""" +part2 = t'''Part 2 +also multiline''' +concatenated = part1 + part2 +print(f"Concatenated triple: {repr(concatenated)}") +print(f" Strings: {concatenated.strings}") + +print("\n=== PEP 701 brace compliance ===") +t_brace1 = t"{{" +print(f"Escaped {{{{: {t_brace1.strings}") +assert t_brace1.strings == ('{',) + +t_brace2 = t"}}" +print(f"Escaped }}}}: {t_brace2.strings}") +assert t_brace2.strings == ('}',) + +t_brace3 = t"test{{escape}}here" +print(f"Mixed: {t_brace3.strings}") +assert t_brace3.strings == ('test{escape}here',) + +print("\n=== Format spec scope resolution ===") +def test_format_scope(): + width = 10 + x = 42 + t_scope = t"{x:{width}}" + print(f"Local vars: width={width}, x={x}") + print(f"Format spec: {t_scope.interpolations[0].format_spec}") + return t_scope.interpolations[0].format_spec == '10' + +result = test_format_scope() +print(f"Scope test: {'PASS' if result else 'FAIL'}") +assert result, "Format spec scope resolution failed" + +precision = 2 +value = 3.14159 +t_prec = t"{value:.{precision}f}" +print(f"Precision: format_spec={t_prec.interpolations[0].format_spec}") +assert t_prec.interpolations[0].format_spec == '.2f' + +print("\nBasic tests completed!") diff --git a/tests/basics/string_tstring_basic.py.exp b/tests/basics/string_tstring_basic.py.exp new file mode 100644 index 0000000000..4fcb3a7583 --- /dev/null +++ b/tests/basics/string_tstring_basic.py.exp @@ -0,0 +1,141 @@ +=== Basic functionality === +Template +Strings: ('Hello ', '') +Value: World +str(): Template(strings=('Hello ', ''), interpolations=(Interpolation('World', 'name', None, ''),)) +Raw: 'Path: C:\test\' +tr: 'Path: C:\test\' + +=== Parser tests === +Complex: Template(strings=('', ''), interpolations=(Interpolation(1, "data['a']['b'][0]", None, ''),)) +None: Template(strings=('', ''), interpolations=(Interpolation(None, 'None', None, ''),)) +True: Template(strings=('', ''), interpolations=(Interpolation(True, 'True', None, ''),)) +False: Template(strings=('', ''), interpolations=(Interpolation(False, 'False', None, ''),)) +Ellipsis: Template(strings=('', ''), interpolations=(Interpolation(Ellipsis, '...', None, ''),)) +Deep nest: Template(strings=('', ''), interpolations=(Interpolation(42, 'obj.a.b[0].c()', None, ''),)) + +=== Conversions and formatting === +repr: Template(strings=('', ''), interpolations=(Interpolation({'key': 'value'}, 'val', 'r', ''),)) +str: Template(strings=('', ''), interpolations=(Interpolation({'key': 'value'}, 'val', 's', ''),)) +Width: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, '10d'),))' +Precision: Template(strings=('', ''), interpolations=(Interpolation(3.14159, '3.14159', None, '.2f'),)) +Debug: Template(strings=('x=', ''), interpolations=(Interpolation(42, 'x', 'r', ''),)) + +=== Constructor tests === +Empty: ('',) +Single: ('single',) +Multi: ('abc',) +Mixed: strings=('start', 'middle', 'end'), values=(1, 2) + +=== Operations === +Concat: 'Template(strings=('Hello World',), interpolations=())' +Iterator: ['str', 'Interpolation', 'str', 'Interpolation', 'str'] +strings attr: ('test', '') +interpolations attr: 1 +values attr: (42,) +repr: Template(strings=(' World',), interpolations=())... + +=== Escaped braces evaluation === +Escaped eval: 'Template(strings=('{', '}', '{{', '}}'), interpolations=(Interpolation(42, 'x', None, ''), Interpolation(42, 'x', None, ''), Interpolation(42, 'x', None, '')))' +Braces in strings: 'Template(strings=('{{hello}}', ' {', '} ', '{{world}}'), interpolations=(Interpolation(1, 'a', None, ''), Interpolation(2, 'b', None, ''), Interpolation(3, 'c', None, '')))' + +=== Memory stress test === +Memory test [10]: 450 chars +Memory test [20]: 880 chars +Memory test [30]: 1310 chars +Large values: 20 values + +=== Nested quotes and braces tests === +Nested quotes 1: Template(strings=('', ''), interpolations=(Interpolation('{}', '"{}"', None, ''),)) + Value: {} + Expression: "{}" +Nested quotes 2: Template(strings=('', ''), interpolations=(Interpolation('hello', "'hello'", None, ''),)) + Value: hello +Nested quotes 3: Template(strings=('', ''), interpolations=(Interpolation('world', '"world"', None, ''),)) + Value: world +Nested quotes 4: Template(strings=('', ''), interpolations=(Interpolation('b', "['a', 'b', 'c'][1]", None, ''),)) + Value: b +Nested quotes 5: Template(strings=('', ''), interpolations=(Interpolation('value', "d['key']", None, ''),)) + Value: value +Escaped braces 1: Template(strings=('{""}',), interpolations=()) + Strings: ('{""}',) +Escaped braces 2: Template(strings=('{}}',), interpolations=()) + Strings: ('{}}',) +Mixed escaped: Template(strings=('{before} ', ' {after}'), interpolations=(Interpolation('test', 'x', None, ''),)) + Strings: ('{before} ', ' {after}') + Value: test +Escape sequences: Template(strings=('', ''), interpolations=(Interpolation('\n\t', "'\\n\\t'", None, ''),)) + Value repr: '\n\t' +Nested expr: Template(strings=('', ''), interpolations=(Interpolation('{}', 'inner', None, ''),)) + Value: {} + +=== Interpolation attribute tests === +Basic conversion: None +Basic format_spec: +With conversion: s +With format_spec: :>10 +Full conversion: r +Full format_spec: :>10 +Template conversion: s +Template format_spec: >10 +Both conversion: r +Both format_spec: >10 + +=== Escape sequence tests === +Template(strings=('Line1\nLine2',), interpolations=()) +Template(strings=('Path\\to\\file',), interpolations=()) +Template(strings=("It's working",), interpolations=()) +Template(strings=('She said "Hello"',), interpolations=()) +Template(strings=('Bell\x07',), interpolations=()) +Template(strings=('Back\x08space',), interpolations=()) +Template(strings=('Tab\there',), interpolations=()) +Template(strings=('Vertical\x0btab',), interpolations=()) +Template(strings=('Form\x0cfeed',), interpolations=()) +Template(strings=('Carriage\rreturn',), interpolations=()) +Template(strings=('A',), interpolations=()) +Template(strings=('\x7f',), interpolations=()) +Template(strings=('\x00',), interpolations=()) +Template(strings=('\x07',), interpolations=()) +Template(strings=('?',), interpolations=()) +Template(strings=('A',), interpolations=()) +Template(strings=('\x01a',), interpolations=()) +Template(strings=('Sc',), interpolations=()) +Template(strings=('First second',), interpolations=()) +Template(strings=('OneTwoThree',), interpolations=()) +Template(strings=('\n\t\r',), interpolations=()) +Template(strings=('\\n\\t\\x41',), interpolations=()) +Template(strings=('C:\\new\\test',), interpolations=()) +Template(strings=('\n\tA',), interpolations=()) +Template(strings=('\\n\\t\\x41',), interpolations=()) + +=== Triple quote edge cases === +Triple in interpolation: Template(strings=('Documentation:\n', '\nEnd of doc'), interpolations=(Interpolation('This is a\nmulti-line\ndocstring', 'doc', None, ''),)) + Value: 'This is a\nmulti-line\ndocstring' +Complex nested: Template(strings=('Users:\n', ': ', '\n', ': ', ''), interpolations=(Interpolation('Alice', "data['users'][0]['name']", None, ''), Interpolation('Say "Hello"', "data['users'][0]['msg']", None, ''), Interpolation('Bob', "data['users'][1]['name']", None, ''), Interpolation("It's great!", "data['users'][1]['msg']", None, ''))) + Values: ('Alice', 'Say "Hello"', 'Bob', "It's great!") +Raw with interpolation: Template(strings=('Path: C:\\Users\\', 'Documents\nRaw newline: \\n\nRaw tab: \\t'), interpolations=(Interpolation('/', 'path_sep', None, ''),)) + String 0 repr: 'Path: C:\\Users\\' +Long line: strings=('Start\n', '\nEnd') + Value length: 50 +Nested templates: Template(strings=('Outer template:\n', '\nEnd'), interpolations=(Interpolation(Template(strings=('inner ', ''), interpolations=(Interpolation(42, '42', None, ''),)), 'inner', None, ''),)) + Inner value: Template(strings=('inner ', ''), interpolations=(Interpolation(42, '42', None, ''),)) +Formatted values: Template(strings=('Math constants:\nPi: ', '\nE: ', '\nSqrt(2): ', ''), interpolations=(Interpolation(314, '314', None, '.2f'), Interpolation(271, '271', None, '.1e'), Interpolation(141, '141', None, '.0f'))) + Interpolation count: 3 + Format specs: ['.2f', '.1e', '.0f'] +Debug complex: Template(strings=('Debug info:\nx + y=', ' x * y * z=', ' x < y < z=', ''), interpolations=(Interpolation(3, 'x + y', 'r', ''), Interpolation(6, 'x * y * z', 'r', ''), Interpolation(True, 'x < y < z', 'r', ''))) + Expressions: ['x + y', 'x * y * z', 'x < y < z'] +Concatenated triple: Template(strings=('Part 1\nwith newlinePart 2\nalso multiline',), interpolations=()) + Strings: ('Part 1\nwith newlinePart 2\nalso multiline',) + +=== PEP 701 brace compliance === +Escaped {{: ('{',) +Escaped }}: ('}',) +Mixed: ('test{escape}here',) + +=== Format spec scope resolution === +Local vars: width=10, x=42 +Format spec: 10 +Scope test: PASS +Precision: format_spec=.2f + +Basic tests completed! diff --git a/tests/basics/string_tstring_basic1.py b/tests/basics/string_tstring_basic1.py new file mode 100644 index 0000000000..89ee3e0d2d --- /dev/null +++ b/tests/basics/string_tstring_basic1.py @@ -0,0 +1,131 @@ +print("=== Parser error tests ===") +try: + exec('t_empty = t"{}"') + print(f"Empty expr: {t_empty.interpolations[0].value}") +except SyntaxError as e: + print(f"Empty expr: SyntaxError - {e}") + +# Whitespace in expression (Python semantics are to strip trailing) +t_ws = t"{ 42 }" +print(f"Whitespace: {str(t_ws)}") + +print("\n=== Error cases ===") +try: + exec('t"{@}"') +except SyntaxError: + print("Invalid syntax: SyntaxError") + +try: + long_expr = "x" * 10001 + exec(f't"{{{long_expr}}}"') +except (ValueError, SyntaxError, RuntimeError, NameError): + print("Long expr: Error") + +try: + exec('t"hello" "world"') +except SyntaxError: + print("Mixed concat: SyntaxError") + +try: + exec('bt"test"') +except SyntaxError: + print("bt prefix: SyntaxError") + +print("\n=== Escape sequence coverage tests ===") + +def expect_invalid_escape(label, expr): + try: + eval(expr) + except SyntaxError: + print(f"{label}: SyntaxError") + else: + print(f"{label}: ERROR (expected SyntaxError)") + +# Deprecated escape sequences (CPython warns, MicroPython accepts as literal) +print(repr(t"\8")) +print(repr(t"\9")) + +# Unknown escape chars +print(repr(t"\z")) +print(repr(t"\k")) + +# Invalid escapes +expect_invalid_escape("Invalid \\x escape", 't"\\xGG"') +expect_invalid_escape("Invalid \\u escape", 't"\\uGGGG"') +expect_invalid_escape("Invalid \\U escape", 't"\\UGGGGGGGG"') + +# Unicode escapes (display format differs) +print(repr(t"\x00\x01\xFF")) +print(repr(t"\u0041")) +print(repr(t"\u03B1")) +print(repr(t"\u2764")) +print(repr(t"\U00000041")) +print(repr(t"\U0001F600")) +print(repr(t"\x41\u0042\103")) + +# Unicode in triple-quoted strings +unicode_test = t"""Unicode test: +Emoji: {'\U0001f40d'} +Special: {'\u03b1 \u03b2 \u03b3'}""" +print(f"Unicode: {str(unicode_test)}") + +print("\n=== Trailing whitespace preservation (PEP 750) ===") +x = 42 +tmpl_trail = t"{x }" +expr = tmpl_trail.interpolations[0].expression +print(f"Expression with trailing spaces: |{expr}|") +assert expr == "x", f"Expected 'x' but got '{expr}'" +assert len(expr) == 1, f"Expected length 1 but got {len(expr)}" + +tmpl_both = t"{ x }" +expr2 = tmpl_both.interpolations[0].expression +print(f"Expression with both spaces: |{expr2}|") +assert expr2 == " x", f"Expected ' x' but got '{expr2}'" +assert len(expr2) == 4, f"Expected length 4 but got {len(expr2)}" + +tmpl_lead = t"{ x}" +expr3 = tmpl_lead.interpolations[0].expression +print(f"Expression with leading spaces: |{expr3}|") +assert expr3 == " x", f"Expected ' x' but got '{expr3}'" + +# Debug specifiers: leading space preserved, trailing space stripped from expression +# PEP 750: "Whitespace is preserved in the debug specifier" (in strings part) +z = 99 +t_debug4 = t"{z =}" +assert t_debug4.strings == ("z =", "") +assert t_debug4.interpolations[0].expression == "z" # Trailing space stripped +assert t_debug4.interpolations[0].conversion == "r" + +t_debug5 = t"{ z=}" +assert t_debug5.strings == (" z=", "") +assert t_debug5.interpolations[0].expression == " z" # Leading space preserved +assert t_debug5.interpolations[0].conversion == "r" + +t_debug6 = t"{ z =}" +assert t_debug6.strings == (" z =", "") +assert t_debug6.interpolations[0].expression == " z" # Leading preserved, trailing stripped +assert t_debug6.interpolations[0].conversion == "r" + +print("Trailing whitespace: PASS") + +print("\n=== Error message tests ===") +try: + exec('t"}"') +except SyntaxError: + print("Lone }} rejected (correct)") + +try: + exec('t"{"') +except SyntaxError: + print("Unterminated {{ rejected (correct)") + +print("\n=== Triple quote empty interpolation ===") +try: + exec('''empty_interp = t"""Start +{} +End"""''') + print(f"Empty interpolation: {str(empty_interp)}") +except Exception as e: + print(f"Empty interpolation error: {type(e).__name__}: {e}") + +print("\nIncompatible tests completed!") diff --git a/tests/basics/string_tstring_basic1.py.exp b/tests/basics/string_tstring_basic1.py.exp new file mode 100644 index 0000000000..52fc6f6c94 --- /dev/null +++ b/tests/basics/string_tstring_basic1.py.exp @@ -0,0 +1,41 @@ +=== Parser error tests === +Empty expr: SyntaxError - malformed f-string +Whitespace: Template(strings=('', ''), interpolations=(Interpolation(42, ' 42', None, ''),)) + +=== Error cases === +Invalid syntax: SyntaxError +Long expr: Error +Mixed concat: SyntaxError +bt prefix: SyntaxError + +=== Escape sequence coverage tests === +Template(strings=('\\8',), interpolations=()) +Template(strings=('\\9',), interpolations=()) +Template(strings=('\\z',), interpolations=()) +Template(strings=('\\k',), interpolations=()) +Invalid \x escape: SyntaxError +Invalid \u escape: SyntaxError +Invalid \U escape: SyntaxError +Template(strings=('\x00\x01\xff',), interpolations=()) +Template(strings=('A',), interpolations=()) +Template(strings=('\u03b1',), interpolations=()) +Template(strings=('\u2764',), interpolations=()) +Template(strings=('A',), interpolations=()) +Template(strings=('\U0001f600',), interpolations=()) +Template(strings=('ABC',), interpolations=()) +Unicode: Template(strings=('Unicode test:\nEmoji: ', '\nSpecial: ', ''), interpolations=(Interpolation('\U0001f40d', "'\\U0001f40d'", None, ''), Interpolation('\u03b1 \u03b2 \u03b3', "'\\u03b1 \\u03b2 \\u03b3'", None, ''))) + +=== Trailing whitespace preservation (PEP 750) === +Expression with trailing spaces: |x| +Expression with both spaces: | x| +Expression with leading spaces: | x| +Trailing whitespace: PASS + +=== Error message tests === +Lone }} rejected (correct) +Unterminated {{ rejected (correct) + +=== Triple quote empty interpolation === +Empty interpolation error: SyntaxError: malformed f-string + +Incompatible tests completed! diff --git a/tests/basics/string_tstring_constructor.py b/tests/basics/string_tstring_constructor.py new file mode 100644 index 0000000000..dc4a09a957 --- /dev/null +++ b/tests/basics/string_tstring_constructor.py @@ -0,0 +1,112 @@ +from string.templatelib import Template, Interpolation + +print("=== Constructor basic usage ===") +t = Template("hello ", Interpolation(42, "x"), "world") +print(f"Template repr: {repr(t)}") + +t_varargs = Template("Hello ", Interpolation("World", "name"), "!") +print(f"Varargs constructor: strings={t_varargs.strings}, values={t_varargs.values}") + +t_concat = Template("A", "B", Interpolation(1, "value"), "C", "D") +print(f"Varargs merged strings: {t_concat.strings}") + +t_leading = Template(Interpolation(1, "x"), " tail") +print(f"Leading interpolation strings: {t_leading.strings}") + +t_trailing = Template("head ", Interpolation(2, "y")) +print(f"Trailing interpolation strings: {t_trailing.strings}") + +t_interps_only = Template(Interpolation(1, "x"), Interpolation(2, "y")) +print(f"Interpolation only strings: {t_interps_only.strings}") + +print("\n=== Special cases ===") + +i = Interpolation(42, "x", "s", ":>10") +try: + i.value = 100 +except AttributeError: + print("Interp read-only: AttributeError") + +t_ws_trim = Template("", Interpolation(None, " ", None, ""), "") +print(f"Whitespace trim: '{t_ws_trim}'") + +t_debug = Template("", Interpolation(42, "x=", None, ""), "") +print(f"Debug =: {t_debug}") + + +class Custom: + def __repr__(self): + return "CustomRepr" + + def __str__(self): + return "CustomStr" + + +obj = Custom() +print(f"Custom !r: {t'{obj!r}'}") +print(f"Custom !s: {t'{obj!s}'}") + +t_empty_start = Template("", Interpolation(1, "1"), "text") +print(f"Empty start iter: {[type(x).__name__ for x in t_empty_start]}") + +t_iter_edge = Template("", Interpolation(1, "1"), "", Interpolation(2, "2"), "") +iter_items = [] +for item in t_iter_edge: + iter_items.append(type(item).__name__) +print(f"Iterator edge: {iter_items}") + +print("\n=== Values property ===") +for n in range(7): + args = [] + for i in range(n): + args.append("") + args.append(Interpolation(i, str(i))) + args.append("") + t = Template(*args) + print(f"Values[{n}]: {t.values}") + +print("\n=== Multiple consecutive strings ===") +try: + t = Template("first", "second", "third", Interpolation(42, "x"), "fourth", "fifth") + if t.strings == ("firstsecondthird", "fourthfifth"): + print("Multiple strings concatenated: OK") + else: + print(f"Multiple strings: strings={t.strings}") +except Exception as e: + print(f"Multiple strings error: {e}") + +print("\n=== Template() constructor with many interpolations ===") +try: + exprs = ["a", "b", "c", "d", "e"] + interps = [Interpolation(i, exprs[i % len(exprs)]) for i in range(20)] + strings = [""] * 21 + t = Template(*strings, *interps) + print(f"Template() constructor: OK ({len(t.interpolations)} interpolations)") +except Exception as e: + print(f"Template() constructor: {type(e).__name__}") + +print("\n=== vstr string concatenation ===") +try: + t1 = Template("part1", "part2", "part3", "part4", Interpolation(1, "x"), "end") + result = str(t1) + print(f"vstr concat: '{result}'") +except Exception as e: + print(f"vstr concat error: {e}") + +print("\n=== High byte handling ===") +try: + result = t"\x7f\x80\x81\xfe\xff" + first_str = result.strings[0] + print( + f"High bytes: len={len(first_str)}, first=0x{ord(first_str[0]):02x}, last=0x{ord(first_str[-1]):02x}" + ) +except Exception as e: + print(f"High bytes error: {e}") + +try: + result = t"\200\201\377" + print(f"Octal high bytes: OK, len={len(result.strings[0])}") +except Exception as e: + print(f"Octal high bytes error: {e}") + +print("\nConstructor tests completed!") diff --git a/tests/basics/string_tstring_constructor.py.exp b/tests/basics/string_tstring_constructor.py.exp new file mode 100644 index 0000000000..e7d6ea90a9 --- /dev/null +++ b/tests/basics/string_tstring_constructor.py.exp @@ -0,0 +1,40 @@ +=== Constructor basic usage === +Template repr: Template(strings=('hello ', 'world'), interpolations=(Interpolation(42, 'x', None, ''),)) +Varargs constructor: strings=('Hello ', '!'), values=('World',) +Varargs merged strings: ('AB', 'CD') +Leading interpolation strings: ('', ' tail') +Trailing interpolation strings: ('head ', '') +Interpolation only strings: ('', '', '') + +=== Special cases === +Interp read-only: AttributeError +Whitespace trim: 'Template(strings=('', ''), interpolations=(Interpolation(None, ' ', None, ''),))' +Debug =: Template(strings=('', ''), interpolations=(Interpolation(42, 'x=', None, ''),)) +Custom !r: Template(strings=('', ''), interpolations=(Interpolation(CustomRepr, 'obj', 'r', ''),)) +Custom !s: Template(strings=('', ''), interpolations=(Interpolation(CustomRepr, 'obj', 's', ''),)) +Empty start iter: ['Interpolation', 'str'] +Iterator edge: ['Interpolation', 'Interpolation'] + +=== Values property === +Values[0]: () +Values[1]: (0,) +Values[2]: (0, 1) +Values[3]: (0, 1, 2) +Values[4]: (0, 1, 2, 3) +Values[5]: (0, 1, 2, 3, 4) +Values[6]: (0, 1, 2, 3, 4, 5) + +=== Multiple consecutive strings === +Multiple strings concatenated: OK + +=== Template() constructor with many interpolations === +Template() constructor: OK (20 interpolations) + +=== vstr string concatenation === +vstr concat: 'Template(strings=('part1part2part3part4', 'end'), interpolations=(Interpolation(1, 'x', None, ''),))' + +=== High byte handling === +High bytes: len=5, first=0x7f, last=0xff +Octal high bytes: OK, len=3 + +Constructor tests completed! diff --git a/tests/basics/string_tstring_constructor1.py b/tests/basics/string_tstring_constructor1.py new file mode 100644 index 0000000000..1593efa9d7 --- /dev/null +++ b/tests/basics/string_tstring_constructor1.py @@ -0,0 +1,24 @@ +# NOTE: Error messages are shortened in MicroPython to avoid stack overflow +# during ROM compression on constrained platforms (Windows x86, ASan). +# CPython 3.14 message: "Template.__new__ *args need to be of type 'str' or 'Interpolation', got int" +# MicroPython message: "Template.__new__ args must be str or Interpolation, got 'int'" + +from string.templatelib import Template + +print("=== Constructor error messages ===") +try: + Template(strings=("test",)) +except TypeError as e: + print(f"Keyword args: {e}") + +try: + Template("hello", 42, "world") +except TypeError as e: + print(f"Invalid type: {e}") + +try: + Template("a", 42, "b") +except TypeError as e: + print(f"Invalid type in varargs: {e}") + +print("\nConstructor error tests completed!") diff --git a/tests/basics/string_tstring_constructor1.py.exp b/tests/basics/string_tstring_constructor1.py.exp new file mode 100644 index 0000000000..932529c97b --- /dev/null +++ b/tests/basics/string_tstring_constructor1.py.exp @@ -0,0 +1,6 @@ +=== Constructor error messages === +Keyword args: function doesn't take keyword arguments +Invalid type: expected str or Interpolation +Invalid type in varargs: expected str or Interpolation + +Constructor error tests completed! diff --git a/tests/basics/string_tstring_errors1.py b/tests/basics/string_tstring_errors1.py new file mode 100644 index 0000000000..7a0c737ef6 --- /dev/null +++ b/tests/basics/string_tstring_errors1.py @@ -0,0 +1,246 @@ +from string.templatelib import Template, Interpolation + +print("\n=== Edge cases ===") +t_empty = Template() +print(f"Empty template: '{t_empty}'") + +t_empty_strs = Template("", Interpolation(1, "a"), "", Interpolation(2, "b"), "") +print(f"Empty strings: {list(t_empty_strs)}") + +t_adj = t"{1}{2}{3}" +print(f"Adjacent: '{t_adj}'") + +t_single = Template("only") +print(f"Single iter: {list(t_single)}") + +t_self = t"test" +print(f"Self+self: '{(t_self + t_self)}'") + +print("\n=== Additional coverage tests ===") + +t_str_literal = t"{'hello'}" +print(f"String literal: {t_str_literal}") + +path = "/usr/local/bin" +count = 42 +raw_path = rt"Path: {path}\n" +print(f"Raw path strings: {raw_path.strings}, value={raw_path.interpolations[0].value}") + +raw_regex = rt"Regex: \\d+{count}" +print(f"Raw regex strings: {raw_regex.strings}, value={raw_regex.interpolations[0].value}") + +try: + t_str_expr = t'{"test"}' + print(f"String expr: '{t_str_expr}'") +except Exception as e: + print(f"String expr error: {e}") + +def raise_error(): + raise ValueError("Special error") + +try: + t_exc = t"{raise_error()}" + print(t_exc) +except ValueError as e: + print(f"Re-raised exception: {e}") + +try: + large_str = "x" * 100000 + exec(f'very_long_name_{large_str} = t"test"') +except (ValueError, MemoryError, SyntaxError, RuntimeError) as e: + print("Large template: SyntaxError") + +try: + exec('''t_triple = t"""Triple "quoted" string"""''') + print(f"Triple quoted: '{t_triple}'") +except Exception as e: + print(f"Triple quoted error: {e}") + +try: + exec(r'''t_raw_triple = rt"""Raw triple\n{42}"""''') + print(f"Raw triple: '{t_raw_triple}'") +except Exception as e: + print(f"Raw triple error: {e}") + +t_concat1 = t"a{1}b" +t_concat2 = t"c{2}d{3}e" +t_concat3 = t_concat1 + t_concat2 +print(f"Complex concat: strings={t_concat3.strings}, values={t_concat3.values}") + +t_empty = Template() +t_nonempty = t"test{42}" +t_concat_empty = t_empty + t_nonempty +print(f"Empty concat: '{t_concat_empty}'") + +t_self_interp = t"x{1}y" +t_self_concat = t_self_interp + t_self_interp +print(f"Self interp concat: values={t_self_concat.values}") + +try: + exec('t"unterminated') +except SyntaxError as e: + print(f"Lonely string: {type(e).__name__}") + +try: + t_edge = t"" + print(f"Empty t-string: '{t_edge}'") +except Exception as e: + print(f"Empty t-string error: {e}") + + +print("\n=== High byte handling ===") +try: + result = t'\x7F\x80\x81\xFE\xFF' + first_str = result.strings[0] + print(f"High bytes: len={len(first_str)}, first=0x{ord(first_str[0]):02x}, last=0x{ord(first_str[-1]):02x}") +except Exception as e: + print(f"High bytes error: {e}") + +try: + result = t'\200\201\377' + print(f"Octal high bytes: OK, len={len(result.strings[0])}") +except Exception as e: + print(f"Octal high bytes error: {e}") + +print("\n=== Deep nesting test ===") +try: + nested = "1" + " + 1" * 150 + code = f't"{{{nested}}}"' + exec(code) + print("Deep nesting: OK") +except Exception as e: + print(f"Deep nesting error: {type(e).__name__}") + +print("\n=== Trailing whitespace in expression ===") +try: + x = 42 + code = 't"{x }"' + result = eval(code) + print(f"Trailing whitespace: OK") +except Exception as e: + print(f"Trailing whitespace error: {e}") + +print("\n=== Single closing brace ===") +try: + exec('t"test }"') + print("ERROR: Single } should have raised SyntaxError") +except SyntaxError as e: + print(f"Single }}: SyntaxError - {e}") + +print("\n=== Escape in string within interpolation ===") + +try: + compile('x = 1; t"{x=:"', '', 'exec') +except SyntaxError: + print('Debug unclosed colon: SyntaxError') + +try: + compile('x = 1; t"{x=!"', '', 'exec') +except SyntaxError: + print('Debug unclosed exclaim: SyntaxError') + +try: + compile('t"prefix{incomplete"', '', 'exec') +except SyntaxError as e: + print('Unclosed brace literal: SyntaxError') + +try: + import sys + if hasattr(sys.implementation, '_mpy'): + compile('f"{x!invalid}"', '', 'exec') +except SyntaxError: + print('Malformed f-string: SyntaxError') + +# NOTE: Error messages are shortened in MicroPython to avoid stack overflow +# during ROM compression on constrained platforms (Windows x86, ASan). +# CPython 3.14 message: "Template.__new__ *args need to be of type 'str' or 'Interpolation', got int" +# MicroPython message: "Template.__new__ args must be str or Interpolation, got 'int'" + +try: + t = Template("string", 123) +except TypeError as e: + print('Type error int:', e) + +try: + t = Template("a", "b", 3.14) +except TypeError: + print('Type error float: TypeError') + +try: + compile('t"text { more text"', '', 'exec') +except SyntaxError as e: + print('Unmatched brace:', e) + +try: + compile('x=1; t"{x= {nested}}"', '', 'exec') +except SyntaxError: + print('Nested debug braces: SyntaxError') + +try: + compile('t"{x!}"', '', 'exec') +except SyntaxError: + print('Missing conversion: SyntaxError') + +try: + compile('t"{x!z}"', '', 'exec') +except SyntaxError as e: + print('Invalid conversion:', e) + +try: + compile('t"{x!r' + chr(0x0b) + ':10}"', '', 'exec') +except SyntaxError as e: + print('Vertical tab:', e) + +try: + compile('t"{x!r@:10}"', '', 'exec') +except SyntaxError as e: + print('Invalid char after conversion:', e) + +# MicroPython doesn't allow space after conversion. +# try: +# x = 'test' +# exec('result = t"{x!r :10}"') +# print('Space after conversion: OK') +# except Exception as e: +# print(f'Unexpected error: {e}') + +try: + x = 'test' + exec('result = t"{x!s\\t:10}"') + print('Tab after conversion: OK') +except Exception as e: + print(f'Unexpected error: {e}') + +try: + x = 'test' + exec('result = t"{x!r\\n:10}"') + print('Newline after conversion: OK') +except Exception as e: + print(f'Unexpected error: {e}') + +try: + x = 'test' + exec('result = t"{x!r\\r:10}"') + print('CR after conversion: OK') +except Exception as e: + print(f'Unexpected error: {e}') + +try: + x = 'test' + exec('result = t"{x!s\\f:10}"') + print('Form-feed after conversion: OK') +except Exception as e: + print(f'Unexpected error: {e}') + +print("\n=== Mixed prefixes ===") +for src in ('ft"{x}"', 'tf"{x}"', 'frt"{x}"', 'rtf"{x}"', 'trf"{x}"'): + try: + exec(src) + print(src.split('"')[0] + ": OK (BUG!)") + except SyntaxError as e: + prefix = src.split('"')[0] + # Check if we get the specific error message or generic one + if "'f' and 't' prefixes are incompatible" in str(e): + print(f"{prefix}: incompatible prefixes") + else: + print(f"{prefix}: invalid syntax") diff --git a/tests/basics/string_tstring_errors1.py.exp b/tests/basics/string_tstring_errors1.py.exp new file mode 100644 index 0000000000..f8d993770d --- /dev/null +++ b/tests/basics/string_tstring_errors1.py.exp @@ -0,0 +1,60 @@ + +=== Edge cases === +Empty template: 'Template(strings=('',), interpolations=())' +Empty strings: [Interpolation(1, 'a', None, ''), Interpolation(2, 'b', None, '')] +Adjacent: 'Template(strings=('', '', '', ''), interpolations=(Interpolation(1, '1', None, ''), Interpolation(2, '2', None, ''), Interpolation(3, '3', None, '')))' +Single iter: ['only'] +Self+self: 'Template(strings=('testtest',), interpolations=())' + +=== Additional coverage tests === +String literal: Template(strings=('', ''), interpolations=(Interpolation('hello', "'hello'", None, ''),)) +Raw path strings: ('Path: ', '\\n'), value=/usr/local/bin +Raw regex strings: ('Regex: \\\\d+', ''), value=42 +String expr: 'Template(strings=('', ''), interpolations=(Interpolation('test', '"test"', None, ''),))' +Re-raised exception: Special error +Large template: SyntaxError +Triple quoted: 'Template(strings=('Triple "quoted" string',), interpolations=())' +Raw triple: 'Template(strings=('Raw triple\\n', ''), interpolations=(Interpolation(42, '42', None, ''),))' +Complex concat: strings=('a', 'bc', 'd', 'e'), values=(1, 2, 3) +Empty concat: 'Template(strings=('test', ''), interpolations=(Interpolation(42, '42', None, ''),))' +Self interp concat: values=(1, 1) +Lonely string: SyntaxError +Empty t-string: 'Template(strings=('',), interpolations=())' + +=== High byte handling === +High bytes: len=5, first=0x7f, last=0xff +Octal high bytes: OK, len=3 + +=== Deep nesting test === +Deep nesting: OK + +=== Trailing whitespace in expression === +Trailing whitespace: OK + +=== Single closing brace === +Single }: SyntaxError - malformed f-string + +=== Escape in string within interpolation === +Debug unclosed colon: SyntaxError +Debug unclosed exclaim: SyntaxError +Unclosed brace literal: SyntaxError +Malformed f-string: SyntaxError +Type error int: expected str or Interpolation +Type error float: TypeError +Unmatched brace: malformed f-string +Nested debug braces: SyntaxError +Missing conversion: SyntaxError +Invalid conversion: invalid syntax +Vertical tab: invalid syntax +Invalid char after conversion: invalid syntax +Unexpected error: invalid syntax +Unexpected error: invalid syntax +Unexpected error: invalid syntax +Unexpected error: invalid syntax + +=== Mixed prefixes === +ft: invalid syntax +tf: invalid syntax +frt: invalid syntax +rtf: invalid syntax +trf: invalid syntax diff --git a/tests/basics/string_tstring_format1.py b/tests/basics/string_tstring_format1.py new file mode 100644 index 0000000000..54fb0f1e76 --- /dev/null +++ b/tests/basics/string_tstring_format1.py @@ -0,0 +1,186 @@ +print("\n=== Format spec edge cases ===") +print(f"Empty fmt: {t'{42:}'}") +print(f"Width: '{t'{42:10}'}'") +print(f"Conv+fmt: '{t'{42!r:>10}'}'") + +width = 10 +print(f"Interp fmt: '{t'{3.14:{width}.2f}'}'") + +try: + t_escaped = t"Hello {{name}} and {{{{value}}}}" + print(f"Escaped braces: '{t_escaped}'") +except Exception as e: + print(f"Escaped braces error: {e}") + +try: + t_conv_fmt = t"{42!r:}" + print(f"Conv empty fmt: '{t_conv_fmt}'") +except Exception as e: + print(f"Conv fmt error: {e}") + +print("\n=== Format spec edge cases ===") +try: + x = 42 + result = t'{x!r:0>+#10.5}' + print(f"Full format: '{result}'") +except Exception as e: + print(f"Full format error: {e}") + +print("\n=== Format spec with special characters ===") +try: + x = 42 + result = t"{x:!r}" + print(f"Format spec '!r': {result.interpolations[0].format_spec == '!r'}") + print(f"Conversion is None: {result.interpolations[0].conversion is None}") +except Exception as e: + print(f"Format spec error: {type(e).__name__}") + +print("\nCoverage tests completed!") +print("\n=== Debug format edge cases ===") +try: + x = 42 + result = t'{x=!r}' + print(f"Debug with conv: '{result}'") +except SyntaxError as e: + print(f"Debug conv: SyntaxError - {e}") + + +print("\n=== Debug specifier tests ===") +try: + value = 42 + t1 = t'{value=}' + print(f"Debug basic: strings={t1.strings}, conv={t1.interpolations[0].conversion}") + + t2 = t'{value=!s}' + print(f"Debug !s: strings={t2.strings}, conv={t2.interpolations[0].conversion}") + + t3 = t'{value=:>10}' + print(f"Debug fmt: strings={t3.strings}, fmt_spec='{t3.interpolations[0].format_spec}'") +except Exception as e: + print(f"Debug format error: {e}") + + +print("\n=== Large number of interpolations ===") +for N in (62, 63, 64, 65): + x = 1 + code = 'result = str(t"' + '{x}' * N + '")' + exec(code) + expected = "Template(strings=" + repr(tuple("" for _ in range(N + 1))) + ", interpolations=(" + ", ".join("Interpolation(1, 'x', None, '')" for _ in range(N)) + "))" + print(N, result == expected) + +print("\n=== Malformed format specs (valid in CPython) ===") +x = 42 +value = 42 +result = t'{x:10!r}' +print(f"Malformed 1: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}") + +result = t'{value:^10!s}' +print(f"Malformed 2: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}") + +result = t'{x:>!10}' +print(f"Malformed 3: format_spec: '{result.interpolations[0].format_spec}', conversion: {result.interpolations[0].conversion}") + +try: + exec(r"t'\U00110000'") + print("ERROR: Should have failed") +except SyntaxError as e: + print(f"Invalid unicode: SyntaxError - {e}") + +class CustomException(Exception): + pass + +class BadClass: + def __getattr__(self, name): + raise CustomException("Test exception") + +try: + bad_obj = BadClass() + result = t"{bad_obj.attr}" + print("ERROR: Should have raised exception") +except CustomException as e: + print(f"Custom exception re-raised: {e}") +except Exception as e: + print(f"Other exception: {type(e).__name__} - {e}") + +print("\nNULL lexer: Tested in heapalloc_fail_tstring.py") + +try: + exec(r"t'\N{LATIN SMALL LETTER A}'") + print("ERROR: \\N{{}} should not be supported") +except (SyntaxError, NotImplementedError) as e: + print(f"\\N{{}} escape: SyntaxError") + +try: + result = rt'\u0041\U00000042' + print(f"Raw Unicode escapes: '{result.strings[0]}'") +except Exception as e: + print(f"Raw Unicode error: {e}") + +try: + code_with_crlf = 't"line1\\r\\nline2"' + exec(code_with_crlf) + print("CR LF handling: OK") +except Exception as e: + print(f"CR LF error: {e}") + +try: + code = "" + for i in range(20): + code += " " * i + "if True:\n" + code += " " * 20 + "x = t'deep indent'" + exec(code) + print("Deep indent: OK") +except Exception as e: + print(f"Deep indent error: {e}") + +try: + exec("t'test\\") + print("ERROR: Trailing backslash should fail") +except SyntaxError as e: + print(f"Trailing backslash: SyntaxError") + +print("\n=== Empty format spec node ===") +try: + x = 42 + result = t'{x:}' + print(f"Empty format after colon: OK") +except Exception as e: + print(f"Empty format spec error: {e}") + + +print("\n=== Format spec with special characters (coverage) ===") +try: + align = "<" + width = 5 + value = "test" + result = t'{value:{"^"*width}}' + print(f"Double quotes expr: format_spec={repr(result.interpolations[0].format_spec)}") +except Exception as e: + print(f"Double quotes expr error: {type(e).__name__}: {e}") + +try: + align = "<" + width = 5 + value = "test" + result = rt"{value:{align}{width}\\}" + print(f"Backslash raw: format_spec={repr(result.interpolations[0].format_spec)}") +except Exception as e: + print(f"Backslash raw error: {type(e).__name__}: {e}") + +try: + align = "<" + width = 5 + value = "test" + result = t"{value:{align}{width}\\}" + print(f"Backslash non-raw: format_spec={repr(result.interpolations[0].format_spec)}") +except Exception as e: + print(f"Backslash non-raw error: {type(e).__name__}: {e}") + +try: + align = "<" + width = 5 + value = "test" + result = t"{value:{align}{width}\n\t\r}" + print(f"Multiple escapes: format_spec={repr(result.interpolations[0].format_spec)}") +except Exception as e: + print(f"Multiple escapes error: {type(e).__name__}: {e}") diff --git a/tests/basics/string_tstring_format1.py.exp b/tests/basics/string_tstring_format1.py.exp new file mode 100644 index 0000000000..601087ccfd --- /dev/null +++ b/tests/basics/string_tstring_format1.py.exp @@ -0,0 +1,54 @@ + +=== Format spec edge cases === +Empty fmt: Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, ''),)) +Width: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', None, '10'),))' +Conv+fmt: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', 'r', '>10'),))' +Interp fmt: 'Template(strings=('', ''), interpolations=(Interpolation(3.14, '3.14', None, '10.2f'),))' +Escaped braces: 'Template(strings=('Hello {name} and {{value}}',), interpolations=())' +Conv empty fmt: 'Template(strings=('', ''), interpolations=(Interpolation(42, '42', 'r', ''),))' + +=== Format spec edge cases === +Full format: 'Template(strings=('', ''), interpolations=(Interpolation(42, 'x', 'r', '0>+#10.5'),))' + +=== Format spec with special characters === +Format spec '!r': True +Conversion is None: True + +Coverage tests completed! + +=== Debug format edge cases === +Debug with conv: 'Template(strings=('x=', ''), interpolations=(Interpolation(42, 'x', 'r', ''),))' + +=== Debug specifier tests === +Debug basic: strings=('value=', ''), conv=r +Debug !s: strings=('value=', ''), conv=s +Debug fmt: strings=('value=', ''), fmt_spec='>10' + +=== Large number of interpolations === +62 True +63 True +64 True +65 True + +=== Malformed format specs (valid in CPython) === +Malformed 1: format_spec: '10!r', conversion: None +Malformed 2: format_spec: '^10!s', conversion: None +Malformed 3: format_spec: '>!10', conversion: None +Invalid unicode: SyntaxError - invalid syntax +Custom exception re-raised: Test exception + +NULL lexer: Tested in heapalloc_fail_tstring.py +\N{} escape: SyntaxError +Raw Unicode escapes: '\u0041\U00000042' +CR LF handling: OK +Deep indent: OK +Trailing backslash: SyntaxError + +=== Empty format spec node === +Empty format after colon: OK + +=== Format spec with special characters (coverage) === +Double quotes expr: format_spec='^^^^^' +Backslash raw: format_spec='<5\\\\' +Backslash non-raw: format_spec='<5\\' +Multiple escapes: format_spec='<5\n\t\r' diff --git a/tests/basics/string_tstring_interpolation1.py b/tests/basics/string_tstring_interpolation1.py new file mode 100644 index 0000000000..da9c469d2b --- /dev/null +++ b/tests/basics/string_tstring_interpolation1.py @@ -0,0 +1,72 @@ +print("\n=== Bracket/paren depth tracking ===") +d = {'a': 1, 'b:c': 2} +items = [10, 20, 30] +colon_key = t"{d['b:c']}" +print(f"Colon in key: expr={colon_key.interpolations[0].expression}, value={colon_key.interpolations[0].value}") + +slice_expr = t"{items[1:3]}" +print(f"Slice colon: expr={slice_expr.interpolations[0].expression}, value={slice_expr.interpolations[0].value}") + +def pair(x, y=':'): + return (x, y) + +func_expr = t"{pair(1, 2)}" +print(f"Function call: expr={func_expr.interpolations[0].expression}, value={func_expr.interpolations[0].value}") + +default_expr = t"{pair(3)}" +print(f"Default arg colon: expr={default_expr.interpolations[0].expression}, value={default_expr.interpolations[0].value}") + +matrix = [[1, 2], [3, 4]] +nested_expr = t"{matrix[0][1]}" +print(f"Nested brackets: expr={nested_expr.interpolations[0].expression}, value={nested_expr.interpolations[0].value}") + + +print("\n=== Inline template literal tests ===") + +try: + result = t"outer { t'' }" + print(f"Empty nested: {type(result.values[0]).__name__}") +except Exception as e: + print(f"Empty nested: FAIL - {type(e).__name__}: {e}") + +try: + result = t"outer { t'inner' }" + print(f"With content: {type(result.values[0]).__name__}") +except Exception as e: + print(f"With content: FAIL - {type(e).__name__}: {e}") + +try: + result = t"result: { t'a' + t'b' }" + print(f"Concatenation: {type(result.values[0]).__name__}") +except Exception as e: + print(f"Concatenation: FAIL - {type(e).__name__}: {e}") + +try: + result = t'Single { t"double" }' + print(f"Mixed quotes: {type(result.values[0]).__name__}") +except Exception as e: + print(f"Mixed quotes: FAIL - {type(e).__name__}: {e}") + +try: + x = 42 + result = t"outer { t'inner {x}' }" + print(f"Nested interp: {type(result.values[0]).__name__}") +except Exception as e: + print(f"Nested interp: FAIL - {type(e).__name__}: {e}") + +print("\n=== Backslashes not allowed in expressions (PEP 498/750) ===") +try: + code = r"result = t'{\"test\"value\"}'" + exec(code) + print(f"ERROR: Backslash in expression should raise SyntaxError") +except SyntaxError as e: + print(f"Backslash in expression: SyntaxError") + +try: + code = r"result = t'{\"\\n\"}'" + exec(code) + print(f"ERROR: Backslash in expression should raise SyntaxError") +except SyntaxError as e: + print(f"Escaped newline: SyntaxError") + +print("\n=== Format spec with special characters ===") diff --git a/tests/basics/string_tstring_interpolation1.py.exp b/tests/basics/string_tstring_interpolation1.py.exp new file mode 100644 index 0000000000..8a62f33182 --- /dev/null +++ b/tests/basics/string_tstring_interpolation1.py.exp @@ -0,0 +1,20 @@ + +=== Bracket/paren depth tracking === +Colon in key: expr=d['b:c'], value=2 +Slice colon: expr=items[1:3], value=[20, 30] +Function call: expr=pair(1, 2), value=(1, 2) +Default arg colon: expr=pair(3), value=(3, ':') +Nested brackets: expr=matrix[0][1], value=2 + +=== Inline template literal tests === +Empty nested: Template +With content: Template +Concatenation: Template +Mixed quotes: Template +Nested interp: Template + +=== Backslashes not allowed in expressions (PEP 498/750) === +Backslash in expression: SyntaxError +Escaped newline: SyntaxError + +=== Format spec with special characters === diff --git a/tests/basics/string_tstring_operations.py b/tests/basics/string_tstring_operations.py new file mode 100644 index 0000000000..3c3222025f --- /dev/null +++ b/tests/basics/string_tstring_operations.py @@ -0,0 +1,55 @@ +from string.templatelib import Template, Interpolation + +print("\n=== Binary operations ===") +t1 = t"template" + +try: + t1 + "string" +except TypeError as e: + print(f"Template+str: TypeError") + +try: + "string" + t1 +except TypeError as e: + print(f"str+Template: TypeError") + +try: + 42 + t1 +except TypeError: + print("int+Template: TypeError") + +for op in ["-", "*", "/", "%", "**", "&", "|", "^", "<<", ">>"]: + try: + eval(f"t1 {op} t1") + except TypeError: + print(f"{op}: unsupported") + +print("\n=== Template.__add__ with multiple interpolations ===") +try: + exprs = ["a", "b", "c", "d", "e"] + interps1 = [Interpolation(i, exprs[i % len(exprs)]) for i in range(20)] + strings1 = [""] * 21 + t1 = Template(*strings1, *interps1) + + interps2 = [Interpolation(i + 20, exprs[i % len(exprs)]) for i in range(20)] + strings2 = [""] * 21 + t2 = Template(*strings2, *interps2) + + result = t1 + t2 + print(f"Template.__add__: OK ({len(result.interpolations)} interpolations)") +except Exception as e: + print(f"Template.__add__: {type(e).__name__}") + +print("\n=== Template + non-string object ===") +try: + + class CustomObj: + pass + + t1 = t"hello" + result = t1 + CustomObj() + print("ERROR: Should have raised TypeError") +except TypeError as e: + print(f"Template + custom object: TypeError (correct)") + +print("\n=== 2-tuple constructor overflow ===") diff --git a/tests/basics/string_tstring_operations.py.exp b/tests/basics/string_tstring_operations.py.exp new file mode 100644 index 0000000000..a565db8fec --- /dev/null +++ b/tests/basics/string_tstring_operations.py.exp @@ -0,0 +1,23 @@ + +=== Binary operations === +Template+str: TypeError +str+Template: TypeError +int+Template: TypeError +-: unsupported +*: unsupported +/: unsupported +%: unsupported +**: unsupported +&: unsupported +|: unsupported +^: unsupported +<<: unsupported +>>: unsupported + +=== Template.__add__ with multiple interpolations === +Template.__add__: OK (40 interpolations) + +=== Template + non-string object === +Template + custom object: TypeError (correct) + +=== 2-tuple constructor overflow === diff --git a/tests/basics/string_tstring_parser1.py b/tests/basics/string_tstring_parser1.py new file mode 100644 index 0000000000..b8853ccd1e --- /dev/null +++ b/tests/basics/string_tstring_parser1.py @@ -0,0 +1,288 @@ +from string.templatelib import Template + +print("\n=== Empty expression tests ===") +try: + exec("t'{}'") + print("ERROR: Empty expression should have raised SyntaxError") +except SyntaxError as e: + print("Empty expr SyntaxError:", e) + +try: + exec("t'{ }'") + print("ERROR: Whitespace-only expression should have raised SyntaxError") +except SyntaxError as e: + print("Whitespace expr SyntaxError:", e) + +try: + exec("t'{\\n\\t \\n}'") + print("ERROR: Whitespace-only expr should fail") +except SyntaxError as e: + print(f"Whitespace expr: SyntaxError") + +try: + exec("t'{value'") + print("ERROR: Unterminated expr should fail") +except SyntaxError as e: + print(f"Unterminated expr: {type(e).__name__}") + +try: + exec("t'{ !r}'") + print("ERROR: Whitespace-conversion expr should fail") +except SyntaxError as e: + print(f"Whitespace + conversion: {type(e).__name__}") + +try: + long_expr = 'x' * 104 + code = f"t'{{{long_expr}}}'" + exec(f"{long_expr} = 'test'; result = {code}") + print("Long expression: OK") +except Exception as e: + print(f"Long expr error: {type(e).__name__}: {e}") + +try: + large_str = 'x' * 1099 + tmpl = Template(large_str) + result = str(tmpl) + print("Template.__str__ with large string: OK") +except Exception as e: + print(f"Template.__str__ error: {type(e).__name__}: {e}") + +try: + x = 'a' + result = t'{x}' + print(f"Basic t-string: OK, result={result}") +except Exception as e: + print(f"Basic t-string error: {type(e).__name__}: {e}") + +try: + code = 't"' + 'text{nested{x}}more' * 50 + '"' + x = 'test' + exec(f"x = 'test'; result = {code}") + print("Nested interpolations: OK") +except Exception as e: + print(f"Nested interpolations error: {type(e).__name__}: {e}") + +width = 5 +x = 42 +try: + tmpl = t'{x:{width}{{literal}}}' + result = str(tmpl) + print(f'Escaped braces result: {result}') +except ValueError as e: + print(f'Escaped braces ValueError: {e}') + +print("\n=== Expression parser tests ===") +try: + exec("t'{[{(x,y):[1,2,3]} for x,y in [(1,2),(3,4)]]}'") + print("Complex expr: OK") +except Exception as e: + print(f"Complex expr error: {type(e).__name__}") + +print("\n=== Lexer edge cases ===") +print("Lexer NULL case: Tested via heapalloc_fail_tstring.py") + +print("\n=== Parser allocation edge cases ===") +try: + deep_expr = "(" * 50 + "x" + ")" * 50 + exec(f"x = 1; result = t'{{{deep_expr}}}'") + print("Deep nesting: OK") +except Exception as e: + print(f"Deep nesting error: {type(e).__name__} - {e}") + +try: + long_expr = "(" * 100 + "x" + ")" * 100 + exec(f"x = 1; result = t'{{{long_expr}}}'") + print("Very long expression: OK") +except Exception as e: + print(f"Long expression error: {e}") + + +print("\n=== Additional parser regression tests ===") + +try: + fmt = '"QUOTED"' + value = 'raw' + tmpl = rt"{value:{fmt}}" + print(f"Raw nested spec: {tmpl.interpolations[0].format_spec}") +except Exception as e: + print(f"Raw nested spec error: {type(e).__name__}: {e}") + +try: + value = 'fmt' + tmpl = t'{value:"QUOTED"}' + print(f"Escaped quote spec: {tmpl.interpolations[0].format_spec}") +except Exception as e: + print(f"Escaped quote spec error: {type(e).__name__}: {e}") + +try: + value = 'fmt' + tmpl = t"{value:\\n}" + print(f"Escaped backslash spec: {tmpl.interpolations[0].format_spec}") +except Exception as e: + print(f"Escaped backslash spec error: {type(e).__name__}: {e}") + +try: + ns = {} + exec("value = 'fmt'\nresult = t\"{value:{'\\\\'}}\"", globals(), ns) + tmpl = ns['result'] + print(f"Nested quoted spec: {tmpl.interpolations[0].format_spec}") +except Exception as e: + print(f"Nested quoted spec error: {type(e).__name__}: {e}") + +try: + value = 'brace' + exec('tmpl = t"{value}}}}tail"') + print(f"Literal brace strings: {tmpl.strings}") +except Exception as e: + print(f"Literal brace error: {type(e).__name__}: {e}") + +try: + exec('t"{value"') + print("ERROR: Unterminated field should have raised") +except SyntaxError as e: + print(f"Unterminated field: {e}") + + +print("\n=== Complex expression stress test ===") +try: + vars_dict = {f"x{i}": i for i in range(60)} + exec_globals = globals().copy() + exec_globals.update(vars_dict) + expr_parts = [f"x{i}" for i in range(60)] + expr_str = " + ".join(expr_parts) + code = f't"{{{expr_str}}}"' + result = eval(code, exec_globals) + expected = sum(range(60)) + actual = result.interpolations[0].value + if actual == expected: + print(f"60 terms: PASS") + else: + print(f"60 terms: FAIL (expected={expected}, got={actual})") +except Exception as e: + print(f"60 terms: ERROR - {type(e).__name__}: {e}") + +try: + vars_dict = {f"x{i}": i for i in range(100)} + exec_globals = globals().copy() + exec_globals.update(vars_dict) + expr_parts = [f"x{i}" for i in range(100)] + expr_str = " + ".join(expr_parts) + code = f't"{{{expr_str}}}"' + result = eval(code, exec_globals) + expected = sum(range(100)) + actual = result.interpolations[0].value + if actual == expected: + print(f"100 terms: PASS") + else: + print(f"100 terms: FAIL (expected={expected}, got={actual})") +except Exception as e: + print(f"100 terms: ERROR - {type(e).__name__}: {e}") + +print("\n=== Stack expansion test (copy_parse_node) ===") +try: + depth = 20 + nested_expr = "[" * depth + "1" + "]" * depth + code = f't"{{{nested_expr}}}"' + result = eval(code) + if len(str(result.interpolations[0].value)) > 30: + print("Stack expansion: OK") + else: + print(f"Stack expansion: FAIL") +except Exception as e: + print(f"Stack expansion error: {type(e).__name__}") + +print("\n=== Format spec with triple-quoted strings ===") +try: + x = 42 + code = '''t'{x:{"""test"""}}' ''' + result = eval(code) + print(f"Triple-quote in format spec: OK") +except Exception as e: + print(f"Triple-quote error: {type(e).__name__}: {e}") + +try: + x = 42 + code = """t"{x:{'''test'''}}" """ + result = eval(code) + print(f"Triple single-quote in format spec: OK") +except Exception as e: + print(f"Triple single-quote error: {type(e).__name__}: {e}") + +print("\n=== Format spec parse error (exception handler) ===") +try: + x = 42 + code = "t\"{x:{1 + (}}\" " # Unclosed parenthesis + result = eval(code) + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print(f"Parse error handler: OK") +except Exception as e: + print(f"Unexpected error: {type(e).__name__}: {e}") + +try: + x = 42 + code = "t\"{x:{1 2 3}}\" " # Invalid syntax + result = eval(code) + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print(f"Parse error handler 2: OK") +except Exception as e: + print(f"Unexpected error 2: {type(e).__name__}: {e}") + +print("\n=== Large integer in t-string ===") +try: + # Use eval() to avoid compile-time overflow in longlong variant + large_int = eval("10**30") + tmpl = t"{large_int}" + if tmpl.interpolations[0].value == large_int: + print("Large integer: OK") + else: + print(f"Large integer: FAIL") +except OverflowError: + # longlong variant doesn't support arbitrary precision integers + print("Large integer: OK") +except Exception as e: + print(f"Large integer error: {type(e).__name__}: {e}") + +print("\n=== Bytes in expression ===") +try: + data = b"test" + tmpl = t"{data}" + if tmpl.interpolations[0].value == b"test": + print("Bytes in expression: OK") + else: + print(f"Bytes: FAIL") +except Exception as e: + print(f"Bytes error: {type(e).__name__}: {e}") + +print("\n=== Boolean constants ===") +try: + tmpl = t"{True} {False}" + if tmpl.values == (True, False): + print("Boolean constants: OK") + else: + print(f"Booleans: FAIL") +except Exception as e: + print(f"Boolean error: {type(e).__name__}: {e}") + +print("\n=== None constant ===") +try: + tmpl = t"{None}" + if tmpl.values[0] is None: + print("None constant: OK") + else: + print(f"None: FAIL") +except Exception as e: + print(f"None error: {type(e).__name__}: {e}") + +print("\n=== Deep nesting for rule stack expansion ===") +try: + depth = 80 + code = '(' * depth + '1' + ')' * depth + result = eval(code) + if result == 1: + print(f"Deep nesting (depth={depth}): OK") + else: + print(f"Deep nesting: FAIL (result={result})") +except Exception as e: + print(f"Deep nesting error: {type(e).__name__}: {e}") diff --git a/tests/basics/string_tstring_parser1.py.exp b/tests/basics/string_tstring_parser1.py.exp new file mode 100644 index 0000000000..cd55b25854 --- /dev/null +++ b/tests/basics/string_tstring_parser1.py.exp @@ -0,0 +1,60 @@ + +=== Empty expression tests === +Empty expr SyntaxError: malformed f-string +Whitespace expr SyntaxError: malformed f-string +Whitespace expr: SyntaxError +Unterminated expr: SyntaxError +Whitespace + conversion: SyntaxError +Long expression: OK +Template.__str__ with large string: OK +Basic t-string: OK, result=Template(strings=('', ''), interpolations=(Interpolation('a', 'x', None, ''),)) +Nested interpolations error: SyntaxError: invalid syntax +Escaped braces result: Template(strings=('', ''), interpolations=(Interpolation(42, 'x', None, '5{literal}'),)) + +=== Expression parser tests === +Complex expr: OK + +=== Lexer edge cases === +Lexer NULL case: Tested via heapalloc_fail_tstring.py + +=== Parser allocation edge cases === +Deep nesting: OK +Very long expression: OK + +=== Additional parser regression tests === +Raw nested spec: "QUOTED" +Escaped quote spec: "QUOTED" +Escaped backslash spec: \n +Nested quoted spec: \ +Literal brace error: SyntaxError: malformed f-string +Unterminated field: malformed f-string + +=== Complex expression stress test === +60 terms: PASS +100 terms: PASS + +=== Stack expansion test (copy_parse_node) === +Stack expansion: OK + +=== Format spec with triple-quoted strings === +Triple-quote in format spec: OK +Triple single-quote in format spec: OK + +=== Format spec parse error (exception handler) === +Parse error handler: OK +Parse error handler 2: OK + +=== Large integer in t-string === +Large integer: OK + +=== Bytes in expression === +Bytes in expression: OK + +=== Boolean constants === +Boolean constants: OK + +=== None constant === +None constant: OK + +=== Deep nesting for rule stack expansion === +Deep nesting (depth=80): OK diff --git a/tests/basics/string_tstring_whitespace.py b/tests/basics/string_tstring_whitespace.py new file mode 100644 index 0000000000..08dd7933c3 --- /dev/null +++ b/tests/basics/string_tstring_whitespace.py @@ -0,0 +1,25 @@ +print("# Empty expression (whitespace only)") +try: + exec('t"{ }"') + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print("Empty expr (space): SyntaxError (correct)") + +try: + exec('t"{\t}"') + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print("Empty expr (tab): SyntaxError (correct)") + +try: + exec('t"{\n}"') + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print("Empty expr (newline): SyntaxError (correct)") + +print("\n# Lexer escape sequence: invalid then valid") +try: + exec("t'\\U00200000\\x41'") + print("ERROR: Should have raised SyntaxError") +except SyntaxError as e: + print("Invalid escape sequence: SyntaxError (correct)") diff --git a/tests/basics/string_tstring_whitespace.py.exp b/tests/basics/string_tstring_whitespace.py.exp new file mode 100644 index 0000000000..f5868faf68 --- /dev/null +++ b/tests/basics/string_tstring_whitespace.py.exp @@ -0,0 +1,7 @@ +# Empty expression (whitespace only) +Empty expr (space): SyntaxError (correct) +Empty expr (tab): SyntaxError (correct) +Empty expr (newline): SyntaxError (correct) + +# Lexer escape sequence: invalid then valid +Invalid escape sequence: SyntaxError (correct) diff --git a/tests/feature_check/tstring.py b/tests/feature_check/tstring.py new file mode 100644 index 0000000000..e1d428b4e7 --- /dev/null +++ b/tests/feature_check/tstring.py @@ -0,0 +1,8 @@ +# check whether t-strings (PEP-750) are supported + +# TODO remove this check when micropython-lib's string extends ustring +from string.templatelib import Template, Interpolation + +a = 1 +t = t"a={a}" +print("tstring") diff --git a/tests/feature_check/tstring.py.exp b/tests/feature_check/tstring.py.exp new file mode 100644 index 0000000000..ba42b0ec66 --- /dev/null +++ b/tests/feature_check/tstring.py.exp @@ -0,0 +1 @@ +tstring diff --git a/tests/micropython/builtin_tstring.py b/tests/micropython/builtin_tstring.py new file mode 100644 index 0000000000..8de0acfd98 --- /dev/null +++ b/tests/micropython/builtin_tstring.py @@ -0,0 +1,24 @@ +# This tests the built-in __template__ (which is MicroPython specific). + +# Test a varying number of arguments. +print(__template__(())) +print(__template__((), ())) +print(__template__((), None, None)) +print(__template__((), None, None, None)) +print(__template__((), None, None, None, None)) +print(__template__((), None, None, None, None, None)) + +# Test two strings and one interpolation. +print(__template__(("Hello ", "!"), (42, "x", None, ""))) + +# Test not enough arguments. +try: + print(__template__()) +except TypeError as er: + print(repr(er)) + +# Test two arguments with second not being a tuple/list. +try: + print(__template__((), None)) +except TypeError as er: + print(repr(er)) diff --git a/tests/micropython/builtin_tstring.py.exp b/tests/micropython/builtin_tstring.py.exp new file mode 100644 index 0000000000..b6b4730612 --- /dev/null +++ b/tests/micropython/builtin_tstring.py.exp @@ -0,0 +1,9 @@ +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=(Interpolation(None, None, None, None),)) +Template(strings=(), interpolations=(Interpolation(None, None, None, None),)) +Template(strings=('Hello ', '!'), interpolations=(Interpolation(42, 'x', None, ''),)) +TypeError('function missing 1 required positional arguments',) +TypeError("object 'NoneType' isn't a tuple or list",) diff --git a/tests/micropython/heapalloc_fail_tstring.py b/tests/micropython/heapalloc_fail_tstring.py new file mode 100644 index 0000000000..94687d57bf --- /dev/null +++ b/tests/micropython/heapalloc_fail_tstring.py @@ -0,0 +1,475 @@ +# Test template string (t-string) operations with heap allocation failure + +import micropython +from string.templatelib import Template, Interpolation + +i1 = Interpolation(1, "1") +i2 = Interpolation(2, "2") + +micropython.heap_lock() +try: + args = [] + for i in range(9): + args.append("x" * 100) + args.append(Interpolation(i, "x" + str(i))) + args.append("x" * 100) + Template(*args) + print("FAIL: Template creation") +except MemoryError: + print("OK: Template creation") +micropython.heap_unlock() + +# Multiple string concatenation +micropython.heap_lock() +try: + Template("first", "second", "third", "fourth") + print("FAIL: Multi string concat") +except MemoryError: + print("OK: Multi string concat") +micropython.heap_unlock() + +# Mixed constructor +micropython.heap_lock() +try: + Template("a", i1, "b", i2, "c", "d", "e") + print("FAIL: Mixed constructor") +except MemoryError: + print("OK: Mixed constructor") +micropython.heap_unlock() + +# Template.__str__() +t = t"Hello {42} world {99}" +micropython.heap_lock() +try: + str(t) + print("FAIL: Template.__str__()") +except MemoryError: + print("OK: Template.__str__()") +micropython.heap_unlock() + +# Template.values property +vals = list(range(10)) +t_many = t"{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}" +micropython.heap_lock() +try: + t_many.values + print("FAIL: Template.values") +except MemoryError: + print("OK: Template.values") +micropython.heap_unlock() + +n = 20 +args_large = [] +for i in range(n): + args_large.append("") + args_large.append(Interpolation(i, "x" + str(i))) +args_large.append("") +t_large = Template(*args_large) +micropython.heap_lock() +try: + t_large.values + print("FAIL: Large values array") +except MemoryError: + print("OK: Large values array") +micropython.heap_unlock() + +# Template concatenation +t1 = t"Hello" +t2 = t"World" +micropython.heap_lock() +try: + t1 + t2 + print("FAIL: Template concatenation") +except MemoryError: + print("OK: Template concatenation") +micropython.heap_unlock() + +# Template iterator +t_iter = t"a{1}b{2}c" +micropython.heap_lock() +try: + iter(t_iter) + print("FAIL: Template iterator") +except MemoryError: + print("OK: Template iterator") +micropython.heap_unlock() + +# Iterator next +t_iter2 = t"a{1}b{2}c{3}d" +it = iter(t_iter2) +micropython.heap_lock() +try: + list(it) + print("FAIL: Iterator next") +except MemoryError: + print("OK: Iterator next") +micropython.heap_unlock() + +# __template__ builtin +strings2 = ("test",) * 5 +interps2 = ((42, "x", None, ""),) * 4 +micropython.heap_lock() +try: + __template__(strings2, interps2) + print("FAIL: __template__ builtin") +except MemoryError: + print("OK: __template__ builtin") +micropython.heap_unlock() + +# Format spec interpolation +width = 10 +t_fmt = t"{42:{width}d}" +micropython.heap_lock() +try: + str(t_fmt) + print("FAIL: Format spec interpolation") +except MemoryError: + print("OK: Format spec interpolation") +micropython.heap_unlock() + +# Debug format +x = 42 +t_debug = t"{x=}" +micropython.heap_lock() +try: + str(t_debug) + print("FAIL: Debug format") +except MemoryError: + print("OK: Debug format") +micropython.heap_unlock() + +# Conversion with format +obj = "test" +t_conv = t"{obj!r:>10}" +micropython.heap_lock() +try: + str(t_conv) + print("FAIL: Conversion + format") +except MemoryError: + print("OK: Conversion + format") +micropython.heap_unlock() + +# String conversion (s) +t_s = t"{'test'!s}" +micropython.heap_lock() +try: + str(t_s) + print("FAIL: s conversion") +except MemoryError: + print("OK: s conversion") +micropython.heap_unlock() + +# ASCII conversion (a) +# t_a = t"{'test'!a}" +# micropython.heap_lock() +# try: +# str(t_a) +# print("FAIL: a conversion") +# except MemoryError: +# print("OK: a conversion") +# micropython.heap_unlock() + +# Complex format spec +fill = "*" +align = ">" +width = 10 +precision = 2 +t_complex = t"{3.14159:{fill}{align}{width}.{precision}f}" +micropython.heap_lock() +try: + str(t_complex) + print("FAIL: Complex format spec") +except MemoryError: + print("OK: Complex format spec") +micropython.heap_unlock() + +# Simple expression +x = 42 +t_simple = t"{x}" +micropython.heap_lock() +try: + str(t_simple) + print("FAIL: Simple expression") +except MemoryError: + print("OK: Simple expression") +micropython.heap_unlock() + +# Interpolation creation +micropython.heap_lock() +try: + Interpolation("value", "expr", "r", ".2f") + print("FAIL: Interpolation creation") +except MemoryError: + print("OK: Interpolation creation") +micropython.heap_unlock() + +# Template repr +t = t"test" +micropython.heap_lock() +try: + repr(t) + print("FAIL: Template repr") +except MemoryError: + print("OK: Template repr") +micropython.heap_unlock() + +# Interpolation repr +i = Interpolation(42, "x") +micropython.heap_lock() +try: + repr(i) + print("FAIL: Interpolation repr") +except MemoryError: + print("OK: Interpolation repr") +micropython.heap_unlock() + +def exhaust_heap(limit): + allocations = [] + try: + for _ in range(limit): + allocations.append("x" * 1024) + except MemoryError: + pass + return allocations + +def test_many_interpolations_heap(): + # Pre-create variables before exhausting heap + x1, x2, x3, x4, x5, x6, x7, x8, x9 = 1, 2, 3, 4, 5, 6, 7, 8, 9 + micropython.heap_lock() + try: + t = t"{x1}{x2}{x3}{x4}{x5}{x6}{x7}{x8}{x9}" + print("FAIL: Many interpolations heap test") + except MemoryError: + print("OK: Many interpolations heap test") + micropython.heap_unlock() + +def test_template_str_heap(): + t = t"x{1}x{2}x{3}x{4}x" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Template str heap test") + except MemoryError: + print("OK: Template str heap test") + micropython.heap_unlock() + +def test_template_iter_heap(): + t = t"a{1}b{2}c" + micropython.heap_lock() + try: + parts = list(iter(t)) + print("FAIL: Template iter heap test") + except MemoryError: + print("OK: Template iter heap test") + micropython.heap_unlock() + +def test_template_concat_heap(): + t1 = t"first" + t2 = t"second" + micropython.heap_lock() + try: + t3 = t1 + t2 + print("FAIL: Template concat heap test") + except MemoryError: + print("OK: Template concat heap test") + micropython.heap_unlock() + +def test_format_spec_heap(): + width = 10 + t = t"{42:{width}d}" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Format spec heap test") + except MemoryError: + print("OK: Format spec heap test") + micropython.heap_unlock() + +def test_debug_format_heap(): + value = 42 + t = t"{value}" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Debug format heap test") + except MemoryError: + print("OK: Debug format heap test") + micropython.heap_unlock() + +print("\n=== Heap allocation failure tests ===") +test_many_interpolations_heap() +test_template_str_heap() +test_template_iter_heap() +test_template_concat_heap() +test_format_spec_heap() +test_debug_format_heap() + +# Additional tests from coverage.c +print("\n=== Coverage.c heap tests ===") + +# Test creating interpolation with heap locked (from coverage.c) +micropython.heap_lock() +try: + i = Interpolation(42, "x", None, None) + print("FAIL: Interpolation with heap locked") +except MemoryError: + print("OK: Interpolation creation with heap locked") +micropython.heap_unlock() + +# Test parsing expression with heap locked (from coverage.c) +micropython.heap_lock() +try: + t = eval('t"{x + 1}"') + print("FAIL: Parse with heap locked") +except Exception as e: + if type(e).__name__ == "MemoryError": + print("OK: Parse with heap locked") + else: + print(f"Parse with heap locked: {type(e).__name__}") +micropython.heap_unlock() + +# Test lexer allocation failure for t-string expression parsing +print("\n=== Lexer/Parser allocation tests ===") + +# Wrap all tests in a try-catch to handle any unexpected errors +try: + # Test 1: Empty expression (tests tstring_expr_parser.c empty check) + try: + micropython.heap_lock() + try: + # This tests the empty expression path + exec("t'{}'") + print("FAIL: Empty expression with heap locked") + except Exception as e: + print(f"OK: Empty expression - {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 2: Whitespace-only expression + try: + micropython.heap_lock() + try: + exec("t'{ }'") + print("FAIL: Whitespace expression with heap locked") + except Exception as e: + print(f"OK: Whitespace expression - {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 3: Very complex expression to stress parser allocation + # Pre-create variables + x, y, z = 1, 2, 3 + try: + micropython.heap_lock() + try: + # Complex nested expression that requires many parse nodes + result = t'{[x*y for x in range(10) for y in range(10) if x+y > 5]}' + print("FAIL: Complex expression with heap locked") + except MemoryError: + print("OK: Complex expression parser allocation") + except Exception as e: + print(f"Complex expression: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 4: Expression parser lexer failure (simulates NULL lexer) + # This happens when memory is exhausted during lexer creation + try: + micropython.heap_lock() + try: + # Try to create t-string with expression during low memory + long_expr = "x" * 100 # Use a moderately long expression + exec(f"{long_expr} = 42; result = t'{{{long_expr}}}'") + print("FAIL: Lexer creation with heap locked") + except MemoryError: + print("OK: Lexer creation failure") + except Exception as e: + print(f"Lexer creation: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 5: Format spec and conversion parsing under memory pressure + val = 42 + try: + micropython.heap_lock() + try: + # This tests format spec node creation + result = t'{val!r:>10.2f}' + print("FAIL: Format spec with heap locked") + except MemoryError: + print("OK: Format spec allocation") + except Exception as e: + print(f"Format spec: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 6: Debug format with memory pressure + x = 100 + try: + micropython.heap_lock() + try: + result = t'{x=:.2f}' + print("FAIL: Debug format with heap locked") + except MemoryError: + print("OK: Debug format allocation") + except Exception as e: + print(f"Debug format: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 7: Multiple interpolations causing allocation failure + vals = list(range(20)) + try: + micropython.heap_lock() + try: + # Many interpolations to stress allocation + result = t'{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}{vals[5]}{vals[6]}{vals[7]}{vals[8]}{vals[9]}' + print("FAIL: Many interpolations with heap locked") + except MemoryError: + print("OK: Many interpolations allocation") + except Exception as e: + print(f"Many interpolations: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 8: Template string size limit (tests overflow check) + # Note: We can't directly test integer overflow, but we can test large templates + print("Template size limit: Tested via exec in coverage tests") + + # Test 9: Compile-time parsing under allocation failure + try: + micropython.heap_lock() + try: + compile("t'{x}'", "", "exec") + print("FAIL: Compile-time parsing under heap lock") + except MemoryError: + print("OK: Compile-time parsing under heap lock") + finally: + micropython.heap_unlock() + except: + pass + +except Exception as e: + # Catch any unexpected errors from the tests + print(f"\nTest error: {type(e).__name__}: {e}") + # Ensure heap is unlocked + try: + micropython.heap_unlock() + except: + pass + +print("\n=== Tests completed ===") diff --git a/tests/micropython/heapalloc_fail_tstring.py.exp b/tests/micropython/heapalloc_fail_tstring.py.exp new file mode 100644 index 0000000000..3bc90ef418 --- /dev/null +++ b/tests/micropython/heapalloc_fail_tstring.py.exp @@ -0,0 +1,42 @@ +OK: Template creation +OK: Multi string concat +OK: Mixed constructor +OK: Template.__str__() +OK: Template.values +OK: Large values array +OK: Template concatenation +OK: Template iterator +OK: Iterator next +OK: __template__ builtin +OK: Format spec interpolation +OK: Debug format +OK: Conversion + format +OK: s conversion +OK: Complex format spec +OK: Simple expression +OK: Interpolation creation +OK: Template repr +OK: Interpolation repr + +=== Heap allocation failure tests === +OK: Many interpolations heap test +OK: Template str heap test +OK: Template iter heap test +OK: Template concat heap test +OK: Format spec heap test +OK: Debug format heap test + +=== Coverage.c heap tests === +OK: Interpolation creation with heap locked +OK: Parse with heap locked + +=== Lexer/Parser allocation tests === +OK: Complex expression parser allocation +OK: Lexer creation failure +OK: Format spec allocation +OK: Debug format allocation +OK: Many interpolations allocation +Template size limit: Tested via exec in coverage tests +OK: Compile-time parsing under heap lock + +=== Tests completed === diff --git a/tests/run-tests.py b/tests/run-tests.py index 3f876e3ff4..2add13df21 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -650,6 +650,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_const = False skip_revops = False skip_fstring = False + skip_tstring = False skip_endian = False skip_inlineasm = False has_complex = True @@ -710,6 +711,11 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): if output != b"a=1\n": skip_fstring = True + # Check if tstring feature is enabled, and skip such tests if it doesn't + output = run_feature_check(pyb, args, "tstring.py") + if output != b"tstring\n": + skip_tstring = True + if args.inlineasm_arch == "thumb": # Check if @micropython.asm_thumb supports Thumb2 instructions, and skip such tests if it doesn't output = run_feature_check(pyb, args, "inlineasm_thumb2.py") @@ -881,6 +887,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): is_async = test_name.startswith(("async_", "asyncio_")) or test_name.endswith("_async") is_const = test_name.startswith("const") is_fstring = test_name.startswith("string_fstring") + is_tstring = test_name.startswith("string_tstring") or test_name.endswith("_tstring") is_inlineasm = test_name.startswith("asm") skip_it = os.path.realpath(test_file) in skip_tests @@ -895,6 +902,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_it |= skip_const and is_const skip_it |= skip_revops and "reverse_op" in test_name skip_it |= skip_fstring and is_fstring + skip_it |= skip_tstring and is_tstring skip_it |= skip_inlineasm and is_inlineasm if skip_it: