From 29fe057222efcfddacbcd6cb3b4fab0b40c33bd1 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Wed, 31 May 2023 15:42:38 +0530 Subject: [PATCH 1/6] model/api_types: Add data structures to support non-subscribed streams. This commit creates data structures, similar to stream_dict, to support unsubscribed and never-subscribed streams. These streams are available in the fetched initial data which is used to populate the new data structures using the _register_non_subscribed_streams function. --- zulipterminal/api_types.py | 30 +++++++++++++++++------------- zulipterminal/model.py | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 8acf4d05e2..bc0fcd89c6 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -204,14 +204,14 @@ class Message(TypedDict, total=False): ############################################################################### -# In "subscriptions" response from: +# In "subscriptions", "unsubscribed", and "never_subscribed" responses from: # https://zulip.com/api/register-queue # Also directly from: # https://zulip.com/api/get-events#subscription-add # https://zulip.com/api/get-subscriptions (unused) -class Subscription(TypedDict): +class Stream(TypedDict): stream_id: int name: str description: str @@ -219,22 +219,11 @@ class Subscription(TypedDict): date_created: int # NOTE: new in Zulip 4.0 / ZFL 30 invite_only: bool subscribers: List[int] - desktop_notifications: Optional[bool] - email_notifications: Optional[bool] - wildcard_mentions_notify: Optional[bool] - push_notifications: Optional[bool] - audible_notifications: Optional[bool] - pin_to_top: bool - email_address: str - - is_muted: bool is_announcement_only: bool # Deprecated in Zulip 3.0 -> stream_post_policy stream_post_policy: int # NOTE: new in Zulip 3.0 / ZFL 1 is_web_public: bool - role: int # NOTE: new in Zulip 4.0 / ZFL 31 - color: str message_retention_days: Optional[int] # NOTE: new in Zulip 3.0 / ZFL 17 history_public_to_subscribers: bool first_message_id: Optional[int] @@ -244,6 +233,21 @@ class Subscription(TypedDict): # in_home_view: bool # Replaced by is_muted in Zulip 2.1; still present in updates +class Subscription(Stream): + desktop_notifications: Optional[bool] + email_notifications: Optional[bool] + wildcard_mentions_notify: Optional[bool] + push_notifications: Optional[bool] + audible_notifications: Optional[bool] + pin_to_top: bool + email_address: str + + is_muted: bool + + role: int # NOTE: new in Zulip 4.0 / ZFL 31 + color: str + + ############################################################################### # In "custom_profile_fields" response from: # https://zulip.com/api/register-queue diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 7073e4f055..91b5d84046 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -46,6 +46,7 @@ PrivateMessageUpdateRequest, RealmEmojiData, RealmUser, + Stream, StreamComposition, StreamMessageUpdateRequest, Subscription, @@ -177,11 +178,18 @@ def __init__(self, controller: Any) -> None: self._update_users_data_from_initial_data() self.stream_dict: Dict[int, Any] = {} + self._unsubscribed_streams: Dict[int, Subscription] = {} + self._never_subscribed_streams: Dict[int, Stream] = {} self.muted_streams: Set[int] = set() self.pinned_streams: List[StreamData] = [] self.unpinned_streams: List[StreamData] = [] self.visual_notified_streams: Set[int] = set() + self._register_non_subscribed_streams( + unsubscribed_streams=self.initial_data["unsubscribed"], + never_subscribed_streams=self.initial_data["never_subscribed"], + ) + self._subscribe_to_streams(self.initial_data["subscriptions"]) # NOTE: The date_created field of stream has been added in feature @@ -1260,6 +1268,19 @@ def user_name_from_id(self, user_id: int) -> str: return self.user_dict[user_email]["full_name"] + def _register_non_subscribed_streams( + self, + unsubscribed_streams: List[Subscription], + never_subscribed_streams: List[Stream], + ) -> None: + self._unsubscribed_streams = { + subscription["stream_id"]: subscription + for subscription in unsubscribed_streams + } + self._never_subscribed_streams = { + stream["stream_id"]: stream for stream in never_subscribed_streams + } + def _subscribe_to_streams(self, subscriptions: List[Subscription]) -> None: def make_reduced_stream_data(stream: Subscription) -> StreamData: # stream_id has been changed to id. From 27ecc64c0f0a6f8cf47f2730375864d0a3d30b8f Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Thu, 27 Jul 2023 16:52:24 +0530 Subject: [PATCH 2/6] api_types: Update Stream and Subscription typeddict fields. This commit updates the `date_created` and `role` fields present in the Stream and Subscription typeddicts respectively. For `date_created`, since servers with ZFL<30 do not include the field, but ZT does add it with a value of None to ensure consistency, the type is changed to `NotRequired[Optional[int]]`. For `role`, the field has been removed in Zulip 6.0 (ZFL 133), so it has been removed from the Subscriptions typeddict. --- zulipterminal/api_types.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index bc0fcd89c6..807bc84552 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -216,7 +216,11 @@ class Stream(TypedDict): name: str description: str rendered_description: str - date_created: int # NOTE: new in Zulip 4.0 / ZFL 30 + + # NOTE: Server data may not contain this field, in which case ZT adds it + # and sets it to None. + date_created: NotRequired[Optional[int]] + invite_only: bool subscribers: List[int] @@ -244,9 +248,11 @@ class Subscription(Stream): is_muted: bool - role: int # NOTE: new in Zulip 4.0 / ZFL 31 color: str + # Deprecated fields + # role: int # Removed in Zulip 6.0 (feature level 133) + ############################################################################### # In "custom_profile_fields" response from: From 1ced8ec95217618b3e8f4e658728dfc4cf97d8f0 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Wed, 19 Jul 2023 10:34:11 +0530 Subject: [PATCH 3/6] model: Improve typing of stream_dict. With the revamp of the Subscription TypedDict in an earlier commit, we use it to improve the typing of stream_dict in this commit. Tests and fixtures updated to support the new typing. --- tests/conftest.py | 46 ++++++++++++++++++++++++++----------- tests/core/test_core.py | 51 +++++++++++++++++++++++++++++++---------- zulipterminal/model.py | 2 +- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fe771d095c..3a862dd8e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ CustomProfileField, Message, MessageType, + Subscription, ) from zulipterminal.config.keys import ( ZT_TO_URWID_CMD_MAPPING, @@ -231,7 +232,7 @@ def logged_on_user() -> Dict[str, Any]: @pytest.fixture -def general_stream() -> Dict[str, Any]: +def general_stream() -> Subscription: return { "name": "Some general stream", "date_created": 1472091253, @@ -243,7 +244,6 @@ def general_stream() -> Dict[str, Any]: "audible_notifications": False, "description": "General Stream", "rendered_description": "General Stream", - "is_old_stream": True, "desktop_notifications": False, "stream_weekly_traffic": 0, "push_notifications": False, @@ -251,13 +251,19 @@ def general_stream() -> Dict[str, Any]: "message_retention_days": 10, "subscribers": [1001, 11, 12], "history_public_to_subscribers": True, + "is_announcement_only": False, + "stream_post_policy": 1, + "first_message_id": 1, + "email_notifications": False, + "wildcard_mentions_notify": False, + "is_web_public": False, } # This is a private stream; # only description/stream_id/invite_only/name/color vary from above @pytest.fixture -def secret_stream() -> Dict[str, Any]: +def secret_stream() -> Subscription: return { "description": "Some private stream", "stream_id": 99, @@ -270,19 +276,24 @@ def secret_stream() -> Dict[str, Any]: "color": "#ccc", # Color in '#xxx' format "is_muted": False, "audible_notifications": False, - "is_old_stream": True, "desktop_notifications": False, "stream_weekly_traffic": 0, "message_retention_days": -1, "push_notifications": False, "subscribers": [1001, 11], "history_public_to_subscribers": False, + "is_announcement_only": False, + "stream_post_policy": 1, + "first_message_id": 1, + "email_notifications": False, + "wildcard_mentions_notify": False, + "is_web_public": False, } # Like public stream but with is_web_public=True @pytest.fixture -def web_public_stream() -> Dict[str, Any]: +def web_public_stream() -> Subscription: return { "description": "Some web public stream", "stream_id": 999, @@ -295,7 +306,6 @@ def web_public_stream() -> Dict[str, Any]: "color": "#ddd", # Color in '#xxx' format "is_muted": False, "audible_notifications": False, - "is_old_stream": True, "desktop_notifications": False, "stream_weekly_traffic": 0, "message_retention_days": -1, @@ -303,15 +313,20 @@ def web_public_stream() -> Dict[str, Any]: "subscribers": [1001, 11], "history_public_to_subscribers": False, "is_web_public": True, + "is_announcement_only": False, + "stream_post_policy": 1, + "first_message_id": 1, + "email_notifications": False, + "wildcard_mentions_notify": False, } @pytest.fixture def streams_fixture( - general_stream: Dict[str, Any], - secret_stream: Dict[str, Any], - web_public_stream: Dict[str, Any], -) -> List[Dict[str, Any]]: + general_stream: Subscription, + secret_stream: Subscription, + web_public_stream: Subscription, +) -> List[Subscription]: streams = [general_stream, secret_stream, web_public_stream] for i in range(1, 3): streams.append( @@ -326,7 +341,6 @@ def streams_fixture( "audible_notifications": False, "description": f"A description of stream {i}", "rendered_description": f"A description of stream {i}", - "is_old_stream": True, "desktop_notifications": False, "stream_weekly_traffic": 0, "push_notifications": False, @@ -334,6 +348,12 @@ def streams_fixture( "email_address": f"stream{i}@example.com", "subscribers": [1001, 11, 12], "history_public_to_subscribers": True, + "is_announcement_only": False, + "stream_post_policy": 1, + "first_message_id": 1, + "email_notifications": False, + "wildcard_mentions_notify": False, + "is_web_public": False, } ) return deepcopy(streams) @@ -872,7 +892,7 @@ def clean_custom_profile_data_fixture() -> List[CustomProfileData]: def initial_data( logged_on_user: Dict[str, Any], users_fixture: List[Dict[str, Any]], - streams_fixture: List[Dict[str, Any]], + streams_fixture: List[Subscription], realm_emojis: Dict[str, Dict[str, Any]], custom_profile_fields_fixture: List[Dict[str, Union[str, int]]], ) -> Dict[str, Any]: @@ -1433,7 +1453,7 @@ def user_id(logged_on_user: Dict[str, Any]) -> int: @pytest.fixture -def stream_dict(streams_fixture: List[Dict[str, Any]]) -> Dict[int, Any]: +def stream_dict(streams_fixture: List[Subscription]) -> Dict[int, Subscription]: return {stream["stream_id"]: stream for stream in streams_fixture} diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 9f13a4061a..12897628b3 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -9,6 +9,7 @@ from pytest import param as case from pytest_mock import MockerFixture +from zulipterminal.api_types import Subscription from zulipterminal.config.themes import generate_theme from zulipterminal.core import Controller from zulipterminal.helper import Index @@ -124,6 +125,7 @@ def test_narrow_to_stream( mocker: MockerFixture, controller: Controller, index_stream: Index, + general_stream: Subscription, stream_id: int = 205, stream_name: str = "PTEST", ) -> None: @@ -131,11 +133,14 @@ def test_narrow_to_stream( controller.model.index = index_stream controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.stream_dict = { - stream_id: { + stream_id: general_stream, + } + controller.model.stream_dict[stream_id].update( + { "color": "#ffffff", "name": stream_name, } - } + ) controller.model.muted_streams = set() mocker.patch(MODEL + ".is_muted_topic", return_value=False) @@ -171,6 +176,7 @@ def test_narrow_to_topic( initial_stream_id: Optional[int], anchor: Optional[int], expected_final_focus: int, + general_stream: Subscription, stream_name: str = "PTEST", topic_name: str = "Test", stream_id: int = 205, @@ -184,11 +190,14 @@ def test_narrow_to_topic( controller.model.stream_id = initial_stream_id controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.stream_dict = { - stream_id: { + stream_id: general_stream, + } + controller.model.stream_dict[stream_id].update( + { "color": "#ffffff", "name": stream_name, } - } + ) controller.model.muted_streams = set() mocker.patch(MODEL + ".is_muted_topic", return_value=False) @@ -253,6 +262,7 @@ def test_narrow_to_all_messages( controller: Controller, index_all_messages: Index, anchor: Optional[int], + general_stream: Subscription, expected_final_focus_msg_id: int, ) -> None: controller.model.narrow = [["stream", "PTEST"]] @@ -261,10 +271,13 @@ def test_narrow_to_all_messages( controller.model.user_email = "some@email" controller.model.user_id = 1 controller.model.stream_dict = { - 205: { + 205: general_stream, + } + controller.model.stream_dict[205].update( + { "color": "#ffffff", } - } + ) controller.model.muted_streams = set() mocker.patch(MODEL + ".is_muted_topic", return_value=False) @@ -300,7 +313,11 @@ def test_narrow_to_all_pm( assert msg_ids == id_list def test_narrow_to_all_starred( - self, mocker: MockerFixture, controller: Controller, index_all_starred: Index + self, + mocker: MockerFixture, + controller: Controller, + index_all_starred: Index, + general_stream: Subscription, ) -> None: controller.model.narrow = [] controller.model.index = index_all_starred @@ -310,10 +327,13 @@ def test_narrow_to_all_starred( mocker.patch(MODEL + ".is_muted_topic", return_value=False) controller.model.user_email = "some@email" controller.model.stream_dict = { - 205: { + 205: general_stream, + } + controller.model.stream_dict[205].update( + { "color": "#ffffff", } - } + ) controller.view.message_view = mocker.patch("urwid.ListBox") controller.narrow_to_all_starred() # FIXME: Add id narrowing test @@ -327,7 +347,11 @@ def test_narrow_to_all_starred( assert msg_ids == id_list def test_narrow_to_all_mentions( - self, mocker: MockerFixture, controller: Controller, index_all_mentions: Index + self, + mocker: MockerFixture, + controller: Controller, + index_all_mentions: Index, + general_stream: Subscription, ) -> None: controller.model.narrow = [] controller.model.index = index_all_mentions @@ -337,10 +361,13 @@ def test_narrow_to_all_mentions( controller.model.user_email = "some@email" controller.model.user_id = 1 controller.model.stream_dict = { - 205: { + 205: general_stream, + } + controller.model.stream_dict[205].update( + { "color": "#ffffff", } - } + ) controller.view.message_view = mocker.patch("urwid.ListBox") controller.narrow_to_all_mentions() # FIXME: Add id narrowing test diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 91b5d84046..2f604f90d7 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -177,7 +177,7 @@ def __init__(self, controller: Any) -> None: self.users: List[MinimalUserData] = [] self._update_users_data_from_initial_data() - self.stream_dict: Dict[int, Any] = {} + self.stream_dict: Dict[int, Subscription] = {} self._unsubscribed_streams: Dict[int, Subscription] = {} self._never_subscribed_streams: Dict[int, Stream] = {} self.muted_streams: Set[int] = set() From 311725acd5cb6b07b3ffa84006124da05bf77443 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Tue, 6 Jun 2023 17:08:53 +0530 Subject: [PATCH 4/6] model: Add stream id helper methods. This commit adds two helper methods - _get_stream_from_id and _get_all_stream_ids. These two helper methods help in preparation for adding stream and subscription property accessor methods in the next commit. Tests added. --- tests/conftest.py | 58 +++++++++++++++++- tests/model/test_model.py | 121 ++++++++++++++++++++++++++++++++++++++ zulipterminal/model.py | 16 +++++ 3 files changed, 194 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3a862dd8e0..20221a3605 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ CustomProfileField, Message, MessageType, + Stream, Subscription, ) from zulipterminal.config.keys import ( @@ -359,6 +360,61 @@ def streams_fixture( return deepcopy(streams) +@pytest.fixture +def unsubscribed_streams_fixture() -> Dict[int, Subscription]: + unsubscribed_streams: Dict[int, Subscription] = {} + for i in range(3, 5): + unsubscribed_streams[i] = { + "name": f"Stream {i}", + "date_created": 1472047124 + i, + "invite_only": False, + "color": "#b0a5fd", + "pin_to_top": False, + "stream_id": i, + "is_muted": False, + "audible_notifications": False, + "description": f"A description of stream {i}", + "rendered_description": f"A description of stream {i}", + "desktop_notifications": False, + "stream_weekly_traffic": 0, + "push_notifications": False, + "message_retention_days": i + 30, + "email_address": f"stream{i}@example.com", + "email_notifications": False, + "wildcard_mentions_notify": False, + "subscribers": [1001, 11, 12], + "history_public_to_subscribers": True, + "is_announcement_only": True, + "stream_post_policy": 0, + "is_web_public": True, + "first_message_id": None, + } + return deepcopy(unsubscribed_streams) + + +@pytest.fixture +def never_subscribed_streams_fixture() -> Dict[int, Stream]: + never_subscribed_streams: Dict[int, Stream] = {} + for i in range(5, 7): + never_subscribed_streams[i] = { + "name": f"Stream {i}", + "date_created": 1472047124 + i, + "invite_only": False, + "stream_id": i, + "description": f"A description of stream {i}", + "rendered_description": f"A description of stream {i}", + "stream_weekly_traffic": 0, + "message_retention_days": i + 30, + "subscribers": [1001, 11, 12], + "history_public_to_subscribers": True, + "is_announcement_only": True, + "stream_post_policy": 0, + "is_web_public": True, + "first_message_id": None, + } + return deepcopy(never_subscribed_streams) + + @pytest.fixture def realm_emojis() -> Dict[str, Dict[str, Any]]: # Omitting source_url, author_id (server version 3.0), @@ -1453,7 +1509,7 @@ def user_id(logged_on_user: Dict[str, Any]) -> int: @pytest.fixture -def stream_dict(streams_fixture: List[Subscription]) -> Dict[int, Subscription]: +def stream_dict(streams_fixture: List[Dict[str, Any]]) -> Dict[int, Any]: return {stream["stream_id"]: stream for stream in streams_fixture} diff --git a/tests/model/test_model.py b/tests/model/test_model.py index fe2ba5535e..1e9c088a63 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1699,6 +1699,127 @@ def test__update_users_data_from_initial_data( assert model.user_dict == user_dict assert model.users == user_list + @pytest.mark.parametrize( + "stream_id, expected_value", + [ + case( + 1000, + { + "name": "Some general stream", + "date_created": None, + "invite_only": False, + "color": "#baf", + "pin_to_top": False, + "stream_id": 1000, + "is_muted": False, + "audible_notifications": False, + "description": "General Stream", + "rendered_description": "General Stream", + "desktop_notifications": False, + "stream_weekly_traffic": 0, + "push_notifications": False, + "email_address": "general@example.comm", + "message_retention_days": None, + "subscribers": [1001, 11, 12], + "history_public_to_subscribers": True, + "is_announcement_only": False, + "stream_post_policy": 1, + "first_message_id": 1, + "email_notifications": False, + "wildcard_mentions_notify": False, + "is_web_public": False, + }, + ), + case( + 3, + { + "name": "Stream 3", + "date_created": 1472047127, + "invite_only": False, + "color": "#b0a5fd", + "pin_to_top": False, + "stream_id": 3, + "is_muted": False, + "audible_notifications": False, + "description": "A description of stream 3", + "rendered_description": "A description of stream 3", + "desktop_notifications": False, + "stream_weekly_traffic": 0, + "push_notifications": False, + "message_retention_days": 33, + "email_address": "stream3@example.com", + "email_notifications": False, + "wildcard_mentions_notify": False, + "subscribers": [1001, 11, 12], + "history_public_to_subscribers": True, + "is_announcement_only": True, + "stream_post_policy": 0, + "is_web_public": True, + "first_message_id": None, + }, + ), + case( + 5, + { + "name": "Stream 5", + "date_created": 1472047129, + "invite_only": False, + "stream_id": 5, + "description": "A description of stream 5", + "rendered_description": "A description of stream 5", + "stream_weekly_traffic": 0, + "message_retention_days": 35, + "subscribers": [1001, 11, 12], + "history_public_to_subscribers": True, + "is_announcement_only": True, + "stream_post_policy": 0, + "is_web_public": True, + "first_message_id": None, + }, + ), + ], + ) + def test__get_stream_from_id( + self, + model, + stream_id, + expected_value, + stream_dict, + unsubscribed_streams_fixture, + never_subscribed_streams_fixture, + ): + model.stream_dict = stream_dict + model._unsubscribed_streams = unsubscribed_streams_fixture + model._never_subscribed_streams = never_subscribed_streams_fixture + assert model._get_stream_from_id(stream_id) == expected_value + + def test__get_stream_from_id__nonexistent_stream( + self, + model, + stream_dict, + unsubscribed_streams_fixture, + never_subscribed_streams_fixture, + stream_id=231, # id 231 does not belong to any stream + ): + model.stream_dict = stream_dict + model._unsubscribed_streams = unsubscribed_streams_fixture + model._never_subscribed_streams = never_subscribed_streams_fixture + with pytest.raises(RuntimeError): + model._get_stream_from_id(stream_id) + + def test__get_all_stream_ids( + self, + model, + stream_dict, + unsubscribed_streams_fixture, + never_subscribed_streams_fixture, + expected_value=[1000, 99, 999, 1, 2, 3, 4, 5, 6], + ): + model.stream_dict = stream_dict + model._unsubscribed_streams = unsubscribed_streams_fixture + model._never_subscribed_streams = never_subscribed_streams_fixture + assert model._get_all_stream_ids() == expected_value + @pytest.mark.parametrize("muted", powerset([99, 1000])) @pytest.mark.parametrize("visual_notification_enabled", powerset([99, 1000])) def test__subscribe_to_streams( diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 2f604f90d7..faf7619f65 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -1281,6 +1281,22 @@ def _register_non_subscribed_streams( stream["stream_id"]: stream for stream in never_subscribed_streams } + def _get_stream_from_id(self, stream_id: int) -> Union[Subscription, Stream]: + if stream_id in self.stream_dict: + return self.stream_dict[stream_id] + elif stream_id in self._unsubscribed_streams: + return self._unsubscribed_streams[stream_id] + elif stream_id in self._never_subscribed_streams: + return self._never_subscribed_streams[stream_id] + else: + raise RuntimeError(f"Stream with id {stream_id} does not exist!") + + def _get_all_stream_ids(self) -> List[int]: + id_list = list(self.stream_dict) + id_list.extend(stream_id for stream_id in self._unsubscribed_streams) + id_list.extend(stream_id for stream_id in self._never_subscribed_streams) + return id_list + def _subscribe_to_streams(self, subscriptions: List[Subscription]) -> None: def make_reduced_stream_data(stream: Subscription) -> StreamData: # stream_id has been changed to id. From 1ce1e82fe388f3b19363f7f5f826c374a87f7f1f Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Tue, 25 Jul 2023 16:21:28 +0530 Subject: [PATCH 5/6] model: Add stream and subscription property accessor functions. This commit adds stream and subscription property accessor functions to limit the mutability of stream/subscription properties where it is not needed. --- zulipterminal/model.py | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index faf7619f65..2c33ca3577 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -28,7 +28,7 @@ import zulip from bs4 import BeautifulSoup -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict from zulipterminal import unicode_emojis from zulipterminal.api_types import ( @@ -1297,6 +1297,53 @@ def _get_all_stream_ids(self) -> List[int]: id_list.extend(stream_id for stream_id in self._never_subscribed_streams) return id_list + STREAM_KEYS = Literal[ + "stream_id", + "name", + "description", + "rendered_description", + "date_created", + "invite_only", + "subscribers", + "is_announcement_only", + "stream_post_policy", + "is_web_public", + "message_retention_days", + "history_public_to_subscribers", + "first_message_id", + "stream_weekly_traffic", + ] + SUBSCRIPTION_KEYS = Literal[ + STREAM_KEYS, + "desktop_notifications", + "email_notifications", + "wildcard_mentions_notify", + "push_notifications", + "audible_notifications", + "pin_to_top", + "email_address", + "is_muted", + "color", + ] + + def stream_property( + self, stream_id: int, property: STREAM_KEYS + ) -> Optional[Union[int, str, bool, List[int]]]: + return self._get_stream_from_id(stream_id)[property] + + def subscription_property( + self, stream_id: int, property: SUBSCRIPTION_KEYS + ) -> Optional[Union[int, str, bool, List[int]]]: + subscription = self._get_stream_from_id(stream_id) + if property in subscription: + subscription = cast(Subscription, subscription) + return subscription[property] + else: + # stream_id is not a subscribed stream. + raise RuntimeError( + f"Stream with id={stream_id} does not have '{property}' property!" + ) + def _subscribe_to_streams(self, subscriptions: List[Subscription]) -> None: def make_reduced_stream_data(stream: Subscription) -> StreamData: # stream_id has been changed to id. From aa051b38f2e2daba884a927e3874c5f1c42530bf Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Mon, 24 Jul 2023 13:05:29 +0530 Subject: [PATCH 6/6] model: Use stream accessor function to access stream name. This commit replaces the direct use of stream_dict to access the 'name' property, with the stream accessor function introduced in the earlier commit. Since the new stream accessor function accesses subscribed streams, was subscribed streams and never-subscribed stream, the keyerror issue described in issue #816 is resolved. --- zulipterminal/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 2c33ca3577..746a9160e9 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -890,7 +890,7 @@ def is_muted_topic(self, stream_id: int, topic: str) -> bool: """ Returns True if topic is muted via muted_topics. """ - stream_name = self.stream_dict[stream_id]["name"] + stream_name = self.stream_property(stream_id, "name") topic_to_search = (stream_name, topic) return topic_to_search in self._muted_topics