jinja_lowcost/jinja_lowcost/main.py

181 lines
6.5 KiB
Python

from collections.abc import Callable
from string import Formatter
from typing import Any, Iterable
from enum import Enum
import re
import shlex
class Instruction(Enum):
REPEAT = "repeat"
JOIN = "join"
IF = "if"
LOWER = "lower"
UPPER = "upper"
STRIP = "strip"
CALL = "call"
NOP = "nop"
class ConditionalOp(Enum):
NOT = "not"
EQUAL = "equal"
NOTEQUAL = "notequal"
EVEN = "even"
ODD = "odd"
CONTAINS = "contains"
INSTRUCTION_PATTERN = re.compile("""(?!['|"].*):(?!.*['|"])""")
class JinjaLowCost(Formatter):
@staticmethod
def parse_instruction(raw_instruction: str) -> tuple[Instruction, list[Any], str | None]:
# Separar la instrucción con sus parámetros y la template
binding = iter(re.split(INSTRUCTION_PATTERN, raw_instruction, maxsplit=1))
pre_instruction = next(binding, None)
# Sí no tiene ninguna instrucción, se retorna NOP
if not pre_instruction:
return Instruction.NOP, [], ""
template = next(binding, None)
# Separar la instrucción de sus parámetros respetando las comillas
instruction, *params = shlex.split(pre_instruction)
try:
# Retornar todas las partes sí la instrucción está en el Enum
return Instruction(instruction), params, template
except ValueError:
# Si no, retornar NOP
return Instruction.NOP, [], ""
def format_field(self, value: Any, format_spec: str) -> Any:
instruction, params, template = self.parse_instruction(format_spec)
match instruction:
case Instruction.REPEAT:
if isinstance(value, dict):
elements = value.items()
elif isinstance(value, int):
elements = range(value)
elif isinstance(value, Iterable):
elements = value
else:
raise Exception(f"'{value}' is not iterable")
if template is None:
raise Exception("A template is needed")
name = next(iter(params), "item")
processed_elements = []
for i, item in enumerate(elements):
processed_element = self.format(template, **{name: item, "#": i})
processed_elements.append(processed_element)
return "".join(processed_elements)
case Instruction.IF:
if not params:
return template if value else ""
iter_params = iter(params)
try:
conditional = ConditionalOp(next(iter_params))
except ValueError:
raise Exception(f"'{params[0]}' is not a valid if conditional")
criteria = next(iter_params, None)
match conditional:
case ConditionalOp.EQUAL:
if criteria is None:
raise Exception("Equal operator requires a parameter")
func = lambda: value == type(value)(criteria)
case ConditionalOp.NOTEQUAL:
if criteria is None:
raise Exception("NotEqual operator requires a parameter")
func = lambda: value != type(value)(criteria)
case ConditionalOp.CONTAINS:
if criteria is None:
raise Exception("Contains operator requires a parameter")
func = lambda: value and type(value[0])(criteria) in value
case ConditionalOp.EVEN:
func = lambda: value % 2 == 0
case ConditionalOp.ODD:
func = lambda: value % 2 != 0
case ConditionalOp.NOT:
func = lambda: not value
return template if func() else ""
case Instruction.JOIN:
if not isinstance(value, Iterable):
raise Exception(f"{value} is not iterable")
criteria = next(iter(params), "")
return criteria.join(map(str, value))
case Instruction.UPPER:
return str(value).upper()
case Instruction.LOWER:
return str(value).lower()
case Instruction.STRIP:
param = next(iter(params), None)
return str(value).strip(param)
case Instruction.CALL:
if not isinstance(value, Callable):
raise Exception(f"{value} is not callable")
return str(value(*params))
case Instruction.NOP:
try:
return super().format_field(value, format_spec)
except:
raise Exception(f"Error trying to format '{format_spec}' instruction")
f = JinjaLowCost()
res = f.format("{x:join ', '}", x=[1, 2, 3, 4])
print(res)
res = f.format("{x:strip '-'}", x="----prueba----")
print(res)
# res = f.format("{a=}", a=10)
# print(res)
res = f.format("{x:repeat n:->{{n}}<-\n}", x=[10, 20, 30])
print(res)
res = f.format("{x:if contains {y}:estoy dentro}", x=[10, 20, 30], y=10)
print(res)
res = f.format("{x:if contains 42:42 is real}", x=range(10, 50))
print(res)
res = f.format("{x:join ', '}", x=[10, 20, 30])
print(res)
res = f.format("{x:upper}", x="Kirbylife")
print(res)
res = f.format("{x:lower}", x="TESTtestTEST")
print(res)
res = f.format("{x.get:call key_a}", x={"key_a": 10, "key_b": 20})
print(res)
res = f.format("{names:repeat name:{{#:if even:+ {{name}}\n}}{{#:if odd:- {{name:upper}}\n}}}", names=["Pastor", "Suadero", "Tripa"])
print(res)
template = "{numbers:repeat num:{{num}} }"
print(f.format(template, numbers=[10, 20, 30]))
res = f.format("{tacos: repeat taco:{{#}}. {{taco:upper}}\n}", tacos=["suadero", "pasTor", "tripa"])
print(res)
res = f.format(
"{nums:repeat num:{{num: repeat n:{{{{n:02}}}} }}\n}",
nums=[range(3), range(3,6), range(6, 9), range(9, 12)]
)
print(res)
res = f.format("{n:repeat:{{#:repeat:* }}\n}", n=7)
print(res)
template = """
<ul>{cities:repeat city:
<li style="background-color: {{#:if even:gray}}{{#:if odd:lightgray}}">{{city}}</li>}
</ul>
"""
print(f.format(template, cities=["New York", "New Delhi", "Tokio", "Monterrey"]))
res = f.format("{name.title:call}", name="JiNjA LoW cOsT")
print(res)
res = f.format("{exchange.get:call {currency}} {currency}", exchange={"USD": 1, "MXN": 19.7}, currency="MXN")
print(res)