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/.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 a296f0052b8..b1f990ed3f1 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -14,9 +14,10 @@ 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 ( @@ -674,6 +675,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 +713,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 +735,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: @@ -971,17 +1006,80 @@ 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]): - """Strip a string. +def string_strip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """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. """ - 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, + var_data=VarData(imports=_PY_STRIP_IMPORT), + ) + + +@var_operation +def string_lstrip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """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. + """ + return var_operation_return( + js_expression=f"pyLstrip({string}, {chars})", + var_type=str, + var_data=VarData(imports=_PY_LSTRIP_IMPORT), + ) + + +@var_operation +def string_rstrip_operation( + string: StringVar[Any], + chars: StringVar[Any] | str | None = None, +): + """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. + """ + return var_operation_return( + js_expression=f"pyRstrip({string}, {chars})", + var_type=str, + var_data=VarData(imports=_PY_RSTRIP_IMPORT), + ) @var_operation 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("