Skip to content

Commit

Permalink
Merge pull request #96 from david-lev/flow-str
Browse files Browse the repository at this point in the history
[flows] adding `FlowStr` - A helper class to create strings containing vars and math expressions without escaping and quoting them
  • Loading branch information
david-lev authored Jan 11, 2025
2 parents dbd8458 + d88bbb0 commit 018e1fc
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 49 deletions.
129 changes: 93 additions & 36 deletions pywa/types/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"Form",
"ScreenDataRef",
"ComponentRef",
"FlowStr",
"TextHeading",
"TextSubheading",
"TextBody",
Expand Down Expand Up @@ -1446,9 +1447,10 @@ class ComponentType(utils.StrEnum):
NAVIGATION_LIST = "NavigationList"


class _Expr:
class _Expr(abc.ABC):
"""Base for refs, conditions, and expressions"""

@abc.abstractmethod
def to_str(self) -> str: ...

def __str__(self) -> str:
Expand All @@ -1468,7 +1470,7 @@ def _format_value(val: _Expr | bool | int | float | str) -> str:
return str(val)


class _Math(_Expr):
class _Math(_Expr, abc.ABC):
"""Base for math expressions"""

def _to_math(self, left: _MathT, operator: str, right: _MathT) -> MathExpression:
Expand Down Expand Up @@ -1507,7 +1509,7 @@ def __rmod__(self: Ref | MathExpression, other: _MathT) -> MathExpression:
return self._to_math(other, "%", self)


class _Combine(_Expr):
class _Combine(_Expr, abc.ABC):
""" "Base for combining refs and conditions"""

