Skip to content

Commit 018e1fc

Browse files
authored
Merge pull request #96 from david-lev/flow-str
[flows] adding `FlowStr` - A helper class to create strings containing vars and math expressions without escaping and quoting them
2 parents dbd8458 + d88bbb0 commit 018e1fc

File tree

5 files changed

+179
-49
lines changed

5 files changed

+179
-49
lines changed

pywa/types/flows.py

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"Form",
6666
"ScreenDataRef",
6767
"ComponentRef",
68+
"FlowStr",
6869
"TextHeading",
6970
"TextSubheading",
7071
"TextBody",
@@ -1446,9 +1447,10 @@ class ComponentType(utils.StrEnum):
14461447
NAVIGATION_LIST = "NavigationList"
14471448

14481449

1449-
class _Expr:
1450+
class _Expr(abc.ABC):
14501451
"""Base for refs, conditions, and expressions"""
14511452

1453+
@abc.abstractmethod
14521454
def to_str(self) -> str: ...
14531455

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

14701472

1471-
class _Math(_Expr):
1473+
class _Math(_Expr, abc.ABC):
14721474
"""Base for math expressions"""
14731475

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

15091511

1510-
class _Combine(_Expr):
1512+
class _Combine(_Expr, abc.ABC):
15111513
""" "Base for combining refs and conditions"""
15121514

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

17901792

