Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions tests/analyzers/test_template_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from tests.dirty_equals import HasSubstring
from tugboat.analyzers.template_tag import (
check_simple_tag_reference,
_check_simple_tag_reference,
check_template_tags,
parse_argo_template_tags,
split_expr_membership,
Expand All @@ -16,7 +16,7 @@

class TestParseArgoTemplateTags:

def test_pass(self):
def test_1(self):
tree = parse_argo_template_tags(
"""
Hello {{ inputs.parameters.name }}
Expand All @@ -34,6 +34,24 @@ def test_pass(self):
(bot_name,) = bot_name_tag.find_token("REF")
assert bot_name == "inputs.parameters['bot']"

def test_2(self):
tree = parse_argo_template_tags(
"""
This is a {sample} document
that have some {{= expression{}}} inside.
"""
)

simple_tags = list(tree.find_data("simple_tag"))
assert not simple_tags

expr_tags = list(tree.find_data("expression_tag"))
assert len(expr_tags) == 1

def test_empty(self):
tree = parse_argo_template_tags("")
assert tree.children == []

def test_error(self):
with pytest.raises(lark.exceptions.UnexpectedCharacters):
parse_argo_template_tags("{{ inputs. parameters.name }}")
Expand All @@ -44,8 +62,7 @@ def test_error(self):
class TestCheckTemplateTags:

def test_pass(self):
references = ReferenceCollection()
references.add(("inputs", "parameters", "name"))
references = ReferenceCollection([("inputs", "parameters", "name")])

diagnoses = list(
check_template_tags("{{ inputs.parameters.name }}", references)
Expand Down Expand Up @@ -103,17 +120,15 @@ def test_unexpected_eof(self):
class TestCheckSimpleTagReference:

def test_pass(self):
references = ReferenceCollection()
references.add(("inputs", "parameters", "name"))
references = ReferenceCollection([("inputs", "parameters", "name")])

diagnosis = check_simple_tag_reference("inputs.parameters.name", references)
diagnosis = _check_simple_tag_reference("inputs.parameters.name", references)
assert diagnosis is None

def test_incorrect_format(self):
references = ReferenceCollection()
references.add(("inputs", "parameters", "name"))
references = ReferenceCollection([("inputs", "parameters", "name")])

diagnosis = check_simple_tag_reference("inputs.parameters['name']", references)
diagnosis = _check_simple_tag_reference("inputs.parameters['name']", references)

assert diagnosis == IsPartialDict(
code="VAR102",
Expand All @@ -123,19 +138,18 @@ def test_incorrect_format(self):

def test_syntax_error(self):
references = ReferenceCollection()
diagnosis = check_simple_tag_reference("inputs.parameters['name", references)
diagnosis = _check_simple_tag_reference("inputs.parameters['name", references)
assert diagnosis == IsPartialDict(code="VAR101")

def test_not_a_argo_variable_1(self):
references = ReferenceCollection()
diagnosis = check_simple_tag_reference("inputs", references)
diagnosis = _check_simple_tag_reference("inputs", references)
assert diagnosis == IsPartialDict(code="VAR202")

def test_unknown_variable(self):
references = ReferenceCollection()
references.add(("demo",))
references = ReferenceCollection([("demo",)])

diagnosis = check_simple_tag_reference("inputs", references)
diagnosis = _check_simple_tag_reference("inputs", references)
assert diagnosis == IsPartialDict(
code="VAR201",
fix="demo",
Expand Down
50 changes: 24 additions & 26 deletions tugboat/analyzers/template_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@
from tugboat.types import Diagnosis


@functools.lru_cache(32)
def parse_argo_template_tags(source: str) -> lark.Tree:
"""
Parse Argo template tags in the given source string.

Parameters
----------
source : str
The source string containing Argo template tags.

Returns
-------
lark.Tree
The parse tree representing the structure of the template tags.
"""
parser = _argo_template_tag_parser()
return parser.parse(source)


@functools.cache
def _argo_template_tag_parser():
return lark.Lark(
Expand All @@ -35,9 +54,7 @@ def _argo_template_tag_parser():
%import common.LETTER
%import common.WS

?start: template

template: (_TEXT | expression_tag | simple_tag)+
?start: (_TEXT | expression_tag | simple_tag)*

# ignore other text
_TEXT: /[^{]+/
Expand All @@ -48,31 +65,12 @@ def _argo_template_tag_parser():
REF: (LETTER | DIGIT | "_" | "-" | "." | "\"" | "'" | "[" | "]")+

# expression template tag
expression_tag: "{{=" WS? _ANY WS? "}}"
_ANY: /[^}]+/
expression_tag: "{{=" _ANY "}}"
_ANY: /([^}]|}(?!}))+/
"""
)


@functools.lru_cache(32)
def parse_argo_template_tags(source: str) -> lark.Tree:
"""
Parse Argo template tags in the given source string.

Parameters
----------
source : str
The source string containing Argo template tags.

Returns
-------
lark.Tree
The parse tree representing the structure of the template tags.
"""
parser = _argo_template_tag_parser()
return parser.parse(source)


def check_template_tags(
source: str, references: ReferenceCollection
) -> Iterator[Diagnosis]:
Expand Down Expand Up @@ -101,7 +99,7 @@ def check_template_tags(
for tag in tree.find_data("simple_tag"):
(ref_repr,) = tag.find_token("REF")
ref_repr = typing.cast("Token", ref_repr)
if diagnosis := check_simple_tag_reference(ref_repr, references):
if diagnosis := _check_simple_tag_reference(ref_repr, references):
yield diagnosis


Expand Down Expand Up @@ -137,7 +135,7 @@ def transform_lark_error(source: str, e: UnexpectedInput) -> Diagnosis:
}


def check_simple_tag_reference(
def _check_simple_tag_reference(
ref_repr: str, references: ReferenceCollection
) -> Diagnosis | None:
# early exit if the reference is known
Expand Down
11 changes: 10 additions & 1 deletion tugboat/references/context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import typing
from collections.abc import MutableSet

from pydantic import BaseModel, Field, InstanceOf
Expand All @@ -10,6 +11,9 @@
normalized_distance as dameau_levenshtein_normalized_distance,
)

if typing.TYPE_CHECKING:
from collections.abc import Iterable

type _TR = tuple[str, ...]
"""Type alias for reference, which is a tuple of strings."""

Expand Down Expand Up @@ -39,10 +43,15 @@ class ReferenceCollection(MutableSet[_TR]):
of :py:data:`AnyStr`.
"""

def __init__(self):
def __init__(self, iterable: Iterable[_TR] = ()):
super().__init__()

self._static = set()
self._dynamic = []

for item in iterable:
self.add(item)

def __iter__(self):
yield from self._static
yield from self._dynamic
Expand Down