From fe051b943aa6fdc187565911c32551c1e7f739cf Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 3 Jul 2024 23:24:53 +0300 Subject: [PATCH 01/10] implement format for immutable vars --- reflex/experimental/vars/base.py | 19 ++++++++++++++++- reflex/vars.py | 36 ++++++++++++++++++++++++-------- reflex/vars.pyi | 3 +++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 52bce916148..f8d021ec23d 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -6,9 +6,10 @@ import sys from typing import Any, Optional, Type +from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.utils import serializers, types from reflex.utils.exceptions import VarTypeError -from reflex.vars import Var, VarData, _extract_var_data +from reflex.vars import Var, VarData, _extract_var_data, _global_vars @dataclasses.dataclass( @@ -28,6 +29,10 @@ class ImmutableVar(Var): # Extra metadata associated with the Var _var_data: Optional[VarData] = dataclasses.field(default=None) + def __post_init__(self): + """Post-initialization.""" + _global_vars[hash(self)] = self + @property def _var_is_local(self) -> bool: """Whether this is a local javascript variable. @@ -156,3 +161,15 @@ def create( _var_type=type_, _var_data=_var_data, ) + + def __format__(self, format_spec: str) -> str: + """Format the var into a Javascript equivalent to an f-string. + + Args: + format_spec: The format specifier (Ignored for now). + + Returns: + The formatted var. + """ + # Encode the _var_data into the formatted output for tracking purposes. + return f"{REFLEX_VAR_OPENING_TAG}{hash(self)}{REFLEX_VAR_CLOSING_TAG}{self._var_name}" diff --git a/reflex/vars.py b/reflex/vars.py index afe3e60c336..ad7c223a930 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -262,6 +262,9 @@ def _encode_var(value: Var) -> str: ) _decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) +# Defined global immutable vars. +_global_vars: Dict[int, Var] = {} + def _decode_var(value: str) -> tuple[VarData | None, str]: """Decode the state name from a formatted var. @@ -294,17 +297,32 @@ def json_loads(s): start, end = m.span() value = value[:start] + value[end:] - # Read the JSON, pull out the string length, parse the rest as VarData. - data = json_loads(m.group(1)) - string_length = data.pop("string_length", None) - var_data = VarData.parse_obj(data) + serialized_data = m.group(1) + + if serialized_data[1:].isnumeric(): + # This is a global immutable var. + var = _global_vars[int(serialized_data)] + var_data = var._var_data + + if var_data is not None: + realstart = start + offset + var_data.interpolations = [ + (realstart, realstart + len(var._var_name)) + ] + + var_datas.append(var_data) + else: + # Read the JSON, pull out the string length, parse the rest as VarData. + data = json_loads(serialized_data) + string_length = data.pop("string_length", None) + var_data = VarData.parse_obj(data) - # Use string length to compute positions of interpolations. - if string_length is not None: - realstart = start + offset - var_data.interpolations = [(realstart, realstart + string_length)] + # Use string length to compute positions of interpolations. + if string_length is not None: + realstart = start + offset + var_data.interpolations = [(realstart, realstart + string_length)] - var_datas.append(var_data) + var_datas.append(var_data) offset += end - start return VarData.merge(*var_datas) if var_datas else None, value diff --git a/reflex/vars.pyi b/reflex/vars.pyi index 9c9a7315af8..a85c249ccdc 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -35,6 +35,9 @@ USED_VARIABLES: Incomplete def get_unique_variable_name() -> str: ... def _encode_var(value: Var) -> str: ... + +_global_vars: Dict[int, Var] = {} + def _decode_var(value: str) -> tuple[VarData, str]: ... def _extract_var_data(value: Iterable) -> list[VarData | None]: ... From a05e965c19dc766f7108ecd54f59b98872551567 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:12:29 +0300 Subject: [PATCH 02/10] add some basic test --- tests/test_var.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_var.py b/tests/test_var.py index e58daaa035e..550977bf330 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -6,11 +6,14 @@ from pandas import DataFrame from reflex.base import Base +from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG +from reflex.experimental.vars.base import ImmutableVar from reflex.state import BaseState from reflex.vars import ( BaseVar, ComputedVar, Var, + VarData, computed_var, ) @@ -835,6 +838,54 @@ def test_state_with_initial_computed_var( assert runtime_dict[var_name] == expected_runtime +def test_retrival(): + var_with_data = ImmutableVar.create("test")._replace( + merge_var_data=VarData( + state="Test", + imports={ + "/utils/context": [ + { + "tag": "StateContexts", + "is_default": False, + "alias": None, + "install": True, + "render": True, + "transpile": False, + } + ], + "react": [ + { + "tag": "useContext", + "is_default": False, + "alias": None, + "install": True, + "render": True, + "transpile": False, + } + ], + }, + hooks={"const state = useContext(StateContexts.state)": None}, + ) + ) + + f_string = f"foo{var_with_data}bar" + + assert REFLEX_VAR_OPENING_TAG in f_string + assert REFLEX_VAR_CLOSING_TAG in f_string + assert ( + Var.create(f"foo{var_with_data}bar")._var_data.state + == var_with_data._var_data.state + ) + assert ( + Var.create(f"foo{var_with_data}bar")._var_data.imports + == var_with_data._var_data.imports + ) + assert ( + Var.create(f"foo{var_with_data}bar")._var_data.hooks + == var_with_data._var_data.hooks + ) + + @pytest.mark.parametrize( "out, expected", [ From 17b3b0fe34ee1e92e88598d47065b85254f5b08c Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:17:15 +0300 Subject: [PATCH 03/10] make reference only after formatting --- reflex/experimental/vars/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index f8d021ec23d..d89ff97171f 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -29,10 +29,6 @@ class ImmutableVar(Var): # Extra metadata associated with the Var _var_data: Optional[VarData] = dataclasses.field(default=None) - def __post_init__(self): - """Post-initialization.""" - _global_vars[hash(self)] = self - @property def _var_is_local(self) -> bool: """Whether this is a local javascript variable. @@ -171,5 +167,7 @@ def __format__(self, format_spec: str) -> str: Returns: The formatted var. """ + _global_vars[hash(self)] = self + # Encode the _var_data into the formatted output for tracking purposes. return f"{REFLEX_VAR_OPENING_TAG}{hash(self)}{REFLEX_VAR_CLOSING_TAG}{self._var_name}" From 52018fd17996f8c1b06569b757238bd6ec230efe Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:21:07 +0300 Subject: [PATCH 04/10] win over pyright --- tests/test_var.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_var.py b/tests/test_var.py index 550977bf330..2b08b573f30 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -872,18 +872,12 @@ def test_retrival(): assert REFLEX_VAR_OPENING_TAG in f_string assert REFLEX_VAR_CLOSING_TAG in f_string - assert ( - Var.create(f"foo{var_with_data}bar")._var_data.state - == var_with_data._var_data.state - ) - assert ( - Var.create(f"foo{var_with_data}bar")._var_data.imports - == var_with_data._var_data.imports - ) - assert ( - Var.create(f"foo{var_with_data}bar")._var_data.hooks - == var_with_data._var_data.hooks - ) + + result_var_data = Var.create(f"foo{var_with_data}bar")._var_data + assert result_var_data is not None + assert result_var_data.state == var_with_data._var_data.state + assert result_var_data._var_data.imports == var_with_data._var_data.imports + assert result_var_data._var_data.hooks == var_with_data._var_data.hooks @pytest.mark.parametrize( From 23a16570090075288cf3b04abbca3f4b6f59f468 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:33:54 +0300 Subject: [PATCH 05/10] hopefully now pyright doesn't hate me --- tests/test_var.py | 63 +++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/tests/test_var.py b/tests/test_var.py index 2b08b573f30..b5724f60132 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -839,35 +839,38 @@ def test_state_with_initial_computed_var( def test_retrival(): - var_with_data = ImmutableVar.create("test")._replace( - merge_var_data=VarData( - state="Test", - imports={ - "/utils/context": [ - { - "tag": "StateContexts", - "is_default": False, - "alias": None, - "install": True, - "render": True, - "transpile": False, - } - ], - "react": [ - { - "tag": "useContext", - "is_default": False, - "alias": None, - "install": True, - "render": True, - "transpile": False, - } - ], - }, - hooks={"const state = useContext(StateContexts.state)": None}, - ) + var_without_data = ImmutableVar.create("test") + assert var_without_data is not None + + original_var_data = VarData( + state="Test", + imports={ + "/utils/context": [ + { + "tag": "StateContexts", + "is_default": False, + "alias": None, + "install": True, + "render": True, + "transpile": False, + } + ], + "react": [ + { + "tag": "useContext", + "is_default": False, + "alias": None, + "install": True, + "render": True, + "transpile": False, + } + ], + }, + hooks={"const state = useContext(StateContexts.state)": None}, ) + var_with_data = var_without_data._replace(merge_var_data=original_var_data) + f_string = f"foo{var_with_data}bar" assert REFLEX_VAR_OPENING_TAG in f_string @@ -875,9 +878,9 @@ def test_retrival(): result_var_data = Var.create(f"foo{var_with_data}bar")._var_data assert result_var_data is not None - assert result_var_data.state == var_with_data._var_data.state - assert result_var_data._var_data.imports == var_with_data._var_data.imports - assert result_var_data._var_data.hooks == var_with_data._var_data.hooks + assert result_var_data.state == original_var_data.state + assert result_var_data._var_data.imports == original_var_data.imports + assert result_var_data._var_data.hooks == original_var_data.hooks @pytest.mark.parametrize( From cdb0a4fcddfd0682e8e4c5b0acc399c191f58e8c Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:37:09 +0300 Subject: [PATCH 06/10] forgot some _var_data --- tests/test_var.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_var.py b/tests/test_var.py index b5724f60132..c5547eebe82 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -876,11 +876,11 @@ def test_retrival(): assert REFLEX_VAR_OPENING_TAG in f_string assert REFLEX_VAR_CLOSING_TAG in f_string - result_var_data = Var.create(f"foo{var_with_data}bar")._var_data + result_var_data = Var.create_safe(f"foo{var_with_data}bar")._var_data assert result_var_data is not None assert result_var_data.state == original_var_data.state - assert result_var_data._var_data.imports == original_var_data.imports - assert result_var_data._var_data.hooks == original_var_data.hooks + assert result_var_data.imports == original_var_data.imports + assert result_var_data.hooks == original_var_data.hooks @pytest.mark.parametrize( From c5bb07b44b9ac744e700c20d2ac6eb103f99977b Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 4 Jul 2024 01:40:53 +0300 Subject: [PATCH 07/10] i don't know how imports work --- tests/test_var.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/test_var.py b/tests/test_var.py index c5547eebe82..80b99c25d3b 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -9,6 +9,7 @@ from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.experimental.vars.base import ImmutableVar from reflex.state import BaseState +from reflex.utils.imports import ImportVar from reflex.vars import ( BaseVar, ComputedVar, @@ -844,28 +845,7 @@ def test_retrival(): original_var_data = VarData( state="Test", - imports={ - "/utils/context": [ - { - "tag": "StateContexts", - "is_default": False, - "alias": None, - "install": True, - "render": True, - "transpile": False, - } - ], - "react": [ - { - "tag": "useContext", - "is_default": False, - "alias": None, - "install": True, - "render": True, - "transpile": False, - } - ], - }, + imports={"react": [ImportVar(tag="useRef")]}, hooks={"const state = useContext(StateContexts.state)": None}, ) From 5d2fd972b5af20985ffb0d2cdcdaab584acce1c9 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 9 Jul 2024 16:52:02 -0400 Subject: [PATCH 08/10] use f_string var and remove assignments from pyi file --- reflex/vars.pyi | 2 +- tests/test_var.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/vars.pyi b/reflex/vars.pyi index a85c249ccdc..129d7888d4d 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -36,7 +36,7 @@ USED_VARIABLES: Incomplete def get_unique_variable_name() -> str: ... def _encode_var(value: Var) -> str: ... -_global_vars: Dict[int, Var] = {} +_global_vars: Dict[int, Var] def _decode_var(value: str) -> tuple[VarData, str]: ... def _extract_var_data(value: Iterable) -> list[VarData | None]: ... diff --git a/tests/test_var.py b/tests/test_var.py index 80b99c25d3b..b466a846031 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -856,7 +856,7 @@ def test_retrival(): assert REFLEX_VAR_OPENING_TAG in f_string assert REFLEX_VAR_CLOSING_TAG in f_string - result_var_data = Var.create_safe(f"foo{var_with_data}bar")._var_data + result_var_data = Var.create_safe(f_string)._var_data assert result_var_data is not None assert result_var_data.state == original_var_data.state assert result_var_data.imports == original_var_data.imports From c084ac6f717d32bf0ff6b5b527124457d12f07ca Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 10 Jul 2024 16:43:25 -0400 Subject: [PATCH 09/10] override post_init to not break immutability --- reflex/experimental/vars/base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index d89ff97171f..f5847567ece 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -9,7 +9,7 @@ from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.utils import serializers, types from reflex.utils.exceptions import VarTypeError -from reflex.vars import Var, VarData, _extract_var_data, _global_vars +from reflex.vars import Var, VarData, _decode_var, _extract_var_data, _global_vars @dataclasses.dataclass( @@ -56,6 +56,15 @@ def _var_full_name_needs_state_prefix(self) -> bool: """ return False + def __post_init__(self): + """Post-initialize the var.""" + # Decode any inline Var markup and apply it to the instance + _var_data, _var_name = _decode_var(self._var_name) + if _var_data: + self.__init__( + _var_name, self._var_type, VarData.merge(self._var_data, _var_data) + ) + def _replace(self, merge_var_data=None, **kwargs: Any): """Make a copy of this Var with updated fields. From c398d4de078237f7e225a692a330dda464d9147b Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 10 Jul 2024 18:17:12 -0400 Subject: [PATCH 10/10] add create_safe and test for it --- reflex/experimental/vars/base.py | 28 ++++++++++++++++++++++++++++ tests/test_var.py | 21 +++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index f5847567ece..3dc1f0d7c55 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -167,6 +167,34 @@ def create( _var_data=_var_data, ) + @classmethod + def create_safe( + cls, + value: Any, + _var_is_local: bool | None = None, + _var_is_string: bool | None = None, + _var_data: VarData | None = None, + ) -> Var: + """Create a var from a value, asserting that it is not None. + + Args: + value: The value to create the var from. + _var_is_local: Whether the var is local. Deprecated. + _var_is_string: Whether the var is a string literal. Deprecated. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + var = cls.create( + value, + _var_is_local=_var_is_local, + _var_is_string=_var_is_string, + _var_data=_var_data, + ) + assert var is not None + return var + def __format__(self, format_spec: str) -> str: """Format the var into a Javascript equivalent to an f-string. diff --git a/tests/test_var.py b/tests/test_var.py index b466a846031..aa0946cb4ca 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -857,10 +857,23 @@ def test_retrival(): assert REFLEX_VAR_CLOSING_TAG in f_string result_var_data = Var.create_safe(f_string)._var_data - assert result_var_data is not None - assert result_var_data.state == original_var_data.state - assert result_var_data.imports == original_var_data.imports - assert result_var_data.hooks == original_var_data.hooks + result_immutable_var_data = ImmutableVar.create_safe(f_string)._var_data + assert result_var_data is not None and result_immutable_var_data is not None + assert ( + result_var_data.state + == result_immutable_var_data.state + == original_var_data.state + ) + assert ( + result_var_data.imports + == result_immutable_var_data.imports + == original_var_data.imports + ) + assert ( + result_var_data.hooks + == result_immutable_var_data.hooks + == original_var_data.hooks + ) @pytest.mark.parametrize(