diff --git a/tests/analyzers/test_template_tag.py b/tests/analyzers/test_template_tag.py index 913b424..f598ae9 100644 --- a/tests/analyzers/test_template_tag.py +++ b/tests/analyzers/test_template_tag.py @@ -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, @@ -16,7 +16,7 @@ class TestParseArgoTemplateTags: - def test_pass(self): + def test_1(self): tree = parse_argo_template_tags( """ Hello {{ inputs.parameters.name }} @@ -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 }}") @@ -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) @@ -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", @@ -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", diff --git a/tugboat/analyzers/template_tag.py b/tugboat/analyzers/template_tag.py index ca15a75..b5a0045 100644 --- a/tugboat/analyzers/template_tag.py +++ b/tugboat/analyzers/template_tag.py @@ -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( @@ -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: /[^{]+/ @@ -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]: @@ -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 @@ -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 diff --git a/tugboat/references/context.py b/tugboat/references/context.py index a703a9e..12192f5 100644 --- a/tugboat/references/context.py +++ b/tugboat/references/context.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing from collections.abc import MutableSet from pydantic import BaseModel, Field, InstanceOf @@ -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.""" @@ -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