Skip to content

Commit

Permalink
fix: numbers are not falsy, see #74
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Sep 9, 2022
1 parent d1673fe commit d5c1c55
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 36 deletions.
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Python Liquid Change Log
========================

Version 1.4.5
-------------

**Hot fix**

- Fixed a bug where boolean expressions and the default filter would treat ``0.0`` and
``decimal.Decimal("0")`` as ``False``. Python considers these values to be falsy,
Liquid does not. See `#74 <https://github.com/jg-rp/liquid/issues/74>`_.

Version 1.4.4
-------------

Expand Down
2 changes: 1 addition & 1 deletion liquid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# flake8: noqa
# pylint: disable=useless-import-alias,missing-module-docstring

__version__ = "1.4.4"
__version__ = "1.4.5"

try:
from markupsafe import escape as escape
Expand Down
5 changes: 3 additions & 2 deletions liquid/builtin/filters/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import datetime
import decimal
import functools

from typing import Any
Expand Down Expand Up @@ -49,8 +50,8 @@ def default(obj: Any, default_: object = "", *, allow_false: bool = False) -> An
if hasattr(obj, "__liquid__"):
_obj = obj.__liquid__()

# Liquid zero is not falsy.
if isinstance(_obj, int) and not isinstance(_obj, bool):
# Liquid 0, 0.0, 0b0, 0X0, 0o0 and Decimal("0") are not falsy.
if not isinstance(obj, bool) and isinstance(obj, (int, float, decimal.Decimal)):
return obj

if allow_false is True and _obj is False:
Expand Down
29 changes: 9 additions & 20 deletions liquid/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from abc import abstractmethod

from collections import abc
from decimal import Decimal
from itertools import islice

from typing import Dict
Expand Down Expand Up @@ -902,35 +903,23 @@ def eval_number_expression(left: Number, operator: str, right: Number) -> bool:
raise LiquidTypeError(f"unknown operator {left} {operator} {right}")


def _is_py_falsy_number(obj: object) -> bool:
# Liquid 0, 0.0, 0b0, 0X0, 0o0 and Decimal("0") are not falsy.
return not isinstance(obj, bool) and isinstance(obj, (int, float, Decimal))


def is_truthy(obj: Any) -> bool:
"""Return True if the given object is Liquid truthy."""
if hasattr(obj, "__liquid__"):
obj = obj.__liquid__()

# Liquid zero is not falsy.
if isinstance(obj, int) and not isinstance(obj, bool):
return True

if obj in (False, None):
return False
return True
return _is_py_falsy_number(obj) or obj not in (False, None)


