Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #186: Parser cannot handle addition in JINJA replacements #191

Merged
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
7 changes: 6 additions & 1 deletion conda_recipe_manager/parser/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class Regex:
V0_UNSUPPORTED_JINJA: Final[list[re.Pattern[str]]] = [re.compile(r"\.join\(")]

# Pattern to detect Jinja variable names and functions
_JINJA_VAR_FUNCTION_PATTERN: Final[str] = r"[a-zA-Z_][a-zA-Z0-9_\|\'\"\(\)\[\]\, =\.\-~]*"
_JINJA_VAR_FUNCTION_PATTERN: Final[str] = r"[a-zA-Z0-9_\|\'\"\(\)\[\]\, =\.\-~\+]*"

## Pre-process conversion tooling regular expressions ##
# Finds `environ[]` used by a some recipe files. Requires a whitespace character to prevent matches with
Expand Down Expand Up @@ -173,12 +173,17 @@ class Regex:
JINJA_FUNCTION_UPPER: Final[re.Pattern[str]] = re.compile(r"\|\s*(upper)")
JINJA_FUNCTION_REPLACE: Final[re.Pattern[str]] = re.compile(r"\|\s*(replace)\((.*)\)")
JINJA_FUNCTION_IDX_ACCESS: Final[re.Pattern[str]] = re.compile(r"(\w+)\[(\d+)\]")
JINJA_FUNCTION_ADD_CONCAT: Final[re.Pattern[str]] = re.compile(
r"([\"\']?[\w\.]+[\"\']?)\s*\+\s*([\"\']?[\w\.]+[\"\']?)"
)
# `match()` is a JINJA function available in the V1 recipe format
JINJA_FUNCTION_MATCH: Final[re.Pattern[str]] = re.compile(r"match\(.*,.*\)")
JINJA_FUNCTIONS_SET: Final[set[re.Pattern[str]]] = {
JINJA_FUNCTION_LOWER,
JINJA_FUNCTION_UPPER,
JINJA_FUNCTION_REPLACE,
JINJA_FUNCTION_IDX_ACCESS,
JINJA_FUNCTION_ADD_CONCAT,
JINJA_FUNCTION_MATCH,
}

Expand Down
56 changes: 50 additions & 6 deletions conda_recipe_manager/parser/recipe_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,13 @@ def _set_on_schema_version(self) -> tuple[int, re.Pattern[str]]:
@staticmethod
def _set_key_and_matches(
key: str,
) -> tuple[str, Optional[re.Match[str]], Optional[re.Match[str]], Optional[re.Match[str]]]:
) -> tuple[str, Optional[re.Match[str]], Optional[re.Match[str]], Optional[re.Match[str]], Optional[re.Match[str]]]:
"""
Helper function for `_render_jinja_vars()` that takes a JINJA statement (string inside the braces) and attempts
to match and apply any currently supported "JINJA functions" to the statement.

:param key: Sanitized key to perform JINJA functions on.
:returns: The modified key, if any JINJA functions apply.
:returns: The modified key, if any JINJA functions apply. Also returns any applicable match objects.
"""
# TODO add support for REPLACE

