jg-rp / liquid

A Python engine for the Liquid template language.
https://jg-rp.github.io/liquid/
MIT License
64 stars 9 forks source link

Guide to build random filter/tag #106

Closed hardiksondagar closed 1 year ago

hardiksondagar commented 1 year ago

We need python's random method exposed in liquid to assign a random value to a variable. Writing a simple custom filter solves the main thing, but in order to do that some variable is required.

Code

@liquid_filter
@with_context
def cast_random(value: str, *, context):
    return random.random()

Usage

{% assign rand_val = null | cast_random %}

Is there any better way to do this without the need for piping? I looked at the tags but am unsure if it's a better solution.

By the way thanks a lot for building this library, we are using it on a daily basis and helping us in a big way to deliver business values.

jg-rp commented 1 year ago

Hi @hardiksondagar,

You could define a random drop with properties that generate a different random number each time they are accessed. This is consistent with how the "standard" now and today objects are implemented (source).

import random

from typing import Iterator
from typing import Mapping
from typing import Union

from liquid import Environment

class RandomDrop(Mapping[str, Union[int, float]]):
    """Mapping-like object for generating random numbers."""

    def __contains__(self, item: object) -> bool:
        return item in ("random", "randint")

    def __getitem__(self, key: str) -> Union[int, float]:
        if key == "random":
            return random.random()
        if key == "randint":
            return random.randint(1, 10)
        raise KeyError(str(key))

    def __iter__(self) -> Iterator[str]:
        return iter(["random", "randint"])

    def __len__(self) -> int:
        return 2

    def __str__(self) -> str:
        return "RandomDrop"

env = Environment(globals={"random": RandomDrop()})

Usage would look like this...

{% assign rand_val = random.random -%}
{{ rand_val }}

{% for i in (1..random.randint) -%}
  {{ random.random }}
{% endfor -%}

Output

0.5856242442692104

0.7310637087401849
0.4737098762489945
0.9324768766566306
0.358920961408541
0.703978047935022

If the leading random. is undesirable, it is possible to add RandomDrop directly to a render context's scope, by subclassing liquid.Context, liquid.BoundTemplate and liquid.Environment.

https://github.com/jg-rp/liquid/blob/859bb8ffd07706d44df1e1e132c7c9cbf82737d6/liquid/context.py#L239-L241

Let me know if you'd like an example of that.

hardiksondagar commented 1 year ago

Thank you for the prompt reply @jg-rp. I would really appreciate an example to use random without random. prefix.

jg-rp commented 1 year ago
import random

from typing import Iterator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Union

from liquid import Context
from liquid import Environment
from liquid import BoundTemplate
from liquid.context import Namespace

class RandomDrop(Mapping[str, Union[int, float]]):
    """Mapping-like object for generating random numbers."""

    def __contains__(self, item: object) -> bool:
        return item in ("random", "randint")

    def __getitem__(self, key: str) -> Union[int, float]:
        if key == "random":
            return random.random()
        if key == "randint":
            return random.randint(1, 10)  # Arbitrary example
        raise KeyError(str(key))

    def __iter__(self) -> Iterator[str]:
        return iter(["random", "randint"])

    def __len__(self) -> int:
        return 2

    def __str__(self) -> str:
        return "RandomDrop"

class MyContext(Context):
    def __init__(
        self,
        env: Environment,
        globals: Optional[Namespace] = None,
        disabled_tags: Optional[List[str]] = None,
        copy_depth: int = 0,
        parent_context: Optional[Context] = None,
        loop_iteration_carry: int = 1,
        local_namespace_size_carry: int = 0,
        template: Optional[BoundTemplate] = None,
    ):
        super().__init__(
            env,
            globals,
            disabled_tags,
            copy_depth,
            parent_context,
            loop_iteration_carry,
            local_namespace_size_carry,
            template,
        )
        # Pushing RandomDrop to the front the the chain map means its
        # properties take priority over any template variables set using
        # `{% assign %}` and `{% capture %}`.
        self.scope.push(RandomDrop())

class MyBoundTemplate(BoundTemplate):
    context_class = MyContext

class MyEnvironment(Environment):
    template_class = MyBoundTemplate

env = MyEnvironment()

The previous example would then become...

{% assign rand_val = random -%}
{{ rand_val }}

{% for i in (1..randint) -%}
  {{ random }}
{% endfor -%}
hardiksondagar commented 1 year ago

Thanks a lot for this example. This example will allow me to add more such drop objects. Thanks again.