Merge pull request #60 from rocky/buildstring

Buildstring
This commit is contained in:
Daniel Bradburn
2016-10-22 20:08:30 +02:00
committed by GitHub
7 changed files with 77 additions and 43 deletions

View File

@@ -21,9 +21,13 @@ def expressions(draw):
'container', 'container',
'self.attribute', 'self.attribute',
'self.method()', 'self.method()',
'sorted(items, key=lambda x: x.name)', # These expressions are failing, I think these are control
'func(*args, **kwargs)', # flow problems rather than problems with FORMAT_VALUE,
'text or default', # however I need to confirm this...
#'sorted(items, key=lambda x: x.name)',
#'func(*args, **kwargs)',
#'text or default',
#'43 if life_the_universe and everything else None'
))) )))
@@ -119,6 +123,8 @@ def test_format_specifiers(format_specifier):
def run_test(text): def run_test(text):
hypothesis.assume(len(text))
hypothesis.assume("f'{" in text)
expr = text + '\n' expr = text + '\n'
code = compile(expr, '<string>', 'single') code = compile(expr, '<string>', 'single')
deparsed = deparse_code(PYTHON_VERSION, code, compile_mode='single') deparsed = deparse_code(PYTHON_VERSION, code, compile_mode='single')
@@ -136,8 +142,8 @@ def test_uncompyle_fstring(fstring):
@pytest.mark.skipif(PYTHON_VERSION < 3.6, reason='need at least python 3.6') @pytest.mark.skipif(PYTHON_VERSION < 3.6, reason='need at least python 3.6')
@pytest.mark.parametrize('fstring', [ @pytest.mark.parametrize('fstring', [
#"f'{abc}{abc!s}'", "f'{abc}{abc!s}'",
"f'{abc!s}'", "f'{abc}0'",
]) ])
def test_uncompyle_direct(fstring): def test_uncompyle_direct(fstring):
"""useful for debugging""" """useful for debugging"""

Binary file not shown.

View File

@@ -1,3 +1,5 @@
var1 = 'x' var1 = 'x'
var2 = 'y' var2 = 'y'
print(f'interpolate {var1} strings {var2!r} {var2!s} py36') print(f'interpolate {var1} strings {var2!r} {var2!s} py36')
print(f'{abc}0')
print(f'{abc}{abc!s}')

View File

@@ -42,13 +42,22 @@ class PythonParser(GenericASTBuilder):
return return
def add_unique_rules(self, rules, customize): def add_unique_rules(self, rules, customize):
"""Add rules to grammar """Add rules (a list of string) to grammar
""" """
for rule in rules: for rule in rules:
if len(rule) == 0:
continue
opname = rule.split('::=')[0].strip() opname = rule.split('::=')[0].strip()
self.add_unique_rule(rule, opname, 0, customize) self.add_unique_rule(rule, opname, 0, customize)
return return
def add_unique_doc_rules(self, rules_str, customize):
"""Add rules (a docstring-like list of rules) to grammar
"""
rules = [r.strip() for r in rules_str.split("\n")]
self.add_unique_rules(rules, customize)
return
def cleanup(self): def cleanup(self):
""" """
Remove recursive references to allow garbage Remove recursive references to allow garbage

View File

@@ -500,7 +500,6 @@ class Python3Parser(PythonParser):
load_attr ::= expr LOOKUP_METHOD load_attr ::= expr LOOKUP_METHOD
call_function ::= expr CALL_METHOD call_function ::= expr CALL_METHOD
""" """
saw_format_value = False
for i, token in enumerate(tokens): for i, token in enumerate(tokens):
opname = token.type opname = token.type
opname_base = opname[:opname.rfind('_')] opname_base = opname[:opname.rfind('_')]
@@ -513,13 +512,6 @@ class Python3Parser(PythonParser):
assign2_pypy ::= expr expr designator designator assign2_pypy ::= expr expr designator designator
""", nop_func) """, nop_func)
continue continue
elif opname == 'FORMAT_VALUE':
# Python 3.6+
self.addRule("""
expr ::= fstring_expr
fstring_expr ::= expr FORMAT_VALUE
""", nop_func)
elif opname in ('CALL_FUNCTION', 'CALL_FUNCTION_VAR', elif opname in ('CALL_FUNCTION', 'CALL_FUNCTION_VAR',
'CALL_FUNCTION_VAR_KW', 'CALL_FUNCTION_KW'): 'CALL_FUNCTION_VAR_KW', 'CALL_FUNCTION_KW'):
self.custom_classfunc_rule(opname, token, customize) self.custom_classfunc_rule(opname, token, customize)
@@ -542,14 +534,6 @@ class Python3Parser(PythonParser):
if opname_base == 'BUILD_TUPLE': if opname_base == 'BUILD_TUPLE':
rule = ('load_closure ::= %s%s' % (('LOAD_CLOSURE ' * v), opname)) rule = ('load_closure ::= %s%s' % (('LOAD_CLOSURE ' * v), opname))
self.add_unique_rule(rule, opname, token.attr, customize) self.add_unique_rule(rule, opname, token.attr, customize)
if opname_base == 'BUILD_LIST' and saw_format_value:
format_or_str_n = "formatted_value_or_str_%s" % v
self.addRule("""
expr ::= joined_str
joined_str ::= LOAD_CONST LOAD_ATTR %s CALL_FUNCTION_1
%s ::= %s%s
""" % (format_or_str_n, format_or_str_n, ("formatted_value_or_str " *v), opname),
nop_func)
elif opname == 'LOOKUP_METHOD': elif opname == 'LOOKUP_METHOD':
# A PyPy speciality - DRY with parse2 # A PyPy speciality - DRY with parse2
@@ -734,7 +718,6 @@ class Python33ParserSingle(Python33Parser, PythonParserSingle):
def info(args): def info(args):
# Check grammar # Check grammar
# Should also add a way to dump grammar
p = Python3Parser() p = Python3Parser()
if len(args) > 0: if len(args) > 0:
arg = args[0] arg = args[0]
@@ -746,7 +729,9 @@ def info(args):
elif arg == '3.2': elif arg == '3.2':
p = Python32Parser() p = Python32Parser()
p.checkGrammar() p.checkGrammar()
if len(sys.argv) > 1 and sys.argv[1] == 'dump':
print('-' * 50)
p.dumpGrammar()
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys

