feat: add spread operator (#596)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-08-24 15:01:18 +02:00 committed by GitHub
parent 36b8fcfbe6
commit d6ec62c6be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 637 additions and 108 deletions

View file

@ -1,11 +1,36 @@
import re
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
from django.template import Context, TemplateSyntaxError
from django.template.base import FilterExpression, Parser
Expression = Union[FilterExpression]
RuntimeKwargsInput = Dict[str, Expression]
RuntimeKwargPairsInput = List[Tuple[str, Expression]]
RuntimeKwargsInput = Dict[str, Union[Expression, "Operator"]]
RuntimeKwargPairsInput = List[Tuple[str, Union[Expression, "Operator"]]]
class Operator(ABC):
"""
Operator describes something that somehow changes the inputs
to template tags (the `{% %}`).
For example, a SpreadOperator inserts one or more kwargs at the
specified location.
"""
@abstractmethod
def resolve(self, context: Context) -> Any: ... # noqa E704
class SpreadOperator(Operator):
"""Operator that inserts one or more kwargs at the specified location."""
def __init__(self, expr: Expression) -> None:
self.expr = expr
def resolve(self, context: Context) -> Dict[str, Any]:
return self.expr.resolve(context)
class RuntimeKwargs:
@ -24,7 +49,12 @@ class RuntimeKwargPairs:
def resolve(self, context: Context) -> List[Tuple[str, Any]]:
resolved_kwarg_pairs: List[Tuple[str, Any]] = []
for key, kwarg in self.kwarg_pairs:
resolved_kwarg_pairs.append((key, kwarg.resolve(context)))
if isinstance(kwarg, SpreadOperator):
spread_kwargs = kwarg.resolve(context)
for spread_key, spread_value in spread_kwargs.items():
resolved_kwarg_pairs.append((spread_key, spread_value))
else:
resolved_kwarg_pairs.append((key, kwarg.resolve(context)))
return resolved_kwarg_pairs
@ -43,12 +73,19 @@ def safe_resolve_list(context: Context, args: List[Expression]) -> List:
def safe_resolve_dict(
context: Context,
kwargs: Dict[str, Expression],
kwargs: Dict[str, Union[Expression, "Operator"]],
) -> Dict[str, Any]:
result = {}
for key, kwarg in kwargs.items():
result[key] = kwarg.resolve(context)
# If we've come across a Spread Operator (...), we insert the kwargs from it here
if isinstance(kwarg, SpreadOperator):
spread_dict = kwarg.resolve(context)
if spread_dict is not None:
for spreadkey, spreadkwarg in spread_dict.items():
result[spreadkey] = spreadkwarg
else:
result[key] = kwarg.resolve(context)
return result
@ -72,6 +109,31 @@ def is_aggregate_key(key: str) -> bool:
return ":" in key and not key.startswith(":")
def is_spread_operator(value: Any) -> bool:
if not isinstance(value, str) or not value:
return False
return value.startswith("...")
# A string that starts with `...1=`, `...29=`, etc.
# We convert the spread syntax to this, so Django parses
# it as a kwarg, so it remains in the original position.
#
# So from `...dict`, we make `...1=dict`
#
# That way it's trivial to merge the kwargs after the spread
# operator is replaced with actual values.
INTERNAL_SPREAD_OPERATOR_RE = re.compile(r"^\.\.\.\d+=")
def is_internal_spread_operator(value: Any) -> bool:
if not isinstance(value, str) or not value:
return False
return bool(INTERNAL_SPREAD_OPERATOR_RE.match(value))
def process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
"""
This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs

View file

@ -12,11 +12,16 @@ from django_components.component_registry import ComponentRegistry
from django_components.component_registry import registry as component_registry
from django_components.expression import (
Expression,
Operator,
RuntimeKwargPairs,
RuntimeKwargPairsInput,
RuntimeKwargs,
RuntimeKwargsInput,
SpreadOperator,
is_aggregate_key,
is_internal_spread_operator,
is_kwarg,
is_spread_operator,
resolve_string,
)
from django_components.logger import trace_msg
@ -389,6 +394,7 @@ def _parse_tag(
else:
seen_kwargs.add(key)
spread_count = 0
for bit in bits:
value = bit
bit_is_kwarg = is_kwarg(bit)
@ -411,6 +417,21 @@ def _parse_tag(
parsed_flags[value] = True
continue
# Extract spread operator (...dict)
elif is_spread_operator(value):
if value == "...":
raise TemplateSyntaxError("Syntax operator is missing a value")
# Replace the leading `...` with `...=`, so the parser
# interprets it as a kwargs, and keeps it in the correct
# position.
# Since there can be multiple spread operators, we suffix
# them with an index, e.g. `...0=`
internal_spread_bit = f"...{spread_count}={value[3:]}"
bits_without_flags.append(internal_spread_bit)
spread_count += 1
continue
bits_without_flags.append(bit)
bits = bits_without_flags
@ -450,13 +471,25 @@ def _parse_tag(
params = [param for param in params_to_sort if param not in optional_params]
# Parse args/kwargs that will be passed to the fill
args, kwarg_pairs = parse_bits(
args, raw_kwarg_pairs = parse_bits(
parser=parser,
bits=bits,
params=[] if isinstance(params, bool) else params,
name=tag_name,
)
# Post-process args/kwargs - Mark special cases like aggregate dicts
# or dynamic expressions
kwarg_pairs: RuntimeKwargPairsInput = []
for key, val in raw_kwarg_pairs:
is_spread_op = is_internal_spread_operator(key + "=")
if is_spread_op:
expr = parser.compile_filter(val.token)
kwarg_pairs.append((key, SpreadOperator(expr)))
else:
kwarg_pairs.append((key, val))
# Allow only as many positional args as given
if params != True and len(args) > len(params): # noqa F712
raise TemplateSyntaxError(f"Tag '{tag_name}' received unexpected positional arguments: {args[len(params):]}")
@ -471,6 +504,11 @@ def _parse_tag(
kwargs: RuntimeKwargsInput = {}
extra_keywords: Set[str] = set()
for key, val in kwarg_pairs:
# Operators are resolved at render-time, so skip them
if isinstance(val, Operator):
kwargs[key] = val
continue
# Check if key allowed
if not keywordonly_kwargs:
is_key_allowed = False