Skip to content

Commit

Permalink
chore: refactor macors into own file
Browse files Browse the repository at this point in the history
  • Loading branch information
robcxyz committed Jul 31, 2022
1 parent 8c33a45 commit ba0039e
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 145 deletions.
156 changes: 156 additions & 0 deletions tackle/macros.py
Original file line number Diff line number Diff line change
@@ -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'}
150 changes: 7 additions & 143 deletions tackle/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions tests/parser/test_parser_args_handler.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions tests/render/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]


Expand Down

0 comments on commit ba0039e

Please sign in to comment.