From 65312e61252683afa6aeb36401b2390940932fe0 Mon Sep 17 00:00:00 2001 From: ubaidrmn Date: Tue, 2 Jun 2026 17:55:58 +0500 Subject: [PATCH 1/3] Update `StringVar` to match Python `str` behavior (#5417) Add missing `lstrip` and `rstrip` methods to align with Python's `str` interface. Update `strip` method to accept a `chars` argument, consistent with `str.strip`. --- packages/reflex-base/news/5417.feature.md | 1 + .../src/reflex_base/vars/sequence.py | 86 ++++++++++++++++--- tests/units/test_var.py | 2 + 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 packages/reflex-base/news/5417.feature.md diff --git a/packages/reflex-base/news/5417.feature.md b/packages/reflex-base/news/5417.feature.md new file mode 100644 index 00000000000..522d7e46f6c --- /dev/null +++ b/packages/reflex-base/news/5417.feature.md @@ -0,0 +1 @@ +`StringVar` now includes `lstrip` and `rstrip` methods. The `strip` method now accepts an optional `chars` argument for consistency with Python’s str API. diff --git a/packages/reflex-base/src/reflex_base/vars/sequence.py b/packages/reflex-base/src/reflex_base/vars/sequence.py index a296f0052b8..df0ccf3f7bb 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -674,6 +674,20 @@ def lower(self) -> StringVar: """ return string_lower_operation(self) + def lstrip(self, chars: StringVar | str | None = None) -> StringVar: + """Left strip the string. + + Args: + chars: Characters to remove from the left side. If None, strip whitespace. + + Returns: + The string lstrip operation. + """ + if chars is not None and not isinstance(chars, (StringVar, str)): + raise_unsupported_operand_types("lstrip", (type(self), type(chars))) + + return string_lstrip_operation(self, chars) + def upper(self) -> StringVar: """Convert the string to uppercase. @@ -698,13 +712,19 @@ def capitalize(self) -> StringVar: """ return string_capitalize_operation(self) - def strip(self) -> StringVar: + def strip(self, chars: StringVar | str | None = None) -> StringVar: """Strip the string. + Args: + chars: Characters to remove from both ends. If None, strip whitespace. + Returns: The string strip operation. """ - return string_strip_operation(self) + if chars is not None and not isinstance(chars, (StringVar, str)): + raise_unsupported_operand_types("strip", (type(self), type(chars))) + + return string_strip_operation(self, chars) def reversed(self) -> StringVar: """Reverse the string. @@ -714,6 +734,20 @@ def reversed(self) -> StringVar: """ return self.split().reverse().join() + def rstrip(self, chars: StringVar | str | None = None) -> StringVar: + """Right strip the string. + + Args: + chars: Characters to remove from the right side. If None, strip whitespace. + + Returns: + The string rstrip operation. + """ + if chars is not None and not isinstance(chars, (StringVar, str)): + raise_unsupported_operand_types("rstrip", (type(self), type(chars))) + + return string_rstrip_operation(self, chars) + def contains( self, other: StringVar | str, field: StringVar | str | None = None ) -> BooleanVar: @@ -972,16 +1006,48 @@ def string_capitalize_operation(string: StringVar[Any]): @var_operation -def string_strip_operation(string: StringVar[Any]): - """Strip a string. +def string_strip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """Strip a string.""" + if str(chars) == "null": + return var_operation_return(js_expression=f"{string}.trim()", var_type=str) - Args: - string: The string to strip. + return var_operation_return( + js_expression=f"{string}.replace(/^[{chars}]+|[{chars}]+$/g, '')", + var_type=str, + ) - Returns: - The stripped string. - """ - return var_operation_return(js_expression=f"{string}.trim()", var_type=str) + +@var_operation +def string_lstrip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """Left strip a string.""" + if str(chars) == "null": + return var_operation_return(js_expression=f"{string}.trimStart()", var_type=str) + + return var_operation_return( + js_expression=f"{string}.replace(/^[{chars}]+/, '')", + var_type=str, + ) + + +@var_operation +def string_rstrip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """Right strip a string.""" + if str(chars) == "null": + return var_operation_return(js_expression=f"{string}.trimEnd()", var_type=str) + + return var_operation_return( + js_expression=f"{string}.replace(/[{chars}]+$/, '')", + var_type=str, + ) @var_operation diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 6971cdffcd4..5b69a39db66 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1001,8 +1001,10 @@ def test_string_operations(): assert str(basic_string.length()) == '"Hello, World!".split("").length' assert str(basic_string.lower()) == '"Hello, World!".toLowerCase()' + assert str(basic_string.lstrip()) == '"Hello, World!".trimStart()' assert str(basic_string.upper()) == '"Hello, World!".toUpperCase()' assert str(basic_string.strip()) == '"Hello, World!".trim()' + assert str(basic_string.rstrip()) == '"Hello, World!".trimEnd()' assert str(basic_string.contains("World")) == '"Hello, World!".includes("World")' assert ( str(basic_string.split(" ").join(",")) == '"Hello, World!".split(" ").join(",")' From 67ad5eafc206ba6ff9cf79d53659beefe4a4a7c3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Jun 2026 14:48:25 -0700 Subject: [PATCH 2/3] Use JS helper functions for strip/lstrip/rstrip instead of regex literals Constructing JS regex character classes from Python values was broken: quoted literal serialization leaked into the class, var expressions were embedded verbatim into static regex syntax, and regex-special characters (], \, ^, -) were not escaped. Replace with pyStrip/pyLstrip/pyRstrip helpers in state.js that match Python str semantics (code-point aware, null chars strips whitespace), and detect the no-args case with isinstance(chars, NoneVar) rather than comparing against the string "null". Add end-to-end integration tests covering whitespace and chars-set stripping, including regex-special characters and state var chars. Co-Authored-By: Claude Fable 5 --- .../reflex_base/.templates/web/utils/state.js | 50 +++++++++++++++ .../src/reflex_base/vars/sequence.py | 62 ++++++++++++++++--- tests/integration/test_var_operations.py | 30 +++++++++ tests/units/test_var.py | 5 ++ 4 files changed, 137 insertions(+), 10 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index 93fa01adbd4..7323a5f4ab6 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -1172,6 +1172,56 @@ export const pyOr = (a, b) => (isTrue(a) ? a : b()); */ export const pyAnd = (a, b) => (isTrue(a) ? b() : a); +/*** + * Python-semantics str.lstrip: remove leading characters in the given set. + * @param {string} s The string to strip. + * @param {string?} chars Characters to remove; null/undefined strips whitespace. + * @returns {string} The stripped string. + */ +export const pyLstrip = (s, chars) => { + if (chars == null) return s.trimStart(); + const charSet = new Set(chars); + let start = 0; + while (start < s.length) { + const cp = String.fromCodePoint(s.codePointAt(start)); + if (!charSet.has(cp)) break; + start += cp.length; + } + return s.slice(start); +}; + +/*** + * Python-semantics str.rstrip: remove trailing characters in the given set. + * @param {string} s The string to strip. + * @param {string?} chars Characters to remove; null/undefined strips whitespace. + * @returns {string} The stripped string. + */ +export const pyRstrip = (s, chars) => { + if (chars == null) return s.trimEnd(); + const charSet = new Set(chars); + let end = s.length; + while (end > 0) { + // step back over a full code point (surrogate pairs are 2 units wide) + let cp = s[end - 1]; + if (end > 1) { + const pair = String.fromCodePoint(s.codePointAt(end - 2)); + if (pair.length === 2) cp = pair; + } + if (!charSet.has(cp)) break; + end -= cp.length; + } + return s.slice(0, end); +}; + +/*** + * Python-semantics str.strip: remove leading and trailing characters in the given set. + * @param {string} s The string to strip. + * @param {string?} chars Characters to remove; null/undefined strips whitespace. + * @returns {string} The stripped string. + */ +export const pyStrip = (s, chars) => + chars == null ? s.trim() : pyRstrip(pyLstrip(s, chars), chars); + /** * Get the value from a ref. * @param ref The ref to get the value from. diff --git a/packages/reflex-base/src/reflex_base/vars/sequence.py b/packages/reflex-base/src/reflex_base/vars/sequence.py index df0ccf3f7bb..fc069fa6553 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -14,15 +14,17 @@ from typing_extensions import TypeVar as TypingExtensionsTypeVar from reflex_base import constants -from reflex_base.constants.base import REFLEX_VAR_OPENING_TAG +from reflex_base.constants.base import REFLEX_VAR_OPENING_TAG, Dirs from reflex_base.utils import types from reflex_base.utils.exceptions import VarTypeError +from reflex_base.utils.imports import ImportDict, ImportVar from reflex_base.utils.types import GenericType, get_origin from .base import ( CachedVarOperation, CustomVarOperationReturn, LiteralVar, + NoneVar, Var, VarData, _global_vars, @@ -1005,18 +1007,40 @@ def string_capitalize_operation(string: StringVar[Any]): ) +_PY_STRIP_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="pyStrip")], +} + +_PY_LSTRIP_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="pyLstrip")], +} + +_PY_RSTRIP_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="pyRstrip")], +} + + @var_operation def string_strip_operation( string: StringVar[Any], chars: StringVar[Any] | str | None = None, ): - """Strip a string.""" - if str(chars) == "null": + """Strip whitespace or the given characters from both ends of a string. + + Args: + string: The string to strip. + chars: The set of characters to remove. If None, strip whitespace. + + Returns: + The stripped string. + """ + if isinstance(chars, NoneVar): return var_operation_return(js_expression=f"{string}.trim()", var_type=str) return var_operation_return( - js_expression=f"{string}.replace(/^[{chars}]+|[{chars}]+$/g, '')", + js_expression=f"pyStrip({string}, {chars})", var_type=str, + var_data=VarData(imports=_PY_STRIP_IMPORT), ) @@ -1025,13 +1049,22 @@ def string_lstrip_operation( string: StringVar[Any], chars: StringVar[Any] | str | None = None, ): - """Left strip a string.""" - if str(chars) == "null": + """Strip whitespace or the given characters from the start of a string. + + Args: + string: The string to strip. + chars: The set of characters to remove. If None, strip whitespace. + + Returns: + The stripped string. + """ + if isinstance(chars, NoneVar): return var_operation_return(js_expression=f"{string}.trimStart()", var_type=str) return var_operation_return( - js_expression=f"{string}.replace(/^[{chars}]+/, '')", + js_expression=f"pyLstrip({string}, {chars})", var_type=str, + var_data=VarData(imports=_PY_LSTRIP_IMPORT), ) @@ -1040,13 +1073,22 @@ def string_rstrip_operation( string: StringVar[Any], chars: StringVar[Any] | str | None = None, ): - """Right strip a string.""" - if str(chars) == "null": + """Strip whitespace or the given characters from the end of a string. + + Args: + string: The string to strip. + chars: The set of characters to remove. If None, strip whitespace. + + Returns: + The stripped string. + """ + if isinstance(chars, NoneVar): return var_operation_return(js_expression=f"{string}.trimEnd()", var_type=str) return var_operation_return( - js_expression=f"{string}.replace(/[{chars}]+$/, '')", + js_expression=f"pyRstrip({string}, {chars})", var_type=str, + var_data=VarData(imports=_PY_RSTRIP_IMPORT), ) diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index 5b5949188ef..f943dfb5eb9 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -47,6 +47,9 @@ class VarOperationState(rx.State): str_var2: rx.Field[str] = rx.field("second") str_var3: rx.Field[str] = rx.field("ThIrD") str_var4: rx.Field[str] = rx.field("a long string") + str_var5: rx.Field[str] = rx.field(" spaced ") + str_var6: rx.Field[str] = rx.field("-^[a]^-stripped-^[a]^-") + strip_chars: rx.Field[str] = rx.field("-^[]a") dict1: rx.Field[dict[int, int]] = rx.field({1: 2}) dict2: rx.Field[dict[int, int]] = rx.field({3: 4}) html_str: rx.Field[str] = rx.field("
hello
") @@ -565,6 +568,24 @@ def index(): rx.text(VarOperationState.str_var3.lower(), id="str_lower"), rx.text(VarOperationState.str_var3.upper(), id="str_upper"), rx.text(VarOperationState.str_var4.split(" ").to_string(), id="str_split"), + rx.text(VarOperationState.str_var5.strip().to_string(), id="str_strip"), + rx.text(VarOperationState.str_var5.lstrip().to_string(), id="str_lstrip"), + rx.text(VarOperationState.str_var5.rstrip().to_string(), id="str_rstrip"), + rx.text(VarOperationState.str_var6.strip("-^[]a"), id="str_strip_chars"), + rx.text(VarOperationState.str_var6.lstrip("-^[]a"), id="str_lstrip_chars"), + rx.text(VarOperationState.str_var6.rstrip("-^[]a"), id="str_rstrip_chars"), + rx.text( + VarOperationState.str_var6.strip(VarOperationState.strip_chars), + id="str_strip_chars_var", + ), + rx.text( + VarOperationState.str_var6.lstrip(VarOperationState.strip_chars), + id="str_lstrip_chars_var", + ), + rx.text( + VarOperationState.str_var6.rstrip(VarOperationState.strip_chars), + id="str_rstrip_chars_var", + ), rx.text(VarOperationState.list3.join(""), id="list_join"), rx.text(VarOperationState.list3.join(","), id="list_join_comma"), # Index from an op var @@ -938,6 +959,15 @@ def test_var_operations(driver, var_operations: AppHarness): ("str_lower", "third"), ("str_upper", "THIRD"), ("str_split", '["a","long","string"]'), + ("str_strip", '"spaced"'), + ("str_lstrip", '"spaced "'), + ("str_rstrip", '" spaced"'), + ("str_strip_chars", "stripped"), + ("str_lstrip_chars", "stripped-^[a]^-"), + ("str_rstrip_chars", "-^[a]^-stripped"), + ("str_strip_chars_var", "stripped"), + ("str_lstrip_chars_var", "stripped-^[a]^-"), + ("str_rstrip_chars_var", "-^[a]^-stripped"), # str, int ("str_mult_int", "firstfirstfirstfirstfirst"), ("str_and_int", "5"), diff --git a/tests/units/test_var.py b/tests/units/test_var.py index f1eca47fa61..dc13608f3fb 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1008,6 +1008,11 @@ def test_string_operations(): assert str(basic_string.upper()) == '"Hello, World!".toUpperCase()' assert str(basic_string.strip()) == '"Hello, World!".trim()' assert str(basic_string.rstrip()) == '"Hello, World!".trimEnd()' + assert str(basic_string.lstrip("!H")) == 'pyLstrip("Hello, World!", "!H")' + assert str(basic_string.strip("!H")) == 'pyStrip("Hello, World!", "!H")' + assert str(basic_string.rstrip("!H")) == 'pyRstrip("Hello, World!", "!H")' + chars_var = Var(_js_expr="state.chars").to(str) + assert str(basic_string.strip(chars_var)) == 'pyStrip("Hello, World!", state.chars)' assert str(basic_string.contains("World")) == '"Hello, World!".includes("World")' assert ( str(basic_string.split(" ").join(",")) == '"Hello, World!".split(" ").join(",")' From e223f21db3cb04a41c3f2997951a470b0bf6bc8e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Jun 2026 15:15:01 -0700 Subject: [PATCH 3/3] Drop the NoneVar fast path; helpers handle null chars pyStrip/pyLstrip/pyRstrip already fall back to trim/trimStart/trimEnd when chars is null, so always emit the helper call. Co-Authored-By: Claude Fable 5 --- packages/reflex-base/src/reflex_base/vars/sequence.py | 10 ---------- tests/units/test_var.py | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/sequence.py b/packages/reflex-base/src/reflex_base/vars/sequence.py index fc069fa6553..b1f990ed3f1 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -24,7 +24,6 @@ CachedVarOperation, CustomVarOperationReturn, LiteralVar, - NoneVar, Var, VarData, _global_vars, @@ -1034,9 +1033,6 @@ def string_strip_operation( Returns: The stripped string. """ - if isinstance(chars, NoneVar): - return var_operation_return(js_expression=f"{string}.trim()", var_type=str) - return var_operation_return( js_expression=f"pyStrip({string}, {chars})", var_type=str, @@ -1058,9 +1054,6 @@ def string_lstrip_operation( Returns: The stripped string. """ - if isinstance(chars, NoneVar): - return var_operation_return(js_expression=f"{string}.trimStart()", var_type=str) - return var_operation_return( js_expression=f"pyLstrip({string}, {chars})", var_type=str, @@ -1082,9 +1075,6 @@ def string_rstrip_operation( Returns: The stripped string. """ - if isinstance(chars, NoneVar): - return var_operation_return(js_expression=f"{string}.trimEnd()", var_type=str) - return var_operation_return( js_expression=f"pyRstrip({string}, {chars})", var_type=str, diff --git a/tests/units/test_var.py b/tests/units/test_var.py index dc13608f3fb..be097011729 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1004,10 +1004,10 @@ def test_string_operations(): assert str(basic_string.length()) == '"Hello, World!".split("").length' assert str(basic_string.lower()) == '"Hello, World!".toLowerCase()' - assert str(basic_string.lstrip()) == '"Hello, World!".trimStart()' + assert str(basic_string.lstrip()) == 'pyLstrip("Hello, World!", null)' assert str(basic_string.upper()) == '"Hello, World!".toUpperCase()' - assert str(basic_string.strip()) == '"Hello, World!".trim()' - assert str(basic_string.rstrip()) == '"Hello, World!".trimEnd()' + assert str(basic_string.strip()) == 'pyStrip("Hello, World!", null)' + assert str(basic_string.rstrip()) == 'pyRstrip("Hello, World!", null)' assert str(basic_string.lstrip("!H")) == 'pyLstrip("Hello, World!", "!H")' assert str(basic_string.strip("!H")) == 'pyStrip("Hello, World!", "!H")' assert str(basic_string.rstrip("!H")) == 'pyRstrip("Hello, World!", "!H")'