Skip to content

Commit bf2deed

Browse files
authored
fix: use python truthiness for compiled and/or Var operations (#6546)
* fix: use python truthiness for compiled `and`/`or` Var operations JS `&&`/`||` use JS truthiness, where `[]` and `{}` are truthy. Python treats them as falsy, so `Var([]) | true` was compiling to `[] || true` and returning `[]` instead of `true`. Same issue for `Var({}) & x`, `Var("") | y`, etc. Compile `a | b` to `pyOr(a, () => b)` and `a & b` to `pyAnd(a, () => b)`, backed by helpers that use `isTrue` for the truthiness check. RHS is wrapped in a thunk so short-circuit semantics are preserved. * docs: add JSDoc types to pyOr/pyAnd helpers
1 parent ad043c9 commit bf2deed

3 files changed

Lines changed: 38 additions & 5 deletions

File tree

packages/reflex-base/src/reflex_base/.templates/web/utils/state.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,28 @@ export const isNotNullOrUndefined = (val) => {
11421142
return (val ?? undefined) !== undefined;
11431143
};
11441144

1145+
/***
1146+
* Python-semantics OR: returns `a` if python-truthy, else evaluates and returns `b`.
1147+
* `b` is a thunk so it is only evaluated when needed, preserving short-circuit.
1148+
* @template A
1149+
* @template B
1150+
* @param {A} a The left-hand value.
1151+
* @param {() => B} b Thunk producing the right-hand value.
1152+
* @returns {A | B} `a` if python-truthy, otherwise the result of `b()`.
1153+
*/
1154+
export const pyOr = (a, b) => (isTrue(a) ? a : b());
1155+
1156+
/***
1157+
* Python-semantics AND: returns `a` if python-falsy, else evaluates and returns `b`.
1158+
* `b` is a thunk so it is only evaluated when needed, preserving short-circuit.
1159+
* @template A
1160+
* @template B
1161+
* @param {A} a The left-hand value.
1162+
* @param {() => B} b Thunk producing the right-hand value.
1163+
* @returns {A | B} `a` if python-falsy, otherwise the result of `b()`.
1164+
*/
1165+
export const pyAnd = (a, b) => (isTrue(a) ? b() : a);
1166+
11451167
/**
11461168
* Get the value from a ref.
11471169
* @param ref The ref to get the value from.

packages/reflex-base/src/reflex_base/vars/base.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,15 @@ def __hash__(self: DataclassInstance) -> int:
19551955
))
19561956

19571957

1958+
_PY_AND_IMPORT: ImportDict = {
1959+
f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="pyAnd")],
1960+
}
1961+
1962+
_PY_OR_IMPORT: ImportDict = {
1963+
f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="pyOr")],
1964+
}
1965+
1966+
19581967
def and_operation(
19591968
a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any
19601969
) -> Var[VAR_TYPE | OTHER_VAR_TYPE]:
@@ -1982,8 +1991,9 @@ def _and_operation(a: Var, b: Var):
19821991
The result of the logical AND operation.
19831992
"""
19841993
return var_operation_return(
1985-
js_expression=f"({a} && {b})",
1994+
js_expression=f"pyAnd({a}, () => ({b}))",
19861995
var_type=unionize(a._var_type, b._var_type),
1996+
var_data=VarData(imports=_PY_AND_IMPORT),
19871997
)
19881998

19891999

@@ -2014,8 +2024,9 @@ def _or_operation(a: Var, b: Var):
20142024
The result of the logical OR operation.
20152025
"""
20162026
return var_operation_return(
2017-
js_expression=f"({a} || {b})",
2027+
js_expression=f"pyOr({a}, () => ({b}))",
20182028
var_type=unionize(a._var_type, b._var_type),
2029+
var_data=VarData(imports=_PY_OR_IMPORT),
20192030
)
20202031

20212032

tests/units/test_var.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ def test_basic_operations(TestObj):
313313
assert str(LiteralNumberVar.create(1) // 2) == "Math.floor(1 / 2)"
314314
assert str(LiteralNumberVar.create(1) % 2) == "(1 % 2)"
315315
assert str(LiteralNumberVar.create(1) ** 2) == "(1 ** 2)"
316-
assert str(LiteralNumberVar.create(1) & v(2)) == "(1 && 2)"
317-
assert str(LiteralNumberVar.create(1) | v(2)) == "(1 || 2)"
316+
assert str(LiteralNumberVar.create(1) & v(2)) == "pyAnd(1, () => (2))"
317+
assert str(LiteralNumberVar.create(1) | v(2)) == "pyOr(1, () => (2))"
318318
assert str(LiteralArrayVar.create([1, 2, 3])[0]) == "[1, 2, 3]?.at?.(0)"
319319
assert (
320320
str(LiteralObjectVar.create({"a": 1, "b": 2})["a"])
@@ -1025,7 +1025,7 @@ def test_all_number_operations():
10251025

10261026
assert (
10271027
str(even_more_complicated_number)
1028-
== "!(isTrue((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))"
1028+
== "!(isTrue(pyOr(Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))), () => (pyAnd(2, () => (Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))))"
10291029
)
10301030

10311031
assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)"

0 commit comments

Comments
 (0)