View File

@@ -16,16 +16,37 @@ class Python36Parser(Python35Parser):
def p_36misc(self, args): def p_36misc(self, args):
""" """
formatted_value ::= LOAD_FAST FORMAT_VALUE fstring_multi ::= fstring_expr_or_strs BUILD_STRING
str ::= LOAD_CONST fstring_expr_or_strs ::= fstring_expr_or_strs fstring_expr_or_str
joined_str ::= LOAD_CONST LOAD_ATTR format_value_or_strs fstring_expr_or_strs ::= fstring_expr_or_str
BUILD_LIST CALL_FUNCTION
format_value_or_strs ::= format_value_or_strs format_value_or_str
format_value_or_strs ::= format_value_or_str
format_value_or_str ::= format_value
format_value_or_str ::= str
""" """
def add_custom_rules(self, tokens, customize):
super(Python36Parser, self).add_custom_rules(tokens, customize)
for i, token in enumerate(tokens):
opname = token.type
if opname == 'FORMAT_VALUE':
rules_str = """
expr ::= fstring_single
fstring_single ::= expr FORMAT_VALUE
"""
self.add_unique_doc_rules(rules_str, customize)
elif opname == 'BUILD_STRING':
v = token.attr
fstring_expr_or_str_n = "fstring_expr_or_str_%s" % v
rules_str = """
expr ::= fstring_expr
fstring_expr ::= expr FORMAT_VALUE
str ::= LOAD_CONST
fstring_expr_or_str ::= fstring_expr
fstring_expr_or_str ::= str
expr ::= fstring_multi
fstring_multi ::= %s BUILD_STRING
%s ::= %sBUILD_STRING
""" % (fstring_expr_or_str_n, fstring_expr_or_str_n, "fstring_expr_or_str " * v)
self.add_unique_doc_rules(rules_str, customize)
class Python36ParserSingle(Python36Parser, PythonParserSingle): class Python36ParserSingle(Python36Parser, PythonParserSingle):
pass pass

View File

@@ -514,12 +514,11 @@ class SourceWalker(GenericASTTraversal, object):
self.name = None self.name = None
self.version = version self.version = version
self.is_pypy = is_pypy self.is_pypy = is_pypy
self.customize_for_version(is_pypy, version) self.customize_for_version(is_pypy, version)
return return
@staticmethod def customize_for_version(self, is_pypy, version):
def customize_for_version(is_pypy, version):
if is_pypy: if is_pypy:
######################## ########################
# PyPy changes # PyPy changes
@@ -632,8 +631,26 @@ class SourceWalker(GenericASTTraversal, object):
# Python 3.6+ Additions # Python 3.6+ Additions
####################### #######################
TABLE_DIRECT.update({ TABLE_DIRECT.update({
'fstring_expr': ( "f'{%c%{conversion}}'", 0), 'fstring_expr': ( "{%c%{conversion}}", 0),
'fstring_single': ( "f'{%c%{conversion}}'", 0),
'fstring_multi': ( "f'%c'", 0),
}) })
FSTRING_CONVERSION_MAP = {1: '!s', 2: '!r', 3: '!a'}
def f_conversion(node):
node.conversion = FSTRING_CONVERSION_MAP.get(node.data[1].attr, '')
def n_fstring_expr(node):
f_conversion(node)
self.default(node)
self.n_fstring_expr = n_fstring_expr
def n_fstring_single(node):
f_conversion(node)
self.default(node)
self.n_fstring_single = n_fstring_single
return return
f = property(lambda s: s.params['f'], f = property(lambda s: s.params['f'],
@@ -1886,12 +1903,6 @@ class SourceWalker(GenericASTTraversal, object):
node[-2][0].type = 'unpack_w_parens' node[-2][0].type = 'unpack_w_parens'
self.default(node) self.default(node)
FSTRING_CONVERSION_MAP = {1: '!s', 2: '!r', 3: '!a'}
def n_fstring_expr(self, node):
node.conversion = self.FSTRING_CONVERSION_MAP.get(node.data[1].attr, '')
self.default(node)
def engine(self, entry, startnode): def engine(self, entry, startnode):
"""The format template interpetation engine. See the comment at the """The format template interpetation engine. See the comment at the
beginning of this module for the how we interpret format specifications such as beginning of this module for the how we interpret format specifications such as