diff --git a/docs/source/content/examples/sign_up_flow.rst b/docs/source/content/examples/sign_up_flow.rst index 961e5ad9..37f9f4e2 100644 --- a/docs/source/content/examples/sign_up_flow.rst +++ b/docs/source/content/examples/sign_up_flow.rst @@ -56,8 +56,8 @@ Let's start from the ``START`` screen. This screen welcomes the user and allows text="Click here to sign up", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="SIGN_UP", ), payload={ @@ -73,8 +73,8 @@ Let's start from the ``START`` screen. This screen welcomes the user and allows text="Click here to login", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="LOGIN", ), payload={ @@ -132,8 +132,8 @@ The ``SIGN_UP`` screen allows the user to sign up (create an account). Let's tak text="Already have an account?", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="LOGIN", ), payload={ @@ -283,8 +283,8 @@ Ok, now to the ``LOGIN`` screen. This screen allows the user to login to their e text="Don't have an account?", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="SIGN_UP", ), payload={ @@ -451,8 +451,8 @@ Here is all the flow code in one place: Footer, Layout, Action, - ActionNext, - ActionNextType, + Next, + NextType, FlowActionType, ComponentRef, InputType, @@ -485,8 +485,8 @@ Here is all the flow code in one place: text="Click here to sign up", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="SIGN_UP", ), payload={ @@ -502,8 +502,8 @@ Here is all the flow code in one place: text="Click here to login", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="LOGIN", ), payload={ @@ -534,8 +534,8 @@ Here is all the flow code in one place: text="Already have an account?", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="LOGIN", ), payload={ @@ -622,8 +622,8 @@ Here is all the flow code in one place: text="Don't have an account?", on_click_action=Action( name=FlowActionType.NAVIGATE, - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="SIGN_UP", ), payload={ diff --git a/docs/source/content/flows/flow_json.rst b/docs/source/content/flows/flow_json.rst index 08c3ebb2..4fb33d4e 100644 --- a/docs/source/content/flows/flow_json.rst +++ b/docs/source/content/flows/flow_json.rst @@ -10,7 +10,9 @@ Here you will find all the components that make up a Flow JSON object. .. autoclass:: Screen() .. autoclass:: ScreenData() - :members: ref, ref_in + :members: ref, ref_in, update + +.. autoclass:: ScreenDataUpdate() .. autoclass:: Layout() @@ -86,9 +88,9 @@ Here you will find all the components that make up a Flow JSON object. .. autoclass:: OpenUrlAction() -.. autoclass:: ActionNext() +.. autoclass:: Next() -.. autoclass:: ActionNextType() +.. autoclass:: NextType() .. autoclass:: ScreenDataRef() diff --git a/pywa/types/flows.py b/pywa/types/flows.py index bef67497..f3d54846 100644 --- a/pywa/types/flows.py +++ b/pywa/types/flows.py @@ -9,7 +9,7 @@ import logging import pathlib import warnings -from typing import Iterable, TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar +from typing import Iterable, TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, TypeAlias import httpx @@ -49,6 +49,7 @@ "FlowJSON", "Screen", "ScreenData", + "ScreenDataUpdate", "Layout", "LayoutType", "Form", @@ -89,8 +90,10 @@ "OpenUrlAction", "FlowActionType", "FlowRequestActionType", - "ActionNext", - "ActionNextType", + "Next", + "NextType", + "ActionNext", # Deprecated + "ActionNextType", # Deprecated ] @@ -916,6 +919,10 @@ def default(self, o): if isinstance(o, _ScreenDatasContainer): data = {} for item in o: + if isinstance(item, ScreenDataUpdate): + data[item.key] = item.new_value + continue + try: data[item.key] = dict( **self._get_json_type(item.example), __example__=item.example @@ -1071,6 +1078,18 @@ def to_dict(self): ) +_ScreenDataValType: TypeAlias = ( + str + | int + | float + | bool + | dict + | datetime.date + | DataSource + | Iterable[str | int | float | bool | dict | datetime.date | DataSource] +) + + class ScreenData: """ Represents a screen data that a screen should get from the previous screen or from the data endpoint. @@ -1091,10 +1110,6 @@ class ScreenData: ... TextInput(required=is_email_required.ref, input_type=InputType.EMAIL, ...) ... ])]) ... ) - - Attributes: - key: The key of the data (To use later in the screen children with ``.ref`` or with :class:`ScreenDataRef`). - example: The example of the data that the screen should get from the previous screen or from the data endpoint (or the previous screen). """ __slots__ = "key", "example" @@ -1103,16 +1118,15 @@ def __init__( self, *, key: str, - example: ( - str - | int - | float - | bool - | dict - | DataSource - | Iterable[str | int | float | bool | dict | DataSource] - ), + example: _ScreenDataValType, ) -> None: + """ + Initialize the screen data. + + Args: + key: The key of the data (if you using ``:=`` to assign this object, the convention is to use the same key as the variable name). + example: The example of the data that the screen should get from the previous screen or from the data endpoint. + """ self.key = key self.example = example @@ -1172,14 +1186,94 @@ def ref_in(self, screen: Screen | str) -> ScreenDataRef: """ return ScreenDataRef(key=self.key, screen=screen) + def update(self, new_value: _ScreenDataValType) -> ScreenDataUpdate: + """ + Update the value of this data. Use this inside the :class:`UpdateDataAction` ``.payload``. + + Example:: + + >>> is_visible = ScreenData(key='is_visible', example=True) + >>> UpdateDataAction(payload=[is_visible.update(False)]) + + Args: + new_value: The new value of the data. + + Returns: + The update action for this data. + """ + return ScreenDataUpdate(key=self.key, new_value=new_value) + def __repr__(self) -> str: return f"{self.__class__.__name__}(key={self.key!r}, example={self.example!r})" +class ScreenDataUpdate: + """ + Represents an update action for a screen data. + + - Use the :meth:`ScreenData.update` method to create an instance of this class from a :class:`ScreenData`. + + .. code-block:: python + :emphasize-lines: 4, 14-19, 23 + :linenos: + + Screen( + id="DEMO_SCREEN", + data=[ + is_txt_visible := ScreenData( + key="is_txt_visible", + example=False, + ), + ], + layout=Layout( + children=[ + OptIn( + label="Show the text", + name="show_txt", + on_select_action=UpdateDataAction( + payload=[is_txt_visible.update(True)] # a much cleaner way + ), + on_unselect_action=UpdateDataAction( + payload=[ScreenDataUpdate(key="is_txt_visible", new_value=False)] # only when you don't have access to the `ScreenData` object + ), + ), + TextBody( + text="You clicked the button!", + visible=is_txt_visible.ref, + ), + ] + ) + ) + """ + + __slots__ = "key", "new_value" + + def __init__( + self, + *, + key: str, + new_value: _ScreenDataValType, + ) -> None: + """ + Initialize the screen data update + + Args: + key: The key of the data to update (The same key as the :class:`ScreenData` object). + new_value: The new value of the data. + """ + self.key = key + self.new_value = new_value + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(key={self.key!r}, new_value={self.new_value!r})" + ) + + class _ScreenDatasContainer: - """A wrapper for the ``Screen.data`` iterable. This is to prevent ``dataclasses.asdict()`` from converting ScreenData objects.""" + """A wrapper to prevent ``dataclasses.asdict()`` from converting ScreenData|Update objects.""" - def __init__(self, datas: Iterable[ScreenData]): + def __init__(self, datas: Iterable[ScreenData | ScreenDataUpdate]): # not mixed self._datas = datas def __iter__(self): @@ -1244,7 +1338,7 @@ def __post_init__(self): def __truediv__(self, ref: _RefT) -> _RefT: """A shortcut to reference screen data / form components in this screen.""" - return ref.__class__(ref.field, screen=self.id) + return ref.__class__(ref._field, screen=self.id) class LayoutType(utils.StrEnum): @@ -1320,32 +1414,17 @@ class ComponentType(utils.StrEnum): SWITCH = "Switch" -class Ref: - """Base class for all references""" +class _Expr: + """Base for refs, conditions, and expressions""" - def __init__(self, prefix: str, field: str, screen: Screen | str | None = None): - self.prefix = prefix - self.field = field - self.screen = ( - f"screen.{screen.id if isinstance(screen, Screen) else screen}." - if screen - else "" - ) - - def to_str(self) -> str: - return "${%s%s.%s}" % ( - self.screen, - self.prefix, - self.field, - ) + def to_str(self) -> str: ... def __str__(self) -> str: - """Allowing to use in string concatenation. Added in v6.0.""" return self.to_str() @staticmethod - def _format_value(val: Ref | bool | int | float | str) -> str: - if isinstance(val, (Ref, MathExpression)): + def _format_value(val: _Expr | bool | int | float | str) -> str: + if isinstance(val, _Expr): return val.to_str() elif isinstance(val, str): return f"'{val}'" @@ -1353,66 +1432,123 @@ def _format_value(val: Ref | bool | int | float | str) -> str: return str(val).lower() return str(val) - def __eq__(self, other: Ref | bool | int | float | str) -> Condition: - return Condition(f"({self.to_str()} == {self._format_value(other)})") - - def __ne__(self, other: Ref | bool | int | float | str) -> Condition: - return Condition(f"({self.to_str()} != {self._format_value(other)})") - - def __gt__(self, other: Ref | int | float) -> Condition: - return Condition(f"({self.to_str()} > {self._format_value(other)})") - - def __ge__(self, other: Ref | int | float) -> Condition: - return Condition(f"({self.to_str()} >= {self._format_value(other)})") - - def __lt__(self, other: Ref | int | float) -> Condition: - return Condition(f"({self.to_str()} < {self._format_value(other)})") - def __le__(self, other: Ref | int | float) -> Condition: - return Condition(f"({self.to_str()} <= {self._format_value(other)})") +class _Math(_Expr): + """Base for math expressions""" - def __add__(self, other: Ref | int | float) -> MathExpression: + def __add__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self.to_str()} + {self._format_value(other)})") - def __radd__(self, other: Ref | int | float) -> MathExpression: + def __radd__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self._format_value(other)} + {self.to_str()})") - def __sub__(self, other: Ref | int | float) -> MathExpression: + def __sub__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self.to_str()} - {self._format_value(other)})") - def __rsub__(self, other: Ref | int | float) -> MathExpression: + def __rsub__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self._format_value(other)} - {self.to_str()})") - def __mul__(self, other: Ref | int | float) -> MathExpression: + def __mul__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self.to_str()} * {self._format_value(other)})") - def __rmul__(self, other: Ref | int | float) -> MathExpression: + def __rmul__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self._format_value(other)} * {self.to_str()})") - def __truediv__(self, other: Ref | int | float) -> MathExpression: + def __truediv__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self.to_str()} / {self._format_value(other)})") - def __rtruediv__(self, other: Ref | int | float) -> MathExpression: + def __rtruediv__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self._format_value(other)} / {self.to_str()})") - def __mod__(self, other: Ref | int | float) -> MathExpression: + def __mod__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self.to_str()} % {self._format_value(other)})") - def __rmod__(self, other: Ref | int | float) -> MathExpression: + def __rmod__( + self: Ref | MathExpression, other: Ref | MathExpression | int | float + ) -> MathExpression: return MathExpression(f"({self._format_value(other)} % {self.to_str()})") - def __and__(self, other: Ref | Condition) -> Condition: - if isinstance(other, Ref): - return Condition(f"({self.to_str()} && {other.to_str()})") - return Condition(f"({self.to_str()} && {other._expression})") - def __or__(self, other: Ref | Condition) -> Condition: - if isinstance(other, Ref): - return Condition(f"({self.to_str()} || {other.to_str()})") - return Condition(f"({self.to_str()} || {other._expression})") +class _Combine(_Expr): + """ "Base for combining refs and conditions""" + + def _get_left_right( + self: Ref | Condition, right: Ref | Condition + ) -> tuple[str, str]: + return self.to_str() if isinstance( + self, Ref + ) else self._expression, right.to_str() if isinstance( + right, Ref + ) else right._expression + + def __and__(self: Ref | Condition, other: Ref | Condition) -> Condition: + left, right = self._get_left_right(other) + return Condition(f"({left} && {right})") + + def __or__(self: Ref | Condition, other: Ref | Condition) -> Condition: + left, right = self._get_left_right(other) + return Condition(f"({left} || {right})") + + def __invert__(self: Ref | Condition) -> Condition: + return Condition( + f"!{self.to_str() if isinstance(self, Ref) else self._expression}" + ) + + +class Ref(_Math, _Combine): + """Base class for all references""" + + def __init__(self, prefix: str, field: str, screen: Screen | str | None = None): + self._prefix = prefix + self._field = field + self._screen = ( + f"screen.{screen.id if isinstance(screen, Screen) else screen}." + if screen + else "" + ) + + def to_str(self) -> str: + return "${%s%s.%s}" % ( + self._screen, + self._prefix, + self._field, + ) - def __invert__(self) -> Condition: - return Condition(f"!{self.to_str()}") + def __eq__(self, other: Ref | bool | int | float | str) -> Condition: + return Condition(f"({self.to_str()} == {self._format_value(other)})") + + def __ne__(self, other: Ref | bool | int | float | str) -> Condition: + return Condition(f"({self.to_str()} != {self._format_value(other)})") + + def __gt__(self, other: Ref | int | float) -> Condition: + return Condition(f"({self.to_str()} > {self._format_value(other)})") + + def __ge__(self, other: Ref | int | float) -> Condition: + return Condition(f"({self.to_str()} >= {self._format_value(other)})") + + def __lt__(self, other: Ref | int | float) -> Condition: + return Condition(f"({self.to_str()} < {self._format_value(other)})") + + def __le__(self, other: Ref | int | float) -> Condition: + return Condition(f"({self.to_str()} <= {self._format_value(other)})") def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_str()})" @@ -1421,7 +1557,7 @@ def __repr__(self) -> str: _RefT = TypeVar("_RefT", bound=Ref) -class MathExpression: +class MathExpression(_Math): """ This class automatically created when using the arithmetic operators on :class:`Ref` objects. @@ -1471,9 +1607,6 @@ class MathExpression: def __init__(self, expression: str): self._expression = expression - def __str__(self) -> str: - return self.to_str() - def __repr__(self) -> str: return f"MathExpression({self._expression})" @@ -1481,7 +1614,7 @@ def to_str(self) -> str: return self._expression -class Condition: +class Condition(_Combine): """ This class automatically created when using the comparison operators on :class:`Ref` objects. @@ -1548,22 +1681,6 @@ def __init__(self, expression: str): self._expression = expression self.wrap_with_backticks = False - def __and__(self, other: Condition | Ref) -> Condition: - if isinstance(other, Condition): - return Condition(f"({self._expression} && {other._expression})") - return Condition(f"({self._expression} && {other.to_str()})") - - def __or__(self, other: Condition | Ref) -> Condition: - if isinstance(other, Condition): - return Condition(f"({self._expression} || {other._expression})") - return Condition(f"({self._expression} || {other.to_str()})") - - def __invert__(self) -> Condition: - return Condition(f"!{self._expression}") - - def __str__(self) -> str: - return self.to_str() - def __repr__(self) -> str: return f"Condition({self._expression})" @@ -1577,12 +1694,12 @@ def to_str(self) -> str: class ScreenDataRef(Ref): """ - Represents a ScreenData reference (converts to ``${data.}`` | ``${screen..data.}``). + Represents a :class:`ScreenData` reference (converts to ``${data.}`` | ``${screen..data.}``). Example: - + - Hint: use this class directly only if you don't have access to the :class:`ScreenData` object. - Hint: use the ``.ref`` property of :class:`ScreenData` to get reference to a ScreenData. - - Hint: use the ``.ref_in(screen)`` method of :class:`ScreenData` to get the data ref from another screen. + - Hint: use the ``.ref_in(screen)`` method of :class:`ScreenData` to get the data ref from another screen (or ``screen/ref``). >>> FlowJSON( ... screens=[ @@ -1594,7 +1711,9 @@ class ScreenDataRef(Ref): ... TextHeading( ... text=welcome.ref, # data in the same screen ... visible=is_visible.ref_in(other) # data from other screen - ... ) + ... ), + ... TextBody( + ... text=ScreenDataRef(key='welcome', screen='START'), # using the class directly ... ]) ... ) ... ] @@ -1615,6 +1734,7 @@ class ComponentRef(Ref): Example: + - Hint: use this class directly only if you don't have access to the component object. - Hint: use the ``.ref`` property of each component to get a reference to this component. - Hint: use the ``.ref_in(screen)`` method of each component to get the component reference variable of that component with the given screen name. @@ -1630,6 +1750,7 @@ class ComponentRef(Ref): ... phone := TextInput(name='phone', ...), ... TextBody(text=phone.ref, ...), # component reference from the same screen ... TextCaption(text=email.ref_in(other), ...) # component reference from another screen + ... TextHeading(text=ComponentRef('phone', screen='START'), ...) # using the class directly ... ]) ... ) ... ] @@ -2347,7 +2468,7 @@ class OptIn(FormComponent): required: bool | str | ScreenDataRef | ComponentRef | None = None visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None init_value: bool | str | ScreenDataRef | ComponentRef | None = None - on_click_action: UpdateDataAction | OpenUrlAction | NavigateAction | None = None + on_click_action: OpenUrlAction | DataExchangeAction | NavigateAction | None = None on_select_action: UpdateDataAction | None = None on_unselect_action: UpdateDataAction | None = None @@ -2366,7 +2487,7 @@ class EmbeddedLink(Component): >>> EmbeddedLink( ... text='Sign up', ... on_click_action=NavigateAction( - ... next=ActionNext(name='SIGNUP_SCREEN'), + ... next=Next(name='SIGNUP_SCREEN'), ... payload={'data': 'value'} ... ) ... ) @@ -2876,7 +2997,7 @@ class FlowActionType(utils.StrEnum): UPDATE_DATA = "update_data" -class ActionNextType(utils.StrEnum): +class NextType(utils.StrEnum): """ The type of the next action @@ -2889,18 +3010,24 @@ class ActionNextType(utils.StrEnum): PLUGIN = "plugin" +ActionNextType = NextType # Deprecated + + @dataclasses.dataclass(slots=True, kw_only=True) -class ActionNext: +class Next: """ The next action Attributes: name: The name of the next screen or plugin - type: The type of the next action (Default: ``ActionNextType.SCREEN``) + type: The type of the next action (Default: ``NextType.SCREEN``) """ name: str - type: ActionNextType | str = ActionNextType.SCREEN + type: NextType | str = NextType.SCREEN + + +ActionNext = Next # Deprecated def _deprecate_action(action: FlowActionType, use_cls: type[BaseAction]) -> None: @@ -3005,7 +3132,7 @@ class NavigateAction(BaseAction): Example: >>> NavigateAction( - ... next=ActionNext(name='NEXT_SCREEN'), + ... next=Next(name='NEXT_SCREEN'), ... payload={'data': 'value'} ... ) @@ -3018,7 +3145,7 @@ class NavigateAction(BaseAction): name: FlowActionType = dataclasses.field( default=FlowActionType.NAVIGATE, init=False, repr=False ) - next: ActionNext + next: Next payload: dict[str, str | bool | Iterable[DataSource] | ScreenDataRef | ComponentRef] @@ -3055,18 +3182,25 @@ class UpdateDataAction(BaseAction): Example: + >>> is_visible = ScreenData(key='is_visible', example=True) >>> UpdateDataAction( - ... payload={'i_agree': True} + ... payload=[ + ... is_visible.update(value=False) + ... ] ... ) Attributes: - payload: The data to update for the current screen. The keys in the payload should match the component names. + payload: The data to update for the current screen. """ name: FlowActionType = dataclasses.field( default=FlowActionType.UPDATE_DATA, init=False, repr=False ) - payload: dict[str, str | bool | Iterable[DataSource] | ScreenDataRef | ComponentRef] + payload: Iterable[ScreenDataUpdate] | dict[str, _ScreenDataValType] + + def __post_init__(self): + if not isinstance(self.payload, dict): + self.payload = _ScreenDatasContainer(self.payload) @dataclasses.dataclass(slots=True, kw_only=True) diff --git a/pywa_async/types/flows.py b/pywa_async/types/flows.py index 5cb1ef80..95c36e41 100644 --- a/pywa_async/types/flows.py +++ b/pywa_async/types/flows.py @@ -21,6 +21,7 @@ "FlowJSON", "Screen", "ScreenData", + "ScreenDataUpdate", "Layout", "LayoutType", "Form", @@ -61,8 +62,10 @@ "OpenUrlAction", "FlowActionType", "FlowRequestActionType", - "ActionNext", - "ActionNextType", + "Next", + "NextType", + "ActionNext", # Deprecated + "ActionNextType", # Deprecated ] import httpx diff --git a/tests/data/flows/2_1/examples.py b/tests/data/flows/2_1/examples.py index 09434bf9..8fef400c 100644 --- a/tests/data/flows/2_1/examples.py +++ b/tests/data/flows/2_1/examples.py @@ -32,9 +32,7 @@ Footer( label="Continue", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="RATE" - ), + next=Next(name="RATE"), payload={ "recommend_radio": recommend_radio.ref, "comment_text": comment_text.ref, @@ -208,8 +206,8 @@ Footer( label="Continue", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="QUESTION_TWO" + next=Next( + type=NextType.SCREEN, name="QUESTION_TWO" ), payload={ "question1Checkbox": question1_checkbox.ref @@ -251,8 +249,8 @@ Footer( label="Continue", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="QUESTION_THREE", ), payload={ @@ -469,9 +467,7 @@ Footer( label="Continue", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="SURVEY" - ), + next=Next(type=NextType.SCREEN, name="SURVEY"), payload={ "firstName": first_name.ref, "lastName": last_name.ref, @@ -568,17 +564,15 @@ EmbeddedLink( text="Don't have an account? Sign up", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="SIGN_UP" - ), + next=Next(type=NextType.SCREEN, name="SIGN_UP"), payload={}, ), ), EmbeddedLink( text="Forgot password", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="FORGOT_PASSWORD", ), payload={"body": "Example"}, @@ -643,8 +637,8 @@ label="I agree with the terms.", required=True, on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="TERMS_AND_CONDITIONS", ), payload={}, @@ -790,8 +784,8 @@ label="I agree with the terms.", required=True, on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="TERMS_AND_CONDITIONS", ), payload={}, @@ -934,8 +928,8 @@ Footer( label="Continue", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="BOOKING_DETAILS", ), payload={}, @@ -1024,8 +1018,8 @@ label="Accept our Privacy Policy", required=True, on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="TERMS_AND_CONDITIONS", ), payload={}, @@ -1232,8 +1226,8 @@ label="Accept our Privacy Policy", required=True, on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, + next=Next( + type=NextType.SCREEN, name="TERMS_AND_CONDITIONS", ), payload={}, diff --git a/tests/data/flows/4_0/examples.py b/tests/data/flows/4_0/examples.py index 3fd9010a..211b909b 100644 --- a/tests/data/flows/4_0/examples.py +++ b/tests/data/flows/4_0/examples.py @@ -206,9 +206,7 @@ Footer( label="CTA", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="SCREEN_TWO" - ), + next=Next(name="SCREEN_TWO"), payload={}, ), ), @@ -301,9 +299,7 @@ EmbeddedLink( text="Choose insurance type", on_click_action=NavigateAction( - next=ActionNext( - type=ActionNextType.SCREEN, name="SELECT_INSURANCE" - ), + next=Next(type=NextType.SCREEN, name="SELECT_INSURANCE"), payload={}, ), ), diff --git a/tests/data/flows/5_1/examples.py b/tests/data/flows/5_1/examples.py index 240167ac..3e524812 100644 --- a/tests/data/flows/5_1/examples.py +++ b/tests/data/flows/5_1/examples.py @@ -32,7 +32,7 @@ required=True, label="RichText can be used to render large static or dynamic texts.", on_click_action=NavigateAction( - next=ActionNext(type=ActionNextType.SCREEN, name="TOC"), + next=Next(name="TOC"), payload={}, ), ), diff --git a/tests/data/flows/6_0/examples.py b/tests/data/flows/6_0/examples.py index 8b94c90f..a78c3abe 100644 --- a/tests/data/flows/6_0/examples.py +++ b/tests/data/flows/6_0/examples.py @@ -105,114 +105,158 @@ id="1", title="USA", on_select_action=UpdateDataAction( - payload={ - states.key: [ - DataSource( - id="new_york", - title="New York", - on_unselect_action=UpdateDataAction( - payload={pincode_visibility.key: False}, - ), - on_select_action=UpdateDataAction( - payload={ - pincode.key: [ - DataSource( - id="10001", title="10001" + payload=[ + states.update( + new_value=[ + DataSource( + id="new_york", + title="New York", + on_unselect_action=UpdateDataAction( + payload=[ + pincode_visibility.update( + new_value=False + ) + ], + ), + on_select_action=UpdateDataAction( + payload=[ + pincode.update( + new_value=[ + DataSource( + id="10001", + title="10001", + ), + DataSource( + id="10005", + title="10005", + ), + ] ), - DataSource( - id="10005", title="10005" + pincode_visibility.update( + new_value=True ), ], - pincode_visibility.key: True, - }, + ), ), - ), - DataSource( - id="california", - title="California", - on_unselect_action=UpdateDataAction( - payload={pincode_visibility.key: False}, - ), - on_select_action=UpdateDataAction( - payload={ - pincode.key: [ - DataSource( - id="90019", title="90019" + DataSource( + id="california", + title="California", + on_unselect_action=UpdateDataAction( + payload={ + pincode_visibility.update( + new_value=False + ) + }, + ), + on_select_action=UpdateDataAction( + payload=[ + pincode.update( + new_value=[ + DataSource( + id="90019", + title="90019", + ), + DataSource( + id="93504", + title="93504", + ), + ] ), - DataSource( - id="93504", title="93504" + pincode_visibility.update( + new_value=True ), ], - pincode_visibility.key: True, - }, + ), ), - ), - ], - state_visibility.key: True, - }, + ] + ), + state_visibility.update(new_value=True), + ], ), on_unselect_action=UpdateDataAction( - payload={ - state_visibility.key: False, - pincode_visibility.key: False, - }, + payload=[ + state_visibility.update(new_value=False), + pincode_visibility.update(new_value=False), + ], ), ), DataSource( id="2", title="Canada", on_select_action=UpdateDataAction( - payload={ - states.key: [ - DataSource( - id="ontario", - title="Ontario", - on_unselect_action=UpdateDataAction( - payload={pincode_visibility.key: False}, - ), - on_select_action=UpdateDataAction( - payload={ - pincode.key: [ - DataSource( - id="L4K", title="L4K" + payload=[ + states.update( + new_value=[ + DataSource( + id="ontario", + title="Ontario", + on_unselect_action=UpdateDataAction( + payload=[ + pincode_visibility.update( + new_value=False + ) + ], + ), + on_select_action=UpdateDataAction( + payload=[ + pincode.update( + new_value=[ + DataSource( + id="L4K", + title="L4K", + ), + DataSource( + id="M3C", + title="M3C", + ), + ] ), - DataSource( - id="M3C", title="M3C" + pincode_visibility.update( + new_value=True ), ], - pincode_visibility.key: True, - }, - ), - ), - DataSource( - id="quebec", - title="Quebec", - on_unselect_action=UpdateDataAction( - payload={pincode_visibility.key: False}, + ), ), - on_select_action=UpdateDataAction( - payload={ - pincode.key: [ - DataSource( - id="M6B2A9", title="M6B2A9" + DataSource( + id="quebec", + title="Quebec", + on_unselect_action=UpdateDataAction( + payload=[ + pincode_visibility.update( + new_value=False + ) + ], + ), + on_select_action=UpdateDataAction( + payload=[ + pincode.update( + new_value=[ + DataSource( + id="M6B2A9", + title="M6B2A9", + ), + DataSource( + id="M5V", + title="M5V", + ), + ] ), - DataSource( - id="M5V", title="M5V" + pincode_visibility.update( + new_value=True ), ], - pincode_visibility.key: True, - }, + ), ), - ), - ], - state_visibility.key: True, - }, + ] + ), + state_visibility.update(new_value=True), + ], ), on_unselect_action=UpdateDataAction( - payload={ - state_visibility.key: False, - pincode_visibility.key: False, - }, + payload=[ + state_visibility.update(new_value=False), + pincode_visibility.update(new_value=False), + ], ), ), ], diff --git a/tests/test_flows.py b/tests/test_flows.py index 3e5d36fd..492ef6cf 100644 --- a/tests/test_flows.py +++ b/tests/test_flows.py @@ -239,6 +239,22 @@ def test_math_op_with_ref(): assert math_op_with_ref.to_str() == "((21 + ${data.age}) + ${data.age})" +def test_math_op_with_math_op(): + ref = Ref(prefix="data", field="age") + math_op_1 = 21 + ref + math_op_2 = 21 + ref + assert ( + math_op_1 + math_op_2 + ).to_str() == "((21 + ${data.age}) + (21 + ${data.age}))" + + +def test_math_op_with_number(): + ref = Ref(prefix="data", field="age") + math_op = 21 + ref + math_op_with_number = math_op + 21 + assert math_op_with_number.to_str() == "((21 + ${data.age}) + 21)" + + def test_ref_with_math_op_and_ref(): ref = Ref(prefix="data", field="age") math_op = 21 + ref