From 9ac12aaf1c088a4751ddca8075ca12e0302029b7 Mon Sep 17 00:00:00 2001 From: pwwang <1188067+pwwang@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:24:36 -0700 Subject: [PATCH] 0.7.3 (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹 Make `default` filter work with `None`; Make `attr` filter work with dicts * 🔖 0.7.3 * 📝 Update changelog --- docs/changelog.md | 6 ++++++ docs/standard.md | 1 - docs/wild.md | 2 ++ liquid/__init__.py | 2 +- liquid/exts/standard.py | 2 +- liquid/filters/manager.py | 4 ++-- liquid/filters/standard.py | 19 ++++++++++++++++--- liquid/liquid.py | 4 ++-- liquid/tags/standard.py | 8 ++++---- liquid/tags/wild.py | 5 +++-- pyproject.toml | 2 +- tests/standard/test_filters.py | 14 ++++++++++++++ 12 files changed, 52 insertions(+), 17 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index d874fdb..c1eb731 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,9 @@ +# 0.7.3 + +- 🩹 Make `default` filter work with `None` +- 🩹 Make `attr` filter work with dicts +- 🩹 Use filter `liquid_map`, in wild mode, instead of `map`, which is overridden by python's builtin `map` + # 0.7.2 - 🐛 Fix `date` filter issues (#38, #40) diff --git a/docs/standard.md b/docs/standard.md index 95a9137..c01dd34 100644 --- a/docs/standard.md +++ b/docs/standard.md @@ -8,7 +8,6 @@ You may checkout the documentation for standard liquid: It always returns a `float` rather than an `integer` when `ndigits=0` - ## Logical operators The logical operators `and`/`or` collapse from left to right (it's right to left in `liquid`) diff --git a/docs/wild.md b/docs/wild.md index beab24f..4f88aa7 100644 --- a/docs/wild.md +++ b/docs/wild.md @@ -21,6 +21,8 @@ Below are some features it supports. - See: https://jinja.palletsprojects.com/en/3.0.x/templates/?highlight=builtin%20filters#builtin-filters - `ifelse`: - See: https://pwwang.github.io/liquidpy/api/liquid.filters.wild/ +- `map()` + - It is overridden by python's `builtins.map()`. To use the one from `liquid`, try `liquid_map()` ## Tests diff --git a/liquid/__init__.py b/liquid/__init__.py index b5382b9..a19c29b 100644 --- a/liquid/__init__.py +++ b/liquid/__init__.py @@ -4,4 +4,4 @@ patch_jinja() -__version__ = "0.7.2" +__version__ = "0.7.3" diff --git a/liquid/exts/standard.py b/liquid/exts/standard.py index caad42c..3f760ca 100644 --- a/liquid/exts/standard.py +++ b/liquid/exts/standard.py @@ -92,7 +92,7 @@ def filter_stream(self, stream: "TokenStream") -> Generator: yield Token(token.lineno, TOKEN_COMMA, None) yield tokens_ahead[3] yield Token(token.lineno, TOKEN_ADD, None) - yield Token(token.lineno, TOKEN_INTEGER, 1) + yield Token(token.lineno, TOKEN_INTEGER, 1) # type: ignore yield Token(token.lineno, TOKEN_RPAREN, None) else: diff --git a/liquid/filters/manager.py b/liquid/filters/manager.py index e75f6e6..fe4f63b 100644 --- a/liquid/filters/manager.py +++ b/liquid/filters/manager.py @@ -1,5 +1,5 @@ """Provides filter manager""" -from typing import TYPE_CHECKING, Callable, Dict, Union +from typing import TYPE_CHECKING, Callable, Dict, Sequence, Union if TYPE_CHECKING: from jinja2 import Environment @@ -19,7 +19,7 @@ def __init__(self) -> None: self.filters: Dict[str, Callable] = {} def register( - self, name_or_filter: Union[str, Callable] = None + self, name_or_filter: Union[str, Sequence[str], Callable] = None ) -> Callable: """Register a filter diff --git a/liquid/filters/standard.py b/liquid/filters/standard.py index 2b3899b..2258232 100644 --- a/liquid/filters/standard.py +++ b/liquid/filters/standard.py @@ -86,12 +86,17 @@ def __bool__(self): return False -def _get_prop(obj, prop): +def _get_prop(obj, prop, _raise=False): """Get the property of the object, allow via getitem""" try: return obj[prop] except (TypeError, KeyError): - return getattr(obj, prop) + try: + return getattr(obj, prop) + except AttributeError: + if _raise: # pragma: no cover + raise + return None # Jinja comes with thses filters @@ -196,6 +201,8 @@ def default(base, deft, allow_false=False): Otherwise, return base""" if allow_false and base is False: return False + if base is None: + return deft return FILTERS["default"](base, deft, isinstance(base, str)) @@ -347,12 +354,18 @@ def where(base, prop, value): return ret or EmptyDrop() -@standard_filter_manager.register("map") +@standard_filter_manager.register(["liquid_map", "map"]) def liquid_map(base, prop): """Map a property to a list of objects""" return [_get_prop(bas, prop) for bas in base] +@standard_filter_manager.register +def attr(base, prop): + """Similar as `__getattr__()` but also works like `__getitem__()""" + return _get_prop(base, prop) + + # @standard_filter_manager.register # def join(base, sep): # """Join a list by the sep""" diff --git a/liquid/liquid.py b/liquid/liquid.py index 16138b3..3e4fa6e 100644 --- a/liquid/liquid.py +++ b/liquid/liquid.py @@ -93,7 +93,7 @@ def __init__( ext_conf[key] = val loader = env_args.pop("loader", None) - fsloader = FileSystemLoader(search_paths) + fsloader = FileSystemLoader(search_paths) # type: ignore if loader: loader = ChoiceLoader([loader, fsloader]) else: @@ -190,7 +190,7 @@ def __init__( # in case template is a PathLike self.template = env.get_template(str(template)) else: - self.template = env.from_string(template) + self.template = env.from_string(str(template)) def render(self, *args, **kwargs) -> Any: """Render the template. diff --git a/liquid/tags/standard.py b/liquid/tags/standard.py index 315f701..00079cf 100644 --- a/liquid/tags/standard.py +++ b/liquid/tags/standard.py @@ -33,11 +33,11 @@ def comment(token: "Token", parser: "Parser") -> nodes.Node: """ if parser.stream.current.type is TOKEN_BLOCK_END: # no args provided, ignore whatever - parser.parse_statements(["name:endcomment"], drop_needle=True) + parser.parse_statements(("name:endcomment", ), drop_needle=True) return nodes.Output([], lineno=token.lineno) args = parser.parse_expression() - body = parser.parse_statements(["name:endcomment"], drop_needle=True) + body = parser.parse_statements(("name:endcomment", ), drop_needle=True) body = decode_raw(body[0].nodes[0].data) body_parts = body.split("\n", 1) if not body_parts[0]: @@ -194,7 +194,7 @@ def tablerow( Returns: The parsed node """ - target = parser.parse_assign_target(extra_end_rules=("name:in")) + target = parser.parse_assign_target(extra_end_rules=("name:in", )) parser.stream.expect("name:in") iter_ = parser.parse_tuple( with_condexpr=False, @@ -222,7 +222,7 @@ def tablerow( "load", ) else: - inner_iter = iter_ + inner_iter: nodes.Getitem = iter_ inner_body = [ nodes.Output( diff --git a/liquid/tags/wild.py b/liquid/tags/wild.py index 3f10942..f51bd76 100644 --- a/liquid/tags/wild.py +++ b/liquid/tags/wild.py @@ -2,7 +2,7 @@ import textwrap from contextlib import redirect_stdout from io import StringIO -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Union from jinja2 import nodes from jinja2.exceptions import TemplateSyntaxError @@ -151,6 +151,7 @@ def addfilter( token = parser.stream.expect("name") filtername = token.value + pass_env: Union[bool, Token] if parser.stream.current.type is TOKEN_BLOCK_END: # no pass_environment pass_env = False @@ -176,7 +177,7 @@ def addfilter( ) from None if pass_env: - filterfunc = pass_environment(filterfunc) + filterfunc = pass_environment(filterfunc) # type: ignore env.filters[filtername] = filterfunc return nodes.Output([], lineno=token.lineno) diff --git a/pyproject.toml b/pyproject.toml index 7065b18..7e827a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "liquidpy" -version = "0.7.2" +version = "0.7.3" description = "A port of liquid template engine for python" authors = [ "pwwang ",] license = "MIT" diff --git a/tests/standard/test_filters.py b/tests/standard/test_filters.py index 1950c71..4868a84 100644 --- a/tests/standard/test_filters.py +++ b/tests/standard/test_filters.py @@ -3,6 +3,7 @@ """ import pytest from datetime import datetime +from collections import namedtuple from liquid import Liquid @@ -394,3 +395,16 @@ def test_basic_typecasting(set_default_standard): assert Liquid('{{ float("1") | plus: 1 }}').render() == "2.0" assert Liquid('{{ str(1) | append: "1" }}').render() == "11" assert Liquid('{{ bool(1) }}').render() == "True" + +def test_attr(set_default_standard): + assert Liquid('{{x | attr: "y"}}').render(x = {}) == "None" + assert Liquid('{{x | attr: "y" | default: 1}}').render(x = {}) == "1" + assert Liquid('{{x | attr: "y"}}').render(x = {"y": 1}) == "1" + assert Liquid('{{x | attr: "y"}}').render(x=namedtuple("X", "y")(2)) == "2" + +def test_liquid_map(set_default_standard): + assert Liquid('{{x | liquid_map: "y" | first}}').render(x=[{}]) == "None" + assert Liquid('{{x | liquid_map: "y" | first}}').render(x=[{"y": 1}]) == "1" + assert Liquid('{{x | liquid_map: "y" | last}}').render( + x=[namedtuple("X", "y")(2)] + ) == "2"