Expand All @@ -263,7 +263,39 @@ def _set_key_and_matches(
if idx_match:
key = key.replace(f"[{cast(str, idx_match.group(2))}]", "").strip()

return key, lower_match, upper_match, idx_match
# Addition/concatenation. Note the key(s) will need to be evaluated later.
# Example: {{ build_number + 100 }}
# Example: {{ version + ".1" }}
add_concat_match = Regex.JINJA_FUNCTION_ADD_CONCAT.search(key)

return key, lower_match, upper_match, idx_match, add_concat_match

def _eval_jinja_token(self, s: str) -> JsonType:
"""
Given a string that matches one of the two groups in the `JINJA_FUNCTION_ADD_CONCAT` regex, evaluate the
string's intended value. NOTE: This does not invoke `eval()` for security reasons.

:param s: The string to evaluate.
:returns: The evaluated value of the string.
"""
# Variable
if s in self._vars_tbl:
return self._vars_tbl[s]

# int
if s.isdigit():
return int(s)

# float
try:
return float(s)
except ValueError:
pass

# Strip outer quotes, if applicable (unrecognized variables will be treated as strings).
if s and s[0] == s[-1] and (s[0] == "'" or s[0] == '"'):
return s[1:-1]
return s

def _render_jinja_vars(self, s: str) -> JsonType:
"""
Expand All @@ -273,16 +305,28 @@ def _render_jinja_vars(self, s: str) -> JsonType:
:returns: The original value, augmented with Jinja substitutions. Types are re-rendered to account for multiline
strings that may have been "normalized" prior to this call.
"""
# TODO: Consider tokenizing expressions over using regular expressions. The scope of this function has expanded
# drastically.

start_idx, sub_regex = self._set_on_schema_version()

# Search the string, replacing all substitutions we can recognize
for match in cast(list[str], sub_regex.findall(s)):
# The regex guarantees the string starts and ends with double braces
key = match[start_idx:-2].strip()
# Check for and interpret common JINJA functions
key, lower_match, upper_match, idx_match = RecipeReader._set_key_and_matches(key)

if key in self._vars_tbl:
key, lower_match, upper_match, idx_match, add_concat_match = RecipeReader._set_key_and_matches(key)

if add_concat_match:
lhs = self._eval_jinja_token(cast(str, add_concat_match.group(1)))
rhs = self._eval_jinja_token(cast(str, add_concat_match.group(2)))
# Perform arithmetic addition, IFF both sides are numeric types.
if isinstance(lhs, (int, float)) and isinstance(rhs, (int, float)):
s = str(lhs + rhs)
# Otherwise, concat two strings and quote them. This ensures YAML will interpret the type correctly.
else:
s = f'"{str(lhs) + str(rhs)}"'
elif key in self._vars_tbl:
# Replace value as a string. Re-interpret the entire value before returning.
value = str(self._vars_tbl[key])
if lower_match:
Expand Down
36 changes: 36 additions & 0 deletions tests/parser/test_recipe_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,24 @@ def test_contains_value(file: str, path: str, expected: bool) -> None:
"sha256": "6d3ac79e36c9ee593c5d4fb33a50cca0e3adceb6ef5cff8b8e5aef67b4c4aaf2",
},
),
# Add/concat cases
("sub_vars.yaml", "/requirements/run_constrained/0", True, 43),
("sub_vars.yaml", "/requirements/run_constrained/1", True, 43.3),
("sub_vars.yaml", "/requirements/run_constrained/2", True, "421"),
("sub_vars.yaml", "/requirements/run_constrained/3", True, "421.3"),
("sub_vars.yaml", "/requirements/run_constrained/4", True, 43),
("sub_vars.yaml", "/requirements/run_constrained/5", True, 43.3),
("sub_vars.yaml", "/requirements/run_constrained/6", True, "142"),
("sub_vars.yaml", "/requirements/run_constrained/7", True, "1.342"),
("sub_vars.yaml", "/requirements/run_constrained/8", True, "0.10.8.61.3"),
("sub_vars.yaml", "/requirements/run_constrained/9", True, "0.10.8.61.3"),
("sub_vars.yaml", "/requirements/run_constrained/10", True, "1.30.10.8.6"),
("sub_vars.yaml", "/requirements/run_constrained/11", True, "1.30.10.8.6"),
("sub_vars.yaml", "/requirements/run_constrained/12", True, 6),
("sub_vars.yaml", "/requirements/run_constrained/13", True, "42"),
("sub_vars.yaml", "/requirements/run_constrained/14", True, "dne42"),
# TODO fix this edge case
# ("sub_vars.yaml", "/requirements/run_constrained/15", True, "foo > 42"),
## v1_simple-recipe.yaml ##
("v1_format/v1_simple-recipe.yaml", "/build/number", False, 0),
("v1_format/v1_simple-recipe.yaml", "/build/number/", False, 0),
Expand Down Expand Up @@ -759,6 +777,24 @@ def test_contains_value(file: str, path: str, expected: bool) -> None:
"sha256": "6d3ac79e36c9ee593c5d4fb33a50cca0e3adceb6ef5cff8b8e5aef67b4c4aaf2",
},
),
# Add/concat cases
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/0", True, 43),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/1", True, 43.3),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/2", True, "421"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/3", True, "421.3"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/4", True, 43),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/5", True, 43.3),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/6", True, "142"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/7", True, "1.342"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/8", True, "0.10.8.61.3"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/9", True, "0.10.8.61.3"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/10", True, "1.30.10.8.6"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/11", True, "1.30.10.8.6"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/12", True, 6),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/13", True, "42"),
("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/14", True, "dne42"),
# TODO fix this edge case
# ("v1_format/v1_sub_vars.yaml", "/requirements/run_constraints/15", True, "foo > 42"),
## multi-output.yaml ##
("multi-output.yaml", "/outputs/0/build/run_exports/0", False, "bar"),
("multi-output.yaml", "/outputs/0/build/run_exports", False, ["bar"]),
Expand Down
19 changes: 19 additions & 0 deletions tests/test_aux_files/sub_vars.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% set name = "TYPES-toml" %}
{% set version = "0.10.8.6" %}
{% set number = 42 %}

package:
name: {{ name|lower }}
Expand All @@ -20,6 +21,24 @@ requirements:
- wheel
- pip
- python
run_constrained:
# Add/concat cases
- {{ number + 1 }}
- {{ number + 1.3 }}
- {{ number + "1" }}
- {{ number + '1.3' }}
- {{ 1 + number }}
- {{ 1.3 + number }}
- {{ "1" + number }}
- {{ '1.3' + number }}
- {{ version + 1.3 }}
- {{ version + '1.3' }}
- {{ 1.3 + version }}
- {{ '1.3' + version }}
- {{ 4 + 2 }}
- {{ '4' + "2" }}
- {{ dne + 42 }}
- foo > {{ '4' + "2" }}
run:
- python

Expand Down
19 changes: 19 additions & 0 deletions tests/test_aux_files/v1_format/v1_sub_vars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ schema_version: 1
context:
name: TYPES-toml
version: 0.10.8.6
number: 42

package:
name: ${{ name|lower }}
Expand All @@ -25,6 +26,24 @@ requirements:
- python
run:
- python
run_constraints:
# Add/concat cases
- ${{ number + 1 }}
- ${{ number + 1.3 }}
- ${{ number + "1" }}
- ${{ number + '1.3' }}
- ${{ 1 + number }}
- ${{ 1.3 + number }}
- ${{ "1" + number }}
- ${{ '1.3' + number }}
- ${{ version + 1.3 }}
- ${{ version + '1.3' }}
- ${{ 1.3 + version }}
- ${{ '1.3' + version }}
- ${{ 4 + 2 }}
- ${{ '4' + "2" }}
- ${{ dne + 42 }}
- foo > ${{ '4' + "2" }}

tests:
- python:
Expand Down
Loading