1793+
class FlowStr(_Expr):
1794+
"""
1795+
Dynamic string that uses variables and math expressions. This is a helper class to avoid all the
1796+
escaping and wrapping with quotes when using string concatenation.
1797+
1798+
- Added in v6.0.
1799+
1800+
Example::
1801+
1802+
>>> FlowJSON(
1803+
... screens=[
1804+
... Screen(
1805+
... id='START',
1806+
... layout=Layout(children=[
1807+
... age := TextInput(name='age', input_type=InputType.NUMBER),
1808+
... email := TextInput(name='email', input_type=InputType.EMAIL),
1809+
... TextHeading(text=FlowStr("Your age is {age} and your email is {email}", age=age.ref, email=email.ref), ...)
1810+
... ])
1811+
... )
1812+
... ]
1813+
... )
1814+
1815+
>>> FlowJSON(
1816+
... screens=[
1817+
... Screen(
1818+
... id='START',
1819+
... layout=Layout(children=[
1820+
... bill := TextInput(name='bill', input_type=InputType.NUMBER),
1821+
... tip := TextInput(name='tip', input_type=InputType.NUMBER),
1822+
... TextHeading(text=FlowStr("Your total bill is {bill}", bill=bill.ref + (bill.ref * tip.ref / 100)), ...)
1823+
... ])
1824+
... )
1825+
... ]
1826+
... )
1827+
1828+
"""
1829+
1830+
def __init__(self, string: str, **variables: Ref | MathExpression):
1831+
"""
1832+
Initialize the dynamic string.
1833+
1834+
Args:
1835+
string: The string with placeholders for the variables.
1836+
**variables: The variables to replace in the string.
1837+
"""
1838+
self.string = string
1839+
self.variables = variables
1840+
1841+
def to_str(self) -> str:
1842+
escaped = re.sub(r"(?<!\\)([`'])", r"\\\\\1", self.string)
1843+
wrapped = re.sub(r"([^{}]+)(?=\{|$)", r" '\1' ", escaped)
1844+
return f"`{wrapped.format(**self.variables)}`"
1845+
1846+
17911847
@dataclasses.dataclass(slots=True, kw_only=True)
17921848
class Form(Component):
17931849
"""
@@ -1861,7 +1917,7 @@ def name(self) -> str: ...
18611917

18621918
@property
18631919
@abc.abstractmethod
1864-
def label(self) -> str | ScreenDataRef | ComponentRef: ...
1920+
def label(self) -> str | ScreenDataRef | ComponentRef | FlowStr: ...
18651921

18661922
@property
18671923
@abc.abstractmethod
@@ -1941,7 +1997,7 @@ class TextComponent(Component, abc.ABC):
19411997

19421998
@property
19431999
@abc.abstractmethod
1944-
def text(self) -> str | ScreenDataRef | ComponentRef: ...
2000+
def text(self) -> str | ScreenDataRef | ComponentRef | FlowStr: ...
19452001

19462002

19472003
class FontWeight(utils.StrEnum):
@@ -1980,7 +2036,7 @@ class TextHeading(TextComponent):
19802036
type: ComponentType = dataclasses.field(
19812037
default=ComponentType.TEXT_HEADING, init=False, repr=False
19822038
)
1983-
text: str | ScreenDataRef | ComponentRef
2039+
text: str | ScreenDataRef | ComponentRef | FlowStr
19842040
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
19852041

19862042

@@ -2003,7 +2059,7 @@ class TextSubheading(TextComponent):
20032059
type: ComponentType = dataclasses.field(
20042060
default=ComponentType.TEXT_SUBHEADING, init=False, repr=False
20052061
)
2006-
text: str | ScreenDataRef | ComponentRef
2062+
text: str | ScreenDataRef | ComponentRef | FlowStr
20072063
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
20082064

20092065

@@ -2034,7 +2090,7 @@ class TextBody(TextComponent):
20342090
type: ComponentType = dataclasses.field(
20352091
default=ComponentType.TEXT_BODY, init=False, repr=False
20362092
)
2037-
text: str | Iterable[str] | ScreenDataRef | ComponentRef
2093+
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
20382094
markdown: bool | None = None
20392095
font_weight: FontWeight | str | ScreenDataRef | ComponentRef | None = None
20402096
strikethrough: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2068,7 +2124,7 @@ class TextCaption(TextComponent):
20682124
type: ComponentType = dataclasses.field(
20692125
default=ComponentType.TEXT_CAPTION, init=False, repr=False
20702126
)
2071-
text: str | Iterable[str] | ScreenDataRef | ComponentRef
2127+
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
20722128
markdown: bool | None = None
20732129
font_weight: FontWeight | str | ScreenDataRef | ComponentRef | None = None
20742130
strikethrough: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2119,7 +2175,7 @@ class RichText(TextComponent):
21192175
type: ComponentType = dataclasses.field(
21202176
default=ComponentType.RICH_TEXT, init=False, repr=False
21212177
)
2122-
text: str | Iterable[str] | ScreenDataRef | ComponentRef
2178+
text: str | Iterable[str | FlowStr] | ScreenDataRef | ComponentRef | FlowStr
21232179
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
21242180

21252181

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

21332189
@property
21342190
@abc.abstractmethod
2135-
def helper_text(self) -> str | ScreenDataRef | ComponentRef | None: ...
2191+
def helper_text(self) -> str | ScreenDataRef | ComponentRef | FlowStr | None: ...
21362192

21372193
@property
21382194
@abc.abstractmethod
@@ -2201,13 +2257,13 @@ class TextInput(TextEntryComponent):
22012257
default=ComponentType.TEXT_INPUT, init=False, repr=False
22022258
)
22032259
name: str
2204-
label: str | ScreenDataRef | ComponentRef
2260+
label: str | ScreenDataRef | ComponentRef | FlowStr
22052261
input_type: InputType | str | ScreenDataRef | ComponentRef | None = None
22062262
pattern: str | re.Pattern | ScreenDataRef | ComponentRef | None = None
22072263
required: bool | str | ScreenDataRef | ComponentRef | None = None
22082264
min_chars: int | str | ScreenDataRef | ComponentRef | None = None
22092265
max_chars: int | str | ScreenDataRef | ComponentRef | None = None
2210-
helper_text: str | ScreenDataRef | ComponentRef | None = None
2266+
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
22112267
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
22122268
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
22132269
init_value: str | ScreenDataRef | ComponentRef | None = None
@@ -2248,10 +2304,10 @@ class TextArea(TextEntryComponent):
22482304
default=ComponentType.TEXT_AREA, init=False, repr=False
22492305
)
22502306
name: str
2251-
label: str | ScreenDataRef | ComponentRef
2307+
label: str | ScreenDataRef | ComponentRef | FlowStr
22522308
required: bool | str | ScreenDataRef | ComponentRef | None = None
22532309
max_length: int | str | ScreenDataRef | ComponentRef | None = None
2254-
helper_text: str | ScreenDataRef | ComponentRef | None = None
2310+
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
22552311
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
22562312
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
22572313
init_value: str | ScreenDataRef | ComponentRef | None = None
@@ -2316,8 +2372,8 @@ class CheckboxGroup(FormComponent):
23162372
)
23172373
name: str
23182374
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
2319-
label: str | ScreenDataRef | ComponentRef | None = None
2320-
description: str | ScreenDataRef | ComponentRef | None = None
2375+
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2376+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
23212377
min_selected_items: int | str | ScreenDataRef | ComponentRef | None = None
23222378
max_selected_items: int | str | ScreenDataRef | ComponentRef | None = None
23232379
required: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2370,8 +2426,8 @@ class RadioButtonsGroup(FormComponent):
23702426
)
23712427
name: str
23722428
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
2373-
label: str | ScreenDataRef | ComponentRef | None = None
2374-
description: str | ScreenDataRef | ComponentRef | None = None
2429+
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2430+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
23752431
required: bool | str | ScreenDataRef | ComponentRef | None = None
23762432
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
23772433
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2419,7 +2475,7 @@ class Dropdown(FormComponent):
24192475
default=ComponentType.DROPDOWN, init=False, repr=False
24202476
)
24212477
name: str
2422-
label: str | ScreenDataRef | ComponentRef
2478+
label: str | ScreenDataRef | ComponentRef | FlowStr
24232479
data_source: Iterable[DataSource] | str | ScreenDataRef | ComponentRef
24242480
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
24252481
required: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2449,11 +2505,11 @@ class Footer(Component):
24492505
default=ComponentType.FOOTER, init=False, repr=False
24502506
)
24512507
visible: None = dataclasses.field(default=None, init=False, repr=False)
2452-
label: str | ScreenDataRef | ComponentRef
2508+
label: str | ScreenDataRef | ComponentRef | FlowStr
24532509
on_click_action: CompleteAction | DataExchangeAction | NavigateAction
2454-
left_caption: str | ScreenDataRef | ComponentRef | None = None
2455-
center_caption: str | ScreenDataRef | ComponentRef | None = None
2456-
right_caption: str | ScreenDataRef | ComponentRef | None = None
2510+
left_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2511+
center_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2512+
right_caption: str | ScreenDataRef | ComponentRef | FlowStr | None = None
24572513
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
24582514

24592515

@@ -2489,7 +2545,7 @@ class OptIn(FormComponent):
24892545
)
24902546
enabled: None = dataclasses.field(default=None, init=False, repr=False)
24912547
name: str
2492-
label: str | ScreenDataRef | ComponentRef
2548+
label: str | ScreenDataRef | ComponentRef | FlowStr
24932549
required: bool | str | ScreenDataRef | ComponentRef | None = None
24942550
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
24952551
init_value: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2526,7 +2582,7 @@ class EmbeddedLink(Component):
25262582
type: ComponentType = dataclasses.field(
25272583
default=ComponentType.EMBEDDED_LINK, init=False, repr=False
25282584
)
2529-
text: str | ScreenDataRef | ComponentRef
2585+
text: str | ScreenDataRef | ComponentRef | FlowStr
25302586
on_click_action: (
25312587
DataExchangeAction | UpdateDataAction | NavigateAction | OpenUrlAction
25322588
)
@@ -2578,8 +2634,8 @@ class NavigationList(Component):
25782634
visible: None = dataclasses.field(default=None, init=False, repr=False)
25792635
name: str
25802636
list_items: Iterable[NavigationItem] | ScreenDataRef | ComponentRef | str
2581-
label: str | ScreenDataRef | ComponentRef | None = None
2582-
description: str | ScreenDataRef | ComponentRef | None = None
2637+
label: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2638+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
25832639
media_size: MediaSize | str | ScreenDataRef | ComponentRef | None = None
25842640
on_click_action: NavigateAction | DataExchangeAction | None = None
25852641

@@ -2721,13 +2777,13 @@ class DatePicker(FormComponent):
27212777
default=ComponentType.DATE_PICKER, init=False, repr=False
27222778
)
27232779
name: str
2724-
label: str | ScreenDataRef | ComponentRef
2780+
label: str | ScreenDataRef | ComponentRef | FlowStr
27252781
min_date: datetime.date | str | ScreenDataRef | ComponentRef | None = None
27262782
max_date: datetime.date | str | ScreenDataRef | ComponentRef | None = None
27272783
unavailable_dates: (
27282784
Iterable[datetime.date | str] | str | ScreenDataRef | ComponentRef | None
27292785
) = None
2730-
helper_text: str | ScreenDataRef | ComponentRef | None = None
2786+
helper_text: str | ScreenDataRef | ComponentRef | FlowStr | None = None
27312787
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
27322788
required: bool | str | ScreenDataRef | ComponentRef | None = None
27332789
visible: bool | str | Condition | ScreenDataRef | ComponentRef | None = None
@@ -2822,8 +2878,8 @@ class CalendarPicker(FormComponent):
28222878
default=ComponentType.CALENDAR_PICKER, init=False, repr=False
28232879
)
28242880
name: str
2825-
title: str | ScreenDataRef | ComponentRef | None = None
2826-
description: str | ScreenDataRef | ComponentRef | None = None
2881+
title: str | ScreenDataRef | ComponentRef | FlowStr | None = None
2882+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
28272883
label: (
28282884
dict[Literal["start-date", "end-date"], str]
28292885
| str
@@ -2846,6 +2902,7 @@ class CalendarPicker(FormComponent):
28462902
| str
28472903
| ScreenDataRef
28482904
| ComponentRef
2905+
| FlowStr
28492906
| None
28502907
) = None
28512908
enabled: bool | str | ScreenDataRef | ComponentRef | None = None
@@ -2995,8 +3052,8 @@ class PhotoPicker(FormComponent):
29953052
required: None = dataclasses.field(default=None, init=False, repr=False)
29963053
init_value: None = dataclasses.field(default=None, init=False, repr=False)
29973054
name: str
2998-
label: str | ScreenDataRef | ComponentRef
2999-
description: str | ScreenDataRef | ComponentRef | None = None
3055+
label: str | ScreenDataRef | ComponentRef | FlowStr
3056+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
30003057
photo_source: PhotoSource | str | ScreenDataRef | ComponentRef | None = None
30013058
max_file_size_kb: int | str | ScreenDataRef | ComponentRef | None = None
30023059
min_uploaded_photos: int | str | ScreenDataRef | ComponentRef | None = None
@@ -3048,8 +3105,8 @@ class DocumentPicker(FormComponent):
30483105
required: None = dataclasses.field(default=None, init=False, repr=False)
30493106
init_value: None = dataclasses.field(default=None, init=False, repr=False)
30503107
name: str
3051-
label: str | ScreenDataRef | ComponentRef
3052-
description: str | ScreenDataRef | ComponentRef | None = None
3108+
label: str | ScreenDataRef | ComponentRef | FlowStr
3109+
description: str | ScreenDataRef | ComponentRef | FlowStr | None = None
30533110
max_file_size_kb: int | str | ScreenDataRef | ComponentRef | None = None
30543111
min_uploaded_documents: int | str | ScreenDataRef | ComponentRef | None = None
30553112
max_uploaded_documents: int | str | ScreenDataRef | ComponentRef | None = None

pywa_async/types/flows.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"Form",
2828
"ScreenDataRef",
2929
"ComponentRef",
30+
"FlowStr",
3031
"TextHeading",
3132
"TextSubheading",
3233
"TextBody",

tests/data/flows/6_0/examples.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
},
2626
{
2727
"type": "TextBody",
28-
"text": "`'Hello ' ${form.first_name}`"
28+
"text": "` 'Hello ' ${form.first_name}`"
2929
},
3030
{
3131
"type": "TextBody",
32-
"text": "`${form.first_name} ' you are ' ${form.age} ' years old.'`"
32+
"text": "`${form.first_name} ' you are ' ${form.age} ' years old.' `"
3333
},
3434
{
3535
"type": "Footer",
@@ -360,10 +360,10 @@
360360
{
361361
"type": "TextBody",
362362
"text": [
363-
"`'The sum of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} + ${data.number_2})`",
364-
"`'The difference of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} - ${data.number_2})`",
365-
"`'The product of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} * ${data.number_2})`",
366-
"`'The division of ' ${data.number_1} ' by ' ${data.number_2} ' is ' (${data.number_1} / ${data.number_2})`"
363+
"` 'The sum of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} + ${data.number_2})`",
364+
"` 'The difference of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} - ${data.number_2})`",
365+
"` 'The product of ' ${data.number_1} ' and ' ${data.number_2} ' is ' (${data.number_1} * ${data.number_2})`",
366+
"` 'The division of ' ${data.number_1} ' by ' ${data.number_2} ' is ' (${data.number_1} / ${data.number_2})`"
367367
]
368368
},
369369
{

0 commit comments

Comments
 (0)