mirror of
https://github.com/django-components/django-components.git
synced 2025-08-10 01:08:00 +00:00
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:
parent
36b8fcfbe6
commit
d6ec62c6be
4 changed files with 637 additions and 108 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue