diff --git a/test/bytecode_3.8_run/02_fstring_debug.pyc b/test/bytecode_3.8_run/02_fstring_debug.pyc new file mode 100644 index 00000000..a62b99f3 Binary files /dev/null and b/test/bytecode_3.8_run/02_fstring_debug.pyc differ diff --git a/test/simple_source/bug38/02_fstring_debug.py b/test/simple_source/bug38/02_fstring_debug.py new file mode 100644 index 00000000..e0bd5ced --- /dev/null +++ b/test/simple_source/bug38/02_fstring_debug.py @@ -0,0 +1,32 @@ +# Tests new "debug" format new in 3.8. +# Much of this is adapted from 3.8 test/test_fstring.py +# RUNNABLE! + +"""This program is self-checking!""" + +# fmt: off +# We want to use "=" and ":=" *without* the surrounding space to test format spec and "=" detection +f'{f"{3.1415=:.1f}":*^20}' == '*****3.1415=3.1*****' + +y = 2 +def f(x, width): + return f'x={x*y:{width}}' + +assert f('foo', 10) == 'x=foofoo ' + +x = 'bar' +assert f(10, 10), 'x= 20' + +x = 'A string' +f"x={x!r}" == 'x=' + repr(x) + +pi = 'π' +assert f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega" + +x = 20 +# This isn't an assignment expression, it's 'x', with a format +# spec of '=10'. +assert f'{x:=10}' == ' 20' + +assert f'{(x:=10)}' == '10' +assert x == 10 diff --git a/uncompyle6/parsers/parse38.py b/uncompyle6/parsers/parse38.py index 3eb1b70a..3c7789d7 100644 --- a/uncompyle6/parsers/parse38.py +++ b/uncompyle6/parsers/parse38.py @@ -20,6 +20,7 @@ from __future__ import print_function from uncompyle6.parser import PythonParserSingle, nop_func from spark_parser import DEFAULT_DEBUG as PARSER_DEFAULT_DEBUG from uncompyle6.parsers.parse37 import Python37Parser +from uncompyle6.parsers.reducecheck import joined_str_check class Python38Parser(Python37Parser): def p_38_stmt(self, args): @@ -521,6 +522,29 @@ class Python38Parser(Python37Parser): self.add_unique_rules(["expr ::= %s" % collection, rule], customize) continue continue + + elif opname == "BUILD_STRING_2": + self.addRule( + """ + expr ::= formatted_value_debug + formatted_value_debug ::= LOAD_STR formatted_value2 BUILD_STRING_2 + formatted_value_debug ::= LOAD_STR formatted_value1 BUILD_STRING_2 + """, + nop_func, + ) + custom_ops_processed.add(opname) + + elif opname == "BUILD_STRING_3": + self.addRule( + """ + expr ::= formatted_value_debug + formatted_value_debug ::= LOAD_STR formatted_value2 LOAD_STR BUILD_STRING_3 + formatted_value_debug ::= LOAD_STR formatted_value1 LOAD_STR BUILD_STRING_3 + """, + nop_func, + ) + custom_ops_processed.add(opname) + elif opname == "LOAD_CLOSURE": self.addRule("""load_closure ::= LOAD_CLOSURE+""", nop_func) diff --git a/uncompyle6/parsers/reducecheck/iflaststmt.py b/uncompyle6/parsers/reducecheck/iflaststmt.py index 93495d24..3fc778d3 100644 --- a/uncompyle6/parsers/reducecheck/iflaststmt.py +++ b/uncompyle6/parsers/reducecheck/iflaststmt.py @@ -1,8 +1,22 @@ -# Copyright (c) 2020 Rocky Bernstein +# Copyright (c) 2020, 2022 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . -def iflaststmt(self, lhs, n, rule, ast, tokens, first, last): - testexpr = ast[0] +def iflaststmt( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + testexpr = tree[0] if testexpr[0] in ("testtrue", "testfalse"): diff --git a/uncompyle6/parsers/reducecheck/joined_str_check.py b/uncompyle6/parsers/reducecheck/joined_str_check.py new file mode 100644 index 00000000..96ddc7b9 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/joined_str_check.py @@ -0,0 +1,47 @@ +# Copyright (c) 2022 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def joined_str_ok( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + # In Python 3.8, there is a new "=" specifier. + # See https://docs.python.org/3/whatsnew/3.8.html#f-strings-support-for-self-documenting-expressions-and-debugging + # We detect this here inside joined_str by looking for an + # expr->LOAD_STR which has an "=" added at the end + # and is equal without the "=" to expr->formated_value2->LOAD_CONST + # converted to a string. + expr1 = tree[0] + if expr1 != "expr": + return False + load_str = expr1[0] + if load_str != "LOAD_STR": + return False + format_value_equal = load_str.attr + if format_value_equal[-1] != "=": + return False + expr2 = tree[1] + if expr2 != "expr": + return False + formatted_value = expr2[0] + if not formatted_value.kind.startswith("formatted_value"): + return False + expr2a = formatted_value[0] + if expr2a != "expr": + return False + load_const = expr2a[0] + if load_const == "LOAD_CONST": + format_value2 = load_const.attr + return str(format_value2) == format_value_equal[:-1] + return True diff --git a/uncompyle6/parsers/reducecheck/whilestmt.py b/uncompyle6/parsers/reducecheck/whilestmt.py new file mode 100644 index 00000000..6d8ade00 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/whilestmt.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def whilestmt( + self, lhs: str, n: int, rule, tree, tokens: list, first: int, last: int +) -> bool: + # When we are missing a COME_FROM_LOOP, the + # "while" statement is nested inside an if/else + # so after the POP_BLOCK we have a JUMP_FORWARD which forms the "else" portion of the "if" + # Check this. + # print("XXX", first, last, rule) + # for t in range(first, last): print(tokens[t]) + # print("="*40) + + return tokens[last - 1] == "POP_BLOCK" and tokens[last] not in ( + "JUMP_FORWARD", + "COME_FROM_LOOP", + "COME_FROM", + ) diff --git a/uncompyle6/parsers/reducecheck/whilestmt38.py b/uncompyle6/parsers/reducecheck/whilestmt38.py new file mode 100644 index 00000000..d53d82a8 --- /dev/null +++ b/uncompyle6/parsers/reducecheck/whilestmt38.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def whilestmt38_check( + self, lhs: str, n: int, rule, ast, tokens: list, first: int, last: int +) -> bool: + # When we are missing a COME_FROM_LOOP, the + # "while" statement is nested inside an if/else + # so after the POP_BLOCK we have a JUMP_FORWARD which forms the "else" portion of the "if" + # Check this. + # print("XXX", first, last, rule) + # for t in range(first, last): + # print(tokens[t]) + # print("=" * 40) + + if tokens[last] != "COME_FROM" and tokens[last - 1] == "COME_FROM": + last -= 1 + if tokens[last - 1].kind.startswith("RAISE_VARARGS"): + return True + while tokens[last] == "COME_FROM": + last -= 1 + # In a "while" loop, (in contrast to "for" loop), the loop jump is + # always to the first offset + first_offset = tokens[first].off2int() + if tokens[last] == "JUMP_LOOP" and ( + tokens[last].attr == first_offset or tokens[last - 1].attr == first_offset + ): + return False + return True diff --git a/uncompyle6/semantics/customize37.py b/uncompyle6/semantics/customize37.py index 1dfc5fe7..fe4c0c4b 100644 --- a/uncompyle6/semantics/customize37.py +++ b/uncompyle6/semantics/customize37.py @@ -24,6 +24,8 @@ from uncompyle6.semantics.consts import ( from uncompyle6.semantics.helper import flatten_list +FSTRING_CONVERSION_MAP = {1: "!s", 2: "!r", 3: "!a", "X": ":X"} + ####################### def customize_for_version37(self, version): ######################## @@ -39,7 +41,9 @@ def customize_for_version37(self, version): PRECEDENCE["call_ex_kw4"] = 1 PRECEDENCE["call_kw"] = 0 PRECEDENCE["call_kw36"] = 1 - PRECEDENCE["formatted_value1"] = 100 + PRECEDENCE["formatted_value1"] = 38 # f"...". This has to be below "named_expr" to make + # f'{(x := 10)}' preserve parenthesis + PRECEDENCE["formatted_value2"] = 38 # See above PRECEDENCE["if_exp_37a"] = 28 PRECEDENCE["if_exp_37b"] = 28 PRECEDENCE["dict_unpack"] = 0 # **{...} diff --git a/uncompyle6/semantics/customize38.py b/uncompyle6/semantics/customize38.py index 0e6e5e70..02ae5089 100644 --- a/uncompyle6/semantics/customize38.py +++ b/uncompyle6/semantics/customize38.py @@ -20,6 +20,8 @@ ####################### from uncompyle6.semantics.consts import PRECEDENCE, TABLE_DIRECT +from uncompyle6.semantics.customize37 import FSTRING_CONVERSION_MAP +from uncompyle6.semantics.helper import escape_string, strip_quotes def customize_for_version38(self, version): @@ -125,7 +127,7 @@ def customize_for_version38(self, version): "set_for": (" for %c in %c", (2, "store"), (0, "expr_or_arg"),), "whilestmt38": ( "%|while %c:\n%+%c%-\n\n", - (1, "testexpr"), + (1, ("bool_op", "testexpr", "testexprc")), (2, ("l_stmts", "pass")), ), "whileTruestmt38": ( @@ -282,6 +284,54 @@ def customize_for_version38(self, version): self.n_set_afor = n_set_afor + def n_formatted_value_debug(node): + p = self.prec + self.prec = 100 + + formatted_value = node[1] + value_equal = node[0].attr + assert formatted_value.kind.startswith("formatted_value") + old_in_format_string = self.in_format_string + self.in_format_string = formatted_value.kind + format_value_attr = node[-1] + + post_str = "" + if node[-1] == "BUILD_STRING_3": + post_load_str = node[-2] + post_str = self.traverse(post_load_str, indent="") + post_str = strip_quotes(post_str) + + if format_value_attr == "FORMAT_VALUE_ATTR": + attr = format_value_attr.attr + if attr & 4: + fmt = strip_quotes(self.traverse(node[3], indent="")) + attr_flags = attr & 3 + if attr_flags: + conversion = "%s:%s" % ( + FSTRING_CONVERSION_MAP.get(attr_flags, ""), + fmt, + ) + else: + conversion = ":%s" % fmt + else: + conversion = FSTRING_CONVERSION_MAP.get(attr, "") + f_str = "f%s" % escape_string( + "{%s%s}%s" % (value_equal, conversion, post_str) + ) + else: + f_conversion = self.traverse(formatted_value, indent="") + # Remove leaving "f" and quotes + conversion = strip_quotes(f_conversion[1:]) + f_str = "f%s" % escape_string(f"{value_equal}{conversion}" + post_str) + + self.write(f_str) + self.in_format_string = old_in_format_string + + self.prec = p + self.prune() + + self.n_formatted_value_debug = n_formatted_value_debug + def n_suite_stmts_return(node): if len(node) > 1: assert len(node) == 2