Skip to content

Commit 8706f5f

Browse files
committed
typing: widget: Add typed widget outputs and JSON shapes.
Define TypedDicts for todo/poll widget outputs and JSON payloads. Update widget processing to use typed structures and casts. Update widget rendering to use the new return shape. Add a widget test fixture and normalize submessages in tests. Fixes: #1576
1 parent 6a79987 commit 8706f5f

File tree

6 files changed

+146
-34
lines changed

6 files changed

+146
-34
lines changed

tests/widget/test_widget.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing import Dict, List, Union
1+
from typing import Dict, List
22

33
import pytest
44
from pytest import param as case
55

6+
from zulipterminal.api_types import PollOptionInfo, Submessage, TodoTaskInfo
67
from zulipterminal.widget import (
7-
Submessage,
88
find_widget_type,
99
process_poll_widget,
1010
process_todo_widget,
@@ -342,12 +342,12 @@ def test_find_widget_type(
342342
def test_process_todo_widget(
343343
submessages: List[Submessage],
344344
expected_title: str,
345-
expected_tasks: Dict[str, Dict[str, Union[str, bool]]],
345+
expected_tasks: Dict[str, TodoTaskInfo],
346346
) -> None:
347-
title, tasks = process_todo_widget(submessages)
347+
result = process_todo_widget(submessages)
348348

349-
assert title == expected_title
350-
assert tasks == expected_tasks
349+
assert result["title"] == expected_title
350+
assert result["tasks"] == expected_tasks
351351

352352

353353
@pytest.mark.parametrize(
@@ -657,9 +657,9 @@ def test_process_todo_widget(
657657
def test_process_poll_widget(
658658
submessages: List[Submessage],
659659
expected_poll_question: str,
660-
expected_options: Dict[str, Dict[str, Union[str, List[str]]]],
660+
expected_options: Dict[str, PollOptionInfo],
661661
) -> None:
662-
poll_question, options = process_poll_widget(submessages)
662+
result = process_poll_widget(submessages)
663663

664-
assert poll_question == expected_poll_question
665-
assert options == expected_options
664+
assert result["question"] == expected_poll_question
665+
assert result["options"] == expected_options

zulipterminal/api_types.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,113 @@ class SubscriptionSettingChange(TypedDict):
180180

181181
## TODO: Improve this typing to split private and stream message data
182182

183+
###############################################################################
184+
# Submessage (widget) types
185+
# Used in messages for polls, todos, and other interactive widgets
186+
187+
188+
class Submessage(TypedDict):
189+
type: Literal["submessage"]
190+
msg_type: str
191+
message_id: int
192+
submessage_id: int
193+
sender_id: int
194+
content: str
195+
196+
197+
# Widget content types for deserialized JSON content
198+
199+
200+
class TodoTask(TypedDict):
201+
task: str
202+
desc: str
203+
204+
205+
class TodoWidgetData(TypedDict):
206+
widget_type: Literal["todo"]
207+
extra_data: "TodoExtraData"
208+
209+
210+
class TodoExtraData(TypedDict, total=False):
211+
task_list_title: str
212+
tasks: List[TodoTask]
213+
214+
215+
class NewTodoTask(TypedDict):
216+
type: Literal["new_task"]
217+
key: int
218+
task: str
219+
desc: str
220+
completed: bool
221+
222+
223+
class TodoStrike(TypedDict):
224+
type: Literal["strike"]
225+
key: str
226+
227+
228+
class NewTodoTitle(TypedDict):
229+
type: Literal["new_task_list_title"]
230+
title: str
231+
232+
233+
class PollOption(TypedDict):
234+
pass # Option text is just a string in the "options" list
235+
236+
237+
class PollWidgetData(TypedDict):
238+
widget_type: Literal["poll"]
239+
extra_data: "PollExtraData"
240+
241+
242+
class PollExtraData(TypedDict):
243+
question: str
244+
options: List[str]
245+
246+
247+
class NewPollOption(TypedDict):
248+
type: Literal["new_option"]
249+
idx: int
250+
option: str
251+
252+
253+
class PollQuestion(TypedDict):
254+
type: Literal["question"]
255+
question: str
256+
257+
258+
class PollVote(TypedDict):
259+
type: Literal["vote"]
260+
key: str
261+
vote: Literal[1, -1]
262+
263+
264+
# Return types for widget processing functions
265+
266+
267+
class TodoTaskInfo(TypedDict):
268+
task: str
269+
desc: str
270+
completed: bool
271+
272+
273+
class TodoWidgetResult(TypedDict):
274+
title: str
275+
tasks: Dict[str, TodoTaskInfo]
276+
277+
278+
class PollOptionInfo(TypedDict):
279+
option: str
280+
votes: List[int]
281+
282+
283+
class PollWidgetResult(TypedDict):
284+
question: str
285+
options: Dict[str, PollOptionInfo]
286+
287+
288+
###############################################################################
289+
183290

184291
class Message(TypedDict, total=False):
185292
id: int
@@ -195,7 +302,7 @@ class Message(TypedDict, total=False):
195302
subject_links: List[str]
196303
is_me_message: bool
197304
reactions: List[Dict[str, Any]]
198-
submessages: List[Dict[str, Any]]
305+
submessages: List[Submessage]
199306
flags: List[MessageFlag]
200307
sender_full_name: str
201308
sender_email: str

zulipterminal/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,9 @@ def show_typing_notification(self) -> None:
451451

452452
# Until conversation becomes "inactive" like when a `stop` event is sent
453453
while self.active_conversation_info:
454-
sender_name = self.active_conversation_info["sender_name"]
454+
sender_name = self.active_conversation_info.get("sender_name")
455+
if not sender_name:
456+
break
455457
self.view.set_footer_text(
456458
[
457459
("footer_contrast", " " + sender_name + " "),

zulipterminal/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1889,7 +1889,7 @@ def _handle_submessage_event(self, event: Event) -> None:
18891889
message = self.index["messages"][message_id]
18901890
message["submessages"].append(
18911891
{
1892-
"type": event["type"],
1892+
"type": "submessage",
18931893
"msg_type": event["msg_type"],
18941894
"message_id": event["message_id"],
18951895
"submessage_id": event["submessage_id"],

zulipterminal/ui_tools/messages.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,9 @@ def main_view(self) -> List[Any]:
736736
widget_type = find_widget_type(self.message.get("submessages", []))
737737

738738
if widget_type == "todo":
739-
title, tasks = process_todo_widget(self.message.get("submessages", []))
739+
todo_result = process_todo_widget(self.message.get("submessages", []))
740+
title = todo_result["title"]
741+
tasks = todo_result["tasks"]
740742

741743
todo_widget = "<strong>To-do</strong>\n" + f"<strong>{title}</strong>"
742744

@@ -758,9 +760,9 @@ def main_view(self) -> List[Any]:
758760
self.message["content"] = todo_widget
759761

760762
elif widget_type == "poll":
761-
poll_question, poll_options = process_poll_widget(
762-
self.message.get("submessages", [])
763-
)
763+
poll_result = process_poll_widget(self.message.get("submessages", []))
764+
poll_question = poll_result["question"]
765+
poll_options = poll_result["options"]
764766

765767
# TODO: ZT doesn't yet support adding poll questions after the
766768
# creation of the poll. So, if the poll question is not provided,

zulipterminal/widget.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,34 @@
33
"""
44

55
import json
6-
from typing import Dict, List, Tuple, Union
6+
from typing import Dict, List
77

8-
9-
Submessage = Dict[str, Union[int, str]]
8+
from zulipterminal.api_types import (
9+
PollOptionInfo,
10+
PollWidgetResult,
11+
Submessage,
12+
TodoTaskInfo,
13+
TodoWidgetResult,
14+
)
1015

1116

1217
def find_widget_type(submessages: List[Submessage]) -> str:
1318
if submessages and "content" in submessages[0]:
1419
content = submessages[0]["content"]
15-
16-
if isinstance(content, str):
17-
try:
18-
loaded_content = json.loads(content)
19-
return loaded_content.get("widget_type", "unknown")
20-
except json.JSONDecodeError:
21-
return "unknown"
22-
else:
20+
try:
21+
loaded_content = json.loads(content)
22+
return loaded_content.get("widget_type", "unknown")
23+
except json.JSONDecodeError:
2324
return "unknown"
2425
else:
2526
return "unknown"
2627

2728

2829
def process_todo_widget(
2930
todo_list: List[Submessage],
30-
) -> Tuple[str, Dict[str, Dict[str, Union[str, bool]]]]:
31+
) -> TodoWidgetResult:
3132
title = ""
32-
tasks = {}
33+
tasks: Dict[str, TodoTaskInfo] = {}
3334

3435
for entry in todo_list:
3536
content = entry.get("content")
@@ -73,14 +74,14 @@ def process_todo_widget(
7374
elif widget.get("type") == "new_task_list_title":
7475
title = widget["title"]
7576

76-
return title, tasks
77+
return {"title": title, "tasks": tasks}
7778

7879

7980
def process_poll_widget(
8081
poll_content: List[Submessage],
81-
) -> Tuple[str, Dict[str, Dict[str, Union[str, List[str]]]]]:
82+
) -> PollWidgetResult:
8283
poll_question = ""
83-
options = {}
84+
options: Dict[str, PollOptionInfo] = {}
8485

8586
for entry in poll_content:
8687
content = entry["content"]
@@ -115,4 +116,4 @@ def process_poll_widget(
115116
option_id = f"{sender_id},{idx}"
116117
options[option_id] = {"option": new_option, "votes": []}
117118

118-
return poll_question, options
119+
return {"question": poll_question, "options": options}

0 commit comments

Comments
 (0)