def _get_left_right(
Expand Down Expand Up @@ -1788,6 +1790,60 @@ def __init__(self, component_name: str, screen: Screen | str | None = None):
super().__init__(prefix="form", field=component_name, screen=screen)


class FlowStr(_Expr):
"""
Dynamic string that uses variables and math expressions. This is a helper class to avoid all the
escaping and wrapping with quotes when using string concatenation.
- Added in v6.0.
Example::
>>> FlowJSON(
... screens=[
... Screen(
... id='START',
... layout=Layout(children=[
... age := TextInput(name='age', input_type=InputType.NUMBER),
... email := TextInput(name='email', input_type=InputType.EMAIL),
... TextHeading(text=FlowStr("Your age is {age} and your email is {email}", age=age.ref, email=email.ref), ...)
... ])
... )
... ]
... )
>>> FlowJSON(
... screens=[
... Screen(
... id='START',
... layout=Layout(children=[
... bill := TextInput(name='bill', input_type=InputType.NUMBER),
... tip := TextInput(name='tip', input_type=InputType.NUMBER),
... TextHeading(text=FlowStr("Your total bill is {bill}", bill=bill.ref + (bill.ref * tip.ref / 100)), ...)
... ])
... )
... ]
... )
"""

def __init__(self, string: str, **variables: Ref | MathExpression):
"""
Initialize the dynamic string.
Args:
string: The string with placeholders for the variables.
**variables: The variables to replace in the string.
"""
self.string = string
self.variables = variables

def to_str(self) -> str:
escaped = re.sub(r"(?<!\\)([`'])", r"\\\\\1", self.string)
wrapped = re.sub(r"([^{}]+)(?=\{|$)", r" '\1' ", escaped)
return f"`{wrapped.format(**self.variables)}`"


@dataclasses.dataclass(slots=True, kw_only=True)
class Form(Component):
"""
Expand Down Expand Up @@ -1861,7 +1917,7 @@ def name(self) -> str: ...

@property
@abc.abstractmethod
def label(self) -> str | ScreenDataRef | ComponentRef: ...
def label(self) -> str | ScreenDataRef | ComponentRef | FlowStr: ...

@property
@abc.abstractmethod
Expand Down Expand Up @@ -1941,7 +1997,7 @@ class TextComponent(Component, abc.ABC):

@property
@abc.abstractmethod
def text(self) -> str | ScreenDataRef | ComponentRef: ...
def text(self) -> str | ScreenDataRef | ComponentRef | FlowStr: ...


class FontWeight(utils.StrEnum):
Expand Down Expand Up @@ -1980,7 +2036,7 @@ class TextHeading(TextComponent):
type: ComponentType = dataclasses.field(
default=ComponentType.TEXT_HEADING, init=False, repr=False
)
text: str | ScreenDataRef | ComponentRef
text: str | ScreenDataRef | ComponentRef | FlowStr
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None


Expand All @@ -2003,7 +2059,7 @@ class TextSubheading(TextComponent):
type: ComponentType = dataclasses.field(
default=ComponentType.TEXT_SUBHEADING, init=False, repr=False
)
text: str | ScreenDataRef | ComponentRef
text: str | ScreenDataRef | ComponentRef | FlowStr
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None


Expand Down Expand Up @@ -2034,7 +2090,7 @@ class TextBody(TextComponent):
type: ComponentType = dataclasses.field(
default=ComponentType.TEXT_BODY, init=False, repr=False
)
text: str | Iterable[str] | ScreenDataRef | ComponentRef
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
markdown: bool | None = None
font_weight: FontWeight | str | ScreenDataRef | ComponentRef | None = None
strikethrough: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2068,7 +2124,7 @@ class TextCaption(TextComponent):
type: ComponentType = dataclasses.field(
default=ComponentType.TEXT_CAPTION, init=False, repr=False
)
text: str | Iterable[str] | ScreenDataRef | ComponentRef
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
markdown: bool | None = None
font_weight: FontWeight | str | ScreenDataRef | ComponentRef | None = None
strikethrough: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2119,7 +2175,7 @@ class RichText(TextComponent):
type: ComponentType = dataclasses.field(
default=ComponentType.RICH_TEXT, init=False, repr=False
)
text: str | Iterable[str] | ScreenDataRef | ComponentRef
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None


Expand All @@ -2132,7 +2188,7 @@ class TextEntryComponent(FormComponent, abc.ABC):

@property
@abc.abstractmethod
def helper_text(self) -> str | ScreenDataRef | ComponentRef | None: ...
def helper_text(self) -> str | ScreenDataRef | ComponentRef | FlowStr | None: ...

@property
@abc.abstractmethod
Expand Down Expand Up @@ -2201,13 +2257,13 @@ class TextInput(TextEntryComponent):
default=ComponentType.TEXT_INPUT, init=False, repr=False
)
name: str
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
input_type: InputType | str | ScreenDataRef | ComponentRef | None = None
pattern: str | re.Pattern | ScreenDataRef | ComponentRef | None = None
required: bool | str | ScreenDataRef | ComponentRef | None = None
min_chars: int | str | ScreenDataRef | ComponentRef | None = None
max_chars: int | str | ScreenDataRef | ComponentRef | None = None
helper_text: str | ScreenDataRef | ComponentRef | None = None
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
init_value: str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2248,10 +2304,10 @@ class TextArea(TextEntryComponent):
default=ComponentType.TEXT_AREA, init=False, repr=False
)
name: str
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
required: bool | str | ScreenDataRef | ComponentRef | None = None
max_length: int | str | ScreenDataRef | ComponentRef | None = None
helper_text: str | ScreenDataRef | ComponentRef | None = None
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
init_value: str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2316,8 +2372,8 @@ class CheckboxGroup(FormComponent):
)
name: str
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | None = None
description: str | ScreenDataRef | ComponentRef | None = None
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
min_selected_items: int | str | ScreenDataRef | ComponentRef | None = None
max_selected_items: int | str | ScreenDataRef | ComponentRef | None = None
required: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2370,8 +2426,8 @@ class RadioButtonsGroup(FormComponent):
)
name: str
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | None = None
description: str | ScreenDataRef | ComponentRef | None = None
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
required: bool | str | ScreenDataRef | ComponentRef | None = None
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2419,7 +2475,7 @@ class Dropdown(FormComponent):
default=ComponentType.DROPDOWN, init=False, repr=False
)
name: str
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
required: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2449,11 +2505,11 @@ class Footer(Component):
default=ComponentType.FOOTER, init=False, repr=False
)
visible: None = dataclasses.field(default=None, init=False, repr=False)
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
on_click_action: CompleteAction | DataExchangeAction | NavigateAction
left_caption: str | ScreenDataRef | ComponentRef | None = None
center_caption: str | ScreenDataRef | ComponentRef | None = None
right_caption: str | ScreenDataRef | ComponentRef | None = None
left_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
center_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
right_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None


Expand Down Expand Up @@ -2489,7 +2545,7 @@ class OptIn(FormComponent):
)
enabled: None = dataclasses.field(default=None, init=False, repr=False)
name: str
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
required: bool | str | ScreenDataRef | ComponentRef | None = None
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
init_value: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2526,7 +2582,7 @@ class EmbeddedLink(Component):
type: ComponentType = dataclasses.field(
default=ComponentType.EMBEDDED_LINK, init=False, repr=False
)
text: str | ScreenDataRef | ComponentRef
text: str | ScreenDataRef | ComponentRef | FlowStr
on_click_action: (
DataExchangeAction | UpdateDataAction | NavigateAction | OpenUrlAction
)
Expand Down Expand Up @@ -2578,8 +2634,8 @@ class NavigationList(Component):
visible: None = dataclasses.field(default=None, init=False, repr=False)
name: str
list_items: Iterable[NavigationItem] | ScreenDataRef | ComponentRef | str
label: str | ScreenDataRef | ComponentRef | None = None
description: str | ScreenDataRef | ComponentRef | None = None
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
media_size: MediaSize | str | ScreenDataRef | ComponentRef | None = None
on_click_action: NavigateAction | DataExchangeAction | None = None

Expand Down Expand Up @@ -2721,13 +2777,13 @@ class DatePicker(FormComponent):
default=ComponentType.DATE_PICKER, init=False, repr=False
)
name: str
label: str | ScreenDataRef | ComponentRef
label: str | ScreenDataRef | ComponentRef | FlowStr
min_date: datetime.date | str | ScreenDataRef | ComponentRef | None = None
max_date: datetime.date | str | ScreenDataRef | ComponentRef | None = None
unavailable_dates: (
Iterable[datetime.date | str] | str | ScreenDataRef | ComponentRef | None
) = None
helper_text: str | ScreenDataRef | ComponentRef | None = None
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
required: bool | str | ScreenDataRef | ComponentRef | None = None
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2822,8 +2878,8 @@ class CalendarPicker(FormComponent):
default=ComponentType.CALENDAR_PICKER, init=False, repr=False
)
name: str
title: str | ScreenDataRef | ComponentRef | None = None
description: str | ScreenDataRef | ComponentRef | None = None
title: str | ScreenDataRef | ComponentRef | FlowStr | None = None
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
label: (
dict[Literal["start-date", "end-date"], str]
| str
Expand All @@ -2846,6 +2902,7 @@ class CalendarPicker(FormComponent):
| str
| ScreenDataRef
| ComponentRef
| FlowStr
| None
) = None
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -2995,8 +3052,8 @@ class PhotoPicker(FormComponent):
required: None = dataclasses.field(default=None, init=False, repr=False)
init_value: None = dataclasses.field(default=None, init=False, repr=False)
name: str
label: str | ScreenDataRef | ComponentRef
description: str | ScreenDataRef | ComponentRef | None = None
label: str | ScreenDataRef | ComponentRef | FlowStr
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
photo_source: PhotoSource | str | ScreenDataRef | ComponentRef | None = None
max_file_size_kb: int | str | ScreenDataRef | ComponentRef | None = None
min_uploaded_photos: int | str | ScreenDataRef | ComponentRef | None = None
Expand Down Expand Up @@ -3048,8 +3105,8 @@ class DocumentPicker(FormComponent):
required: None = dataclasses.field(default=None, init=False, repr=False)
init_value: None = dataclasses.field(default=None, init=False, repr=False)
name: str
label: str | ScreenDataRef | ComponentRef
description: str | ScreenDataRef | ComponentRef | None = None
label: str | ScreenDataRef | ComponentRef | FlowStr
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
max_file_size_kb: int | str | ScreenDataRef | ComponentRef | None = None
min_uploaded_documents: int | str | ScreenDataRef | ComponentRef | None = None
max_uploaded_documents: int | str | ScreenDataRef | ComponentRef | None = None
Expand Down
1 change: 1 addition & 0 deletions pywa_async/types/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"Form",
"ScreenDataRef",
"ComponentRef",
"FlowStr",
"TextHeading",
"TextSubheading",
"TextBody",
Expand Down
12 changes: 6 additions & 6 deletions tests/data/flows/6_0/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
},
{
"type": "TextBody",
"text": "`'Hello ' ${form.first_name}`"
"text": "` 'Hello ' ${form.first_name}`"
},
{
"type": "TextBody",
"text": "`${form.first_name} ' you are ' ${form.age} ' years old.'`"
"text": "`${form.first_name} ' you are ' ${form.age} ' years old.' `"
},
{
"type": "Footer",
Expand Down Expand Up @@ -360,10 +360,10 @@
{
"type": "TextBody",
"text": [
"`'The sum of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} + ${data.number_2})`",
"`'The difference of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} - ${data.number_2})`",
"`'The product of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} * ${data.number_2})`",
"`'The division of ' ${data.number_1} ' by ' ${data.number_2} ' is ' (${data.number_1} / ${data.number_2})`"
"` 'The sum of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} + ${data.number_2})`",
"` 'The difference of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} - ${data.number_2})`",
"` 'The product of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} * ${data.number_2})`",
"` 'The division of ' ${data.number_1} ' by ' ${data.number_2} ' is ' (${data.number_1} / ${data.number_2})`"
]
},
{
Expand Down
Loading

0 comments on commit 018e1fc

Please sign in to comment.