From ba0039ee00ef4b6457fec2291fbcb3ae0113b820 Mon Sep 17 00:00:00 2001 From: robcxyz Date: Sat, 30 Jul 2022 21:15:36 -0600 Subject: [PATCH] chore: refactor macors into own file --- tackle/macros.py | 156 +++++++++++++++++++++++ tackle/parser.py | 150 +--------------------- tests/parser/test_parser_args_handler.py | 6 +- tests/render/test_render.py | 2 + 4 files changed, 169 insertions(+), 145 deletions(-) create mode 100644 tackle/macros.py diff --git a/tackle/macros.py b/tackle/macros.py new file mode 100644 index 000000000..fbd7910d2 --- /dev/null +++ b/tackle/macros.py @@ -0,0 +1,156 @@ +from typing import TYPE_CHECKING + +from tackle.utils.dicts import ( + nested_get, + nested_delete, + nested_set, + get_target_and_key, + smush_key_path, +) + +from tackle.models import BaseHook + +if TYPE_CHECKING: + from tackle.models import Context + + +def var_hook_macro(args) -> list: + """ + Handler for cases where we have a hook call with a renderable string as the first + argument which we rewrite as a var hook. For instance `foo->: foo-{{ bar }}-baz` + would be rewritten as `foo->: var foo-{{bar}}-baz`. + """ + if '{{' in args[0]: + # We split up the string before based on whitespace so eval individually + if '}}' in args[0]: + # This is single templatable string -> key->: "{{this}}" => args: ['this'] + args.insert(0, 'var') + else: + # Situation where we have key->: "{{ this }}" => args: ['{{', 'this' '}}'] + for i in range(1, len(args)): + if '}}' in args[i]: + joined_template = ' '.join(args[: (i + 1)]) + other_args = args[(i + 1) :] + args = ['var'] + [joined_template] + other_args + break + return args + + +def blocks_macro(context: 'Context'): + """ + Handle keys appended with arrows and interpret them as `block` hooks. Value is + re-written over with a `block` hook to support the following syntax. + + a-key->: + if: stuff == 'things' + foo->: print ... + to + a-key: + ->: block + if: stuff == 'things' + items: + foo->: print ... + """ + # Break up key paths + base_key_path = context.key_path[:-1] + new_key = [context.key_path[-1][:-2]] + # Handle embedded blocks which need to have their key paths adjusted + key_path = (base_key_path + new_key)[len(context.key_path_block) :] + arrow = [context.key_path[-1][-2:]] + + indexed_key_path = context.key_path[ + (len(context.key_path_block) - len(context.key_path)) : + ] + input_dict = nested_get( + element=context.input_context, + keys=indexed_key_path[:-1], + ) + value = input_dict[indexed_key_path[-1]] + + for k, v in list(input_dict.items()): + if k == indexed_key_path[-1]: + nested_set(context.input_context, key_path, {arrow[0]: 'block'}) + nested_delete(context.input_context, indexed_key_path) + else: + input_dict[k] = input_dict.pop(k) + + # Iterate through the block keys except for the reserved keys like `for` or `if` + aliases = [v.alias for _, v in BaseHook.__fields__.items()] + ['->', '_>'] + for k, v in value.copy().items(): + if k in aliases: + nested_set( + element=context.input_context, + keys=key_path + [k], + value=v, + ) + else: + # Set the keys under the `items` key per the block hook's input + nested_set( + element=context.input_context, + keys=key_path + ['items', k], + value=v, + ) + + +def compact_hook_call_macro(context: 'Context', element: str) -> dict: + """ + Rewrite the input_context with an expanded expression on the called compact key. + Returns the string element as a dict with the key as the arrow and element as + value. + """ + # TODO: Clean this up + base_key_path = context.key_path[:-1] + new_key = [context.key_path[-1][:-2]] + key_path = (base_key_path + new_key)[len(context.key_path_block) :] + arrow = context.key_path[-1][-2:] + + extra_keys = len(context.key_path) - len(context.key_path_block) + old_key_path = context.key_path[-extra_keys:] + + value = nested_get( + element=context.input_context, + keys=smush_key_path(old_key_path)[:-1], + ) + + replacement = {context.key_path[-1]: new_key[0]} + for k, v in list(value.items()): + value[replacement.get(k, k)] = ( + value.pop(k) if k != context.key_path[-1] else {arrow: value.pop(k)} + ) + + # Reset the key_path without arrow + context.key_path = context.key_path_block + key_path + + return {arrow: element} + + +def list_to_var_macro(context: 'Context', element: list) -> dict: + """ + Convert arrow keys with a list as the value to `var` hooks via a re-write to the + input. + """ + # TODO: Convert this to a block. Issue is that keys are not rendered by default so + # when str items in a list are parsed, they are not rendered by default. Should + # have some validator or something on block to render str items in a list. + base_key_path = context.key_path[:-1] + new_key = [context.key_path[-1][:-2]] + old_key_path = context.key_path[len(context.key_path_block) :] + arrow = context.key_path[-1][-2:] + + _, key_path = get_target_and_key(context) + if isinstance(context.input_context, dict): + nested_set( + element=context.input_context, + keys=base_key_path[-(len(base_key_path) - len(context.key_path_block)) :] + + new_key, + value={arrow: 'var', 'input': element}, + ) + # Remove the old key + nested_delete(context.input_context, old_key_path) + context.key_path = base_key_path + new_key + + else: + context.input_context = {arrow: 'var', 'input': element} + context.key_path = base_key_path + + return {arrow: 'var'} diff --git a/tackle/parser.py b/tackle/parser.py index 89d4d9170..52304a366 100644 --- a/tackle/parser.py +++ b/tackle/parser.py @@ -47,6 +47,12 @@ LazyBaseFunction, BaseContext, ) +from tackle.macros import ( + var_hook_macro, + blocks_macro, + compact_hook_call_macro, + list_to_var_macro, +) # TODO: Replace with single import from tackle.exceptions import ( @@ -576,28 +582,6 @@ def evaluate_args( ) -def handle_leading_brackets(args) -> list: - """ - Handler for cases where we have a hook call with a renderable string as the first - argument which we rewrite as a var hook. For instance `foo->: foo-{{ bar }}-baz` - would be rewritten as `foo->: var foo-{{bar}}-baz`. - """ - if '{{' in args[0]: - # We split up the string before based on whitespace so eval individually - if '}}' in args[0]: - # This is single templatable string -> key->: "{{this}}" => args: ['this'] - args.insert(0, 'var') - else: - # Situation where we have key->: "{{ this }}" => args: ['{{', 'this' '}}'] - for i in range(1, len(args)): - if '}}' in args[i]: - joined_template = ' '.join(args[: (i + 1)]) - other_args = args[(i + 1) :] - args = ['var'] + [joined_template] + other_args - break - return args - - def run_hook(context: 'Context'): """ Run the hook by qualifying the input argument and matching the input params with the @@ -606,7 +590,7 @@ def run_hook(context: 'Context'): """ if isinstance(context.input_string, str): args, kwargs, flags = unpack_args_kwargs_string(context.input_string) - args = handle_leading_brackets(args) + args = var_hook_macro(args) # Remove first args it will be consumed and no longer relevant first_arg = args.pop(0) @@ -676,126 +660,6 @@ def run_hook(context: 'Context'): parse_hook(hook_dict, Hook, context) -def blocks_macro(context: 'Context'): - """ - Handle keys appended with arrows and interpret them as `block` hooks. Value is - re-written over with a `block` hook to support the following syntax. - - a-key->: - if: stuff == 'things' - foo->: print ... - to - a-key: - ->: block - if: stuff == 'things' - items: - foo->: print ... - """ - # Break up key paths - base_key_path = context.key_path[:-1] - new_key = [context.key_path[-1][:-2]] - # Handle embedded blocks which need to have their key paths adjusted - key_path = (base_key_path + new_key)[len(context.key_path_block) :] - arrow = [context.key_path[-1][-2:]] - - indexed_key_path = context.key_path[ - (len(context.key_path_block) - len(context.key_path)) : - ] - input_dict = nested_get( - element=context.input_context, - keys=indexed_key_path[:-1], - ) - value = input_dict[indexed_key_path[-1]] - - for k, v in list(input_dict.items()): - if k == indexed_key_path[-1]: - nested_set(context.input_context, key_path, {arrow[0]: 'block'}) - nested_delete(context.input_context, indexed_key_path) - else: - input_dict[k] = input_dict.pop(k) - - # Iterate through the block keys except for the reserved keys like `for` or `if` - aliases = [v.alias for _, v in BaseHook.__fields__.items()] + ['->', '_>'] - for k, v in value.copy().items(): - if k in aliases: - nested_set( - element=context.input_context, - keys=key_path + [k], - value=v, - ) - else: - # Set the keys under the `items` key per the block hook's input - nested_set( - element=context.input_context, - keys=key_path + ['items', k], - value=v, - ) - - -def compact_hook_call_macro(context: 'Context', element: str) -> dict: - """ - Rewrite the input_context with an expanded expression on the called compact key. - Returns the string element as a dict with the key as the arrow and element as - value. - """ - # TODO: Clean this up - base_key_path = context.key_path[:-1] - new_key = [context.key_path[-1][:-2]] - key_path = (base_key_path + new_key)[len(context.key_path_block) :] - arrow = context.key_path[-1][-2:] - - extra_keys = len(context.key_path) - len(context.key_path_block) - old_key_path = context.key_path[-extra_keys:] - - value = nested_get( - element=context.input_context, - keys=smush_key_path(old_key_path)[:-1], - ) - - replacement = {context.key_path[-1]: new_key[0]} - for k, v in list(value.items()): - value[replacement.get(k, k)] = ( - value.pop(k) if k != context.key_path[-1] else {arrow: value.pop(k)} - ) - - # Reset the key_path without arrow - context.key_path = context.key_path_block + key_path - - return {arrow: element} - - -def list_to_var_macro(context: 'Context', element: list) -> dict: - """ - Convert arrow keys with a list as the value to `var` hooks via a re-write to the - input. - """ - # TODO: Convert this to a block. Issue is that keys are not rendered by default so - # when str items in a list are parsed, they are not rendered by default. Should - # have some validator or something on block to render str items in a list. - base_key_path = context.key_path[:-1] - new_key = [context.key_path[-1][:-2]] - old_key_path = context.key_path[len(context.key_path_block) :] - arrow = context.key_path[-1][-2:] - - _, key_path = get_target_and_key(context) - if isinstance(context.input_context, dict): - nested_set( - element=context.input_context, - keys=base_key_path[-(len(base_key_path) - len(context.key_path_block)) :] - + new_key, - value={arrow: 'var', 'input': element}, - ) - # Remove the old key - nested_delete(context.input_context, old_key_path) - context.key_path = base_key_path + new_key - - else: - context.input_context = {arrow: 'var', 'input': element} - context.key_path = base_key_path - - return {arrow: 'var'} - - def walk_sync(context: 'Context', element): """ Traverse an object looking for hook calls and running those hooks. Here we are diff --git a/tests/parser/test_parser_args_handler.py b/tests/parser/test_parser_args_handler.py index fe5db0454..d5a5ed767 100644 --- a/tests/parser/test_parser_args_handler.py +++ b/tests/parser/test_parser_args_handler.py @@ -1,7 +1,9 @@ import pytest from tackle import tackle -from tackle.parser import handle_leading_brackets + +# from tackle.parser import handle_leading_brackets +from tackle.macros import var_hook_macro from tackle.utils.command import unpack_args_kwargs_string TEMPLATES = [ @@ -23,7 +25,7 @@ def test_unpack_args_kwargs_handle_leading_brackets( ): """Validate the count of each input arg/kwarg/flag.""" args, kwargs, flags = unpack_args_kwargs_string(template) - args = handle_leading_brackets(args) + args = var_hook_macro(args) assert len_args == len(args) assert len_kwargs == len(kwargs) diff --git a/tests/render/test_render.py b/tests/render/test_render.py index de0299c55..aa6be8918 100644 --- a/tests/render/test_render.py +++ b/tests/render/test_render.py @@ -15,6 +15,8 @@ # Normal ({'adict': {'stuff': 'things'}}, '{{adict}}', {'stuff': 'things'}), ({'list': ['stuff', 'things']}, '{{list}}', ['stuff', 'things']), + # Dashes don't work -> + # ({'a-dash': 'dash'}, '{{a-dash}}', 'dash'), ]