# pylint: disable=too-many-return-statements
def compare_bool(left: Any, operator: str, right: Any) -> bool:
"""Compare an object to a boolean value."""
if isinstance(left, bool) and (
isinstance(right, int) and not isinstance(right, bool)
):
if operator in ("==", "<", ">", "<=", ">="):
return False
if operator in ("!=", "<>"):
return True
raise LiquidTypeError(
f"unknown operator: {type(left)} {operator} {type(right)}"
)
if isinstance(right, bool) and (
isinstance(left, int) and not isinstance(left, bool)
if (isinstance(left, bool) and _is_py_falsy_number(right)) or (
isinstance(right, bool) and _is_py_falsy_number(left)
):
if operator in ("==", "<", ">", "<=", ">="):
return False
Expand Down
5 changes: 5 additions & 0 deletions liquid/golden/default_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,9 @@
template=r'{{ 0 | default: "bar", allow_false: true }}',
expect="0",
),
Case(
description="0.0 is not falsy",
template=r'{{ 0.0 | default: "bar" }}',
expect="0.0",
),
]
5 changes: 5 additions & 0 deletions liquid/golden/if_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@
template=(r"{% if 0 %}Hello{% else %}Goodbye{% endif %}"),
expect="Hello",
),
Case(
description=("0.0 is truthy"),
template=(r"{% if 0.0 %}Hello{% else %}Goodbye{% endif %}"),
expect="Hello",
),
Case(
description=("one is not equal to true"),
template=(r"{% if 1 == true %}Hello{% else %}Goodbye{% endif %}"),
Expand Down
15 changes: 15 additions & 0 deletions tests/filters/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test miscellaneous filter functions."""
# pylint: disable=too-many-public-methods,too-many-lines,missing-class-docstring
import datetime
import decimal
import platform
import unittest

Expand Down Expand Up @@ -277,6 +278,20 @@ def test_default(self):
kwargs={},
expect=0,
),
Case(
description="0.0 is not false",
val=0.0,
args=["bar"],
kwargs={},
expect=0.0,
),
Case(
description="Decimal('0') is not false",
val=decimal.Decimal("0"),
args=["bar"],
kwargs={},
expect=decimal.Decimal("0"),
),
Case(
description="one is not false or true",
val=1,
Expand Down
95 changes: 82 additions & 13 deletions tests/test_evaluate_expression.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
"""Liquid expression evaluator test cases."""

import unittest
from typing import NamedTuple, Any, Mapping

from decimal import Decimal

from typing import Any
from typing import Mapping
from typing import NamedTuple

from liquid.environment import Environment
from liquid.context import Context
from liquid.stream import TokenStream
from liquid.lex import (
tokenize_filtered_expression,
tokenize_boolean_expression,
tokenize_loop_expression,
tokenize_assignment_expression,
)
from liquid.parse import (
parse_filtered_expression,
parse_boolean_expression,
parse_assignment_expression,
parse_loop_expression,
)

from liquid.lex import tokenize_filtered_expression
from liquid.lex import tokenize_boolean_expression
from liquid.lex import tokenize_loop_expression
from liquid.lex import tokenize_assignment_expression

from liquid.parse import parse_filtered_expression
from liquid.parse import parse_boolean_expression
from liquid.parse import parse_assignment_expression
from liquid.parse import parse_loop_expression


class Case(NamedTuple):
Expand Down Expand Up @@ -495,6 +498,12 @@ def test_eval_boolean_expression(self):
expression="0",
expect=True,
),
Case(
description="0.0",
context={},
expression="0.0",
expect=True,
),
Case(
description="one",
context={},
Expand All @@ -507,6 +516,24 @@ def test_eval_boolean_expression(self):
expression="0 == false",
expect=False,
),
Case(
description="zero equals true",
context={},
expression="0 == true",
expect=False,
),
Case(
description="0.0 equals false",
context={},
expression="0.0 == false",
expect=False,
),
Case(
description="0.0 equals true",
context={},
expression="0.0 == true",
expect=False,
),
Case(
description="one equals true",
context={},
Expand All @@ -531,6 +558,12 @@ def test_eval_boolean_expression(self):
expression="0 < true",
expect=False,
),
Case(
description="0.0 is less than true",
context={},
expression="0.0 < true",
expect=False,
),
Case(
description="one is not equal true",
context={},
Expand All @@ -543,18 +576,54 @@ def test_eval_boolean_expression(self):
expression="0 != false",
expect=True,
),
Case(
description="0.0 is not equal false",
context={},
expression="0.0 != false",
expect=True,
),
Case(
description="false is not equal zero",
context={},
expression="false != 0",
expect=True,
),
Case(
description="false is not equal 0.0",
context={},
expression="false != 0.0",
expect=True,
),
Case(
description="false is less than string",
context={},
expression="false < 'false'",
expect=False,
),
Case(
description="decimal zero",
context={"n": Decimal("0")},
expression="n",
expect=True,
),
Case(
description="decimal non-zero",
context={"n": Decimal("1")},
expression="n",
expect=True,
),
Case(
description="decimal zero equals false",
context={"n": Decimal("0")},
expression="n == false",
expect=False,
),
Case(
description="decimal zero equals true",
context={"n": Decimal("0")},
expression="n == true",
expect=False,
),
]

self._test(test_cases, tokenize_boolean_expression, parse_boolean_expression)
Expand Down

0 comments on commit d5c1c55

Please sign in to comment.