From ca1679e636a6d2deef0658f9857ee0ddba9b17fe Mon Sep 17 00:00:00 2001 From: Daniel Bradburn Date: Wed, 10 May 2017 21:49:42 +0200 Subject: [PATCH] Added support for support for Python 3.6 CALL_FUNCTION_KW --- pytest/test_CALL_FUNCTION_KW.sh | 6 ++ pytest/test_function_call.py | 138 +++++++++++++++++++++---------- uncompyle6/parsers/parse3.py | 3 +- uncompyle6/parsers/parse36.py | 11 +++ uncompyle6/scanners/scanner36.py | 2 + uncompyle6/semantics/pysource.py | 14 ++++ 6 files changed, 128 insertions(+), 46 deletions(-) create mode 100644 pytest/test_CALL_FUNCTION_KW.sh diff --git a/pytest/test_CALL_FUNCTION_KW.sh b/pytest/test_CALL_FUNCTION_KW.sh new file mode 100644 index 00000000..5b3fe0ab --- /dev/null +++ b/pytest/test_CALL_FUNCTION_KW.sh @@ -0,0 +1,6 @@ +source ../.venv.3.6/bin/activate +py.test -k test_CALL_FUNCTION_KW +source ../.venv.3.5/bin/activate +py.test -k test_CALL_FUNCTION_KW +source ../.venv.2.7/bin/activate +py.test -k test_CALL_FUNCTION_KW \ No newline at end of file diff --git a/pytest/test_function_call.py b/pytest/test_function_call.py index 7b7b9729..099a208f 100644 --- a/pytest/test_function_call.py +++ b/pytest/test_function_call.py @@ -1,20 +1,24 @@ # std import string # 3rd party -from hypothesis import given, assume, strategies as st +from hypothesis import given, assume, example, settings, strategies as st import pytest # uncompyle from validate import validate_uncompyle +from test_fstring import expressions alpha = st.sampled_from(string.ascii_lowercase) numbers = st.sampled_from(string.digits) alphanum = st.sampled_from(string.ascii_lowercase + string.digits) -expressions = st.sampled_from([x for x in string.ascii_lowercase + string.digits] + ['x+1']) @st.composite -def function_calls(draw): +def function_calls(draw, + min_keyword_args=0, max_keyword_args=5, + min_positional_args=0, max_positional_args=5, + min_star_args=0, max_star_args=1, + min_double_star_args=0, max_double_star_args=1): """ Strategy factory for generating function calls. @@ -22,21 +26,49 @@ def function_calls(draw): :return: The function call text. """ - list1 = st.lists(alpha, min_size=0, max_size=1) - list3 = st.lists(alpha, min_size=0, max_size=3) + st_positional_args = st.lists( + alpha, + min_size=min_positional_args, + max_size=max_positional_args + ) + st_keyword_args = st.lists( + alpha, + min_size=min_keyword_args, + max_size=max_keyword_args + ) + st_star_args = st.lists( + alpha, + min_size=min_star_args, + max_size=max_star_args + ) + st_double_star_args = st.lists( + alpha, + min_size=min_double_star_args, + max_size=max_double_star_args + ) - positional_args = draw(list3) - named_args = [x + '=0' for x in draw(list3)] - star_args = ['*' + x for x in draw(list1)] - double_star_args = ['**' + x for x in draw(list1)] + positional_args = draw(st_positional_args) + keyword_args = draw(st_keyword_args) + st_values = st.lists( + expressions(), + min_size=len(keyword_args), + max_size=len(keyword_args) + ) + keyword_args = [ + x + '=' + e + for x, e in + zip(keyword_args, draw(st_values)) + ] + star_args = ['*' + x for x in draw(st_star_args)] + double_star_args = ['**' + x for x in draw(st_double_star_args)] - arguments = positional_args + named_args + star_args + double_star_args + arguments = positional_args + keyword_args + star_args + double_star_args draw(st.randoms()).shuffle(arguments) arguments = ','.join(arguments) function_call = 'fn({arguments})'.format(arguments=arguments) try: - # TODO: Figure out the exact rules for ordering of positional, named, + # TODO: Figure out the exact rules for ordering of positional, keyword, # star args, double star args and in which versions the various # types of arguments are supported so we don't need to check that the # expression compiles like this. @@ -46,9 +78,56 @@ def function_calls(draw): return function_call -@pytest.mark.xfail() -def test_CALL_FUNCTION(): - validate_uncompyle("fn(w,m,f)") +def test_function_no_args(): + validate_uncompyle("fn()") + + +def isolated_function_calls(which): + """ + Returns a strategy for generating function calls, but isolated to + particular types of arguments, for example only positional arguments. + + This can help reason about debugging errors in specific types of function + calls. + + :param which: One of 'keyword', 'positional', 'star', 'double_star' + + :return: Strategy for generating an function call isolated to specific + argument types. + """ + kwargs = dict( + max_keyword_args=0, + max_positional_args=0, + max_star_args=0, + max_double_star_args=0, + ) + kwargs['_'.join(('min', which, 'args'))] = 1 + kwargs['_'.join(('max', which, 'args'))] = 5 if 'star' not in which else 1 + return function_calls(**kwargs) + + +with settings(max_examples=25): + + @given(isolated_function_calls('positional')) + @example("fn(0)") + def test_function_positional_only(expr): + validate_uncompyle(expr) + + @given(isolated_function_calls('keyword')) + @example("fn(a=0)") + def test_function_call_keyword_only(expr): + validate_uncompyle(expr) + + @given(isolated_function_calls('star')) + @example("fn(*items)") + def test_function_call_star_only(expr): + validate_uncompyle(expr) + + @pytest.mark.xfail() + @given(isolated_function_calls('double_star')) + @example("fn(**{})") + def test_function_call_double_star_only(expr): + validate_uncompyle(expr) @pytest.mark.xfail() @@ -61,11 +140,6 @@ def test_BUILD_MAP_BUILD_MAP_UNPACK_WITH_CALL_BUILD_TUPLE_CALL_FUNCTION_EX(): validate_uncompyle("fn(a=0,**g)") -@pytest.mark.xfail() -def test_CALL_FUNCTION_KW(): - validate_uncompyle("fn(j=0)") - - @pytest.mark.xfail() def test_CALL_FUNCTION_EX(): validate_uncompyle("fn(*g,**j)") @@ -100,29 +174,3 @@ def test_BUILD_CONST_KEY_MAP_CALL_FUNCTION_EX(): @given(function_calls()) def test_function_call(function_call): validate_uncompyle(function_call) - - -examples = set() -generate_examples = False - - -@pytest.mark.skipif(not generate_examples, reason='not generating examples') -@given(function_calls()) -def test_generate_hypothesis(function_call): - examples.add(function_call) - - -@pytest.mark.skipif(not generate_examples, reason='not generating examples') -def test_generate_examples(): - import dis - example_opcodes = {} - for example in examples: - opcodes = tuple(sorted(set( - instruction.opname - for instruction in dis.Bytecode(example) - if instruction.opname not in ('LOAD_CONST', 'LOAD_NAME', 'RETURN_VALUE') - ))) - example_opcodes[opcodes] = example - for k, v in example_opcodes.items(): - print('def test_' + '_'.join(k) + '():\n validate_uncompyle("' + v + '")\n\n') - return diff --git a/uncompyle6/parsers/parse3.py b/uncompyle6/parsers/parse3.py index bef1f34b..624c780b 100644 --- a/uncompyle6/parsers/parse3.py +++ b/uncompyle6/parsers/parse3.py @@ -607,7 +607,8 @@ class Python3Parser(PythonParser): """, nop_func) continue elif opname in ('CALL_FUNCTION', 'CALL_FUNCTION_VAR', - 'CALL_FUNCTION_VAR_KW', 'CALL_FUNCTION_KW'): + 'CALL_FUNCTION_VAR_KW') \ + or opname.startswith('CALL_FUNCTION_KW'): self.custom_classfunc_rule(opname, token, customize) elif opname == 'LOAD_DICTCOMP': rule_pat = ("dictcomp ::= LOAD_DICTCOMP %sMAKE_FUNCTION_0 expr " diff --git a/uncompyle6/parsers/parse36.py b/uncompyle6/parsers/parse36.py index f93a86a8..dee9fcee 100644 --- a/uncompyle6/parsers/parse36.py +++ b/uncompyle6/parsers/parse36.py @@ -58,6 +58,17 @@ class Python36Parser(Python35Parser): """ % (fstring_expr_or_str_n, fstring_expr_or_str_n, "fstring_expr_or_str " * v) self.add_unique_doc_rules(rules_str, customize) + def custom_classfunc_rule(self, opname, token, customize): + + if opname.startswith('CALL_FUNCTION_KW'): + values = 'expr ' * token.attr + rule = 'call_function ::= expr kwargs_only_36 {token.type}'.format(**locals()) + self.add_unique_rule(rule, token.type, token.attr, customize) + rule = 'kwargs_only_36 ::= {values} LOAD_CONST'.format(**locals()) + self.add_unique_rule(rule, token.type, token.attr, customize) + else: + super(Python36Parser, self).custom_classfunc_rule(opname, token, customize) + class Python36ParserSingle(Python36Parser, PythonParserSingle): pass diff --git a/uncompyle6/scanners/scanner36.py b/uncompyle6/scanners/scanner36.py index 65ca0ba5..15265f12 100644 --- a/uncompyle6/scanners/scanner36.py +++ b/uncompyle6/scanners/scanner36.py @@ -28,6 +28,8 @@ class Scanner36(Scanner3): if t.op == self.opc.CALL_FUNCTION_EX and t.attr & 1: t.type = 'CALL_FUNCTION_EX_KW' pass + if t.op == self.opc.CALL_FUNCTION_KW: + t.type = 'CALL_FUNCTION_KW_{t.attr}'.format(**locals()) pass return tokens, customize diff --git a/uncompyle6/semantics/pysource.py b/uncompyle6/semantics/pysource.py index e4d8666e..2b9a0323 100644 --- a/uncompyle6/semantics/pysource.py +++ b/uncompyle6/semantics/pysource.py @@ -392,6 +392,7 @@ class SourceWalker(GenericASTTraversal, object): 'fstring_single': ( "f'{%c%{conversion}}'", 0), 'fstring_multi': ( "f'%c'", 0), 'func_args36': ( "%c(**", 0), + #'kwargs_only_36': ( "%c(**", 0), }) TABLE_R.update({ 'CALL_FUNCTION_EX': ('%c(*%P)', 0, (1, 2, ', ', 100)), @@ -413,6 +414,17 @@ class SourceWalker(GenericASTTraversal, object): self.default(node) self.n_fstring_single = n_fstring_single + def n_kwargs_only_36(node): + keys = node[-1].attr + num_kwargs = len(keys) + values = node[:num_kwargs] + for i, (key, value) in enumerate(zip(keys, values)): + self.write(key + '=') + self.preorder(value) + if i < num_kwargs: + self.write(',') + self.prune() + self.n_kwargs_only_36 = n_kwargs_only_36 return f = property(lambda s: s.params['f'], @@ -1800,6 +1812,8 @@ class SourceWalker(GenericASTTraversal, object): if k.startswith('CALL_METHOD'): # This happens in PyPy only TABLE_R[k] = ('%c(%P)', 0, (1, -1, ', ', 100)) + elif self.version >= 3.6 and k.startswith('CALL_FUNCTION_KW'): + TABLE_R[k] = ('%c(%P)', 0, (1, -1, ', ', 100)) elif op == 'CALL_FUNCTION': TABLE_R[k] = ('%c(%P)', 0, (1, -1, ', ', 100)) elif op in ('CALL_FUNCTION_VAR',