Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for all streams #1408

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

theViz343
Copy link
Member

@theViz343 theViz343 commented Jun 8, 2023

What does this PR do, and why?

This PR adds support for unsubscribed streams and never_subscribed streams in ZT. It creates new data structures and accessor methods to access them. This PR is needed for further work on reducing stream related issues and adding support for stream (un)subscription.

Outstanding aspect(s)

External discussion & connections

How did you test this?

  • Manually - Behavioral changes
  • Manually - Visual changes
  • Adapting existing automated tests
  • Adding automated tests for new behavior (or missing tests)
  • Existing automated tests should already cover this (only a refactor of tested code)

Self-review checklist for each commit

  • It is a minimal coherent idea
  • It has a commit summary following the documented style (title & body)
  • It has a commit summary describing the motivation and reasoning for the change
  • It individually passes linting and tests
  • It contains test additions for any new behavior
  • It flows clearly from a previous branch commit, and/or prepares for the next commit

Visual changes

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.
@zulipbot zulipbot added the size: XL [Automatic label added by zulipbot] label Jun 8, 2023
This commit adds two accessor methods - _get_stream_from_id and
get_all_stream_ids. These two accessor methods help in preparation for
including specific stream property accessor methods in the next set of
commits.

Tests added.
@theViz343 theViz343 force-pushed the add-support-for-all-streams branch from 62e7272 to 76d71ca Compare June 10, 2023 11:09
This commit replaces the usage of directly indexing stream_dict for
stream "name" with the new stream property accessor method
"get_stream_name".

Test added.
…ict.

This commit replaces the usage of directly indexing stream_dict for
"subscribers" with the new stream property accessor method
"get_stream_subscribers".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"date_created" data with the new stream property accessor methods
"get_stream_date_created" and "set_stream_date_created".

Tests added.
@theViz343 theViz343 force-pushed the add-support-for-all-streams branch from 76d71ca to ec9f1d8 Compare June 10, 2023 15:12
@theViz343 theViz343 marked this pull request as ready for review June 10, 2023 15:13
@theViz343 theViz343 added PR needs review PR requires feedback to proceed PR needs buddy review labels Jun 11, 2023
Copy link
Member

@prah23 prah23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @theViz343!
This is a neat refactor; I've just left some minor points of feedback.

@@ -174,11 +175,18 @@ def __init__(self, controller: Any) -> None:
self.users = self.get_all_users()

self.stream_dict: Dict[int, Any] = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can now be typed as a Dict with int keys and Subscription values; reducing the use of Any will improve our type-checking.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in #1419 👍

@@ -1204,6 +1204,9 @@ def get_stream_date_created(self, stream_id: int) -> Optional[int]:
def set_stream_date_created(self, stream_id: int, value: Optional[int]) -> None:
self._get_stream_from_id(stream_id)["date_created"] = value

def is_stream_web_public(self, stream_id: int) -> Optional[bool]:
return self._get_stream_from_id(stream_id).get("is_web_public")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the is_web_public key is guaranteed to be present and is typed as a bool in api_types, you can index the dict instead of using get so the return type for this method can be bool, consistent with its type in the Stream class.

return self.stream_dict[stream_id]["email_address"]
elif stream_id in self._unsubscribed_streams:
return self._unsubscribed_streams[stream_id]["email_address"]
return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this method is only being used in the StreamInfoView and you haven't added the feature of viewing stream info for a never subscribed stream, can the base condition of returning None be avoided? The return value of this method is currently attached to a copy command, and having to copy a None to the clipboard doesn't seem appropriate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made it so that the method raises a RuntimeError if the stream is not a subscription (or does not exist).

theViz343 added 13 commits June 15, 2023 12:58
This commit replaces the usage of directly indexing stream_dict for
"is_web_public" data with the new stream property accessor method
"is_stream_web_public".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"message_retention_days" data with the new stream property accessor
methods "get_stream_message_retention_days" and
"set_stream_message_retention_days".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"is_invite_only" data with the new stream property accessor method
"is_stream_invite_only".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"stream_post_policy" with the new stream property accessor method
"get_stream_post_policy".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"is_announcement_only" data with the new stream property accessor method
"is_stream_announcement_only".

Test added.
…thod.

This commit replaces the usage of directly indexing stream_dict for
"history_public_to_subscribers" data with the new stream property
accessor method "is_stream_history_public_to_subscribers".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"stream_weekly_traffic" data with the new stream property
accessor method "get_stream_weekly_traffic".

Test added.
This commit replaces the usage of directly indexing stream_dict for
"rendered_description" data with the new stream property
accessor method "get_stream_rendered_description".

Test added.
This commit adds the subscription ids accessor method
"get_all_stream_ids". This helps in preparation for including
subscription color accessor method added in the next commit.

Tests added.
This commit replaces the usage of directly indexing stream_dict for
"color" data with the new stream property accessor method
"get_subscription_color".

Test added.
…ict.

This commit replaces the usage of directly indexing stream_dict for
"email_address" data with the new stream property accessor method
"get_subscription_email".

Test added.
This commit improves the typing of stream_dict by using Subscription
TypedDict instead of Dict[str, Any].

Tests updated.
This commit renames stream_dict to _subscribed_streams. This change is
necessary due to the introduction of _unsubscribed_streams and
never_subscribed streams in the model.

Tests updated.
@theViz343 theViz343 force-pushed the add-support-for-all-streams branch from ec9f1d8 to 308fa13 Compare June 15, 2023 09:41
@Subhasish-Behera Subhasish-Behera self-requested a review June 20, 2023 18:19
MODEL + "._get_stream_from_id", return_value=get_stream_from_id_fixture
)
assert model.get_stream_name(stream_id) == expected_value
model._get_stream_from_id.assert_called_once()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can assert_called_once_with be used here?

@Subhasish-Behera
Copy link
Contributor

@theViz343 Thanks for working on this! This seems to work well and was pretty easy to understand because of the commit structure and commit messages.
Apart from that, I have some minor code suggestions in the comments.

@zulipbot
Copy link
Member

Heads up @theViz343, we just merged some commits that conflict with the changes you made in this pull request! You can review this repository's recent commits to see where the conflicts occur. Please rebase your feature branch against the upstream/main branch and resolve your pull request's merge conflicts accordingly.

Copy link
Collaborator

@neiljp neiljp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theViz343 This has grown quite a lot since I last looked! I started a review of this before Hari, so apologies if this overlaps a little. The end result is great, though I think we can shrink it a little.

The PR seems to be split into two parts:

  • adding the un/never-subscribed data, improving the data types, and generalizing the current code to avoid eg. the KeyErrors
  • adding the encapsulation via the accessors, followed by the rename

Both of these parts have value, but I'd suggest grouping the first set together at the start so that we can consider them together, and so potentially merge the earlier commits in this PR first.

There are a lot of commits, particularly for the encapsulating functions, and isolating adding different methods and tests per commit is great. However, some of these functions are not used outside the model, and don't appear to be preparing for anything, or the effect is replicated in other functions already. You also have some 'setters', which we likely don't need for users of the model.

With a smaller set of new methods I think the changes will be clearer, but it may be worth discussing whether adding so many functions is the way to go, or rather using more general stream_properties and subscription_properties functions?

@@ -237,6 +226,21 @@ class Subscription(TypedDict):
# in_home_view: bool # Replaced by is_muted in Zulip 2.1; still present in updates
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a personal setting.

stream_id: int
name: str
description: str
rendered_description: str
date_created: int # NOTE: new in Zulip 4.0 / ZFL 30
date_created: Optional[int] # NOTE: new in Zulip 4.0 / ZFL 30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you find this started to fail a type check? We do set it explicitly to None!

The tricky thing here is that we reuse the existing assumed datastructures from the server for simplicity. For the purposes of loading the data it is NotRequired and is never None; once loaded it is expected to be present and may be Optional.

Maybe Union[NotRequired[int], Optional[int]]? I've not tested for validity of that syntax, and I'm unsure if it would help with our typing beyond what you have.

In any case, assuming we include the Optional[int] then we should add a comment that the None part is ZT-specific.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the part where we add the date_created key and set it to None fails the typecheck. Since we get the data from the server initially, mypy doesn't catch that date_created is not present. We could account for this by using NotRequired[Optional[int]] instead, but that would purely be for documentation since it passes the typecheck with just Optional[int]. I'll also add a comment.


is_muted: bool

role: NotRequired[int] # NOTE: new in Zulip 4.0 / ZFL 31
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems accurate, and we may want to apply NotRequired more often in general - it wasn't an option until quite recently. However, the API notes indicate it was removed again in 6.0/133, so I guess we can include it commented-out as with in_home_view. (I think it's being replaced by the user-group approach for stream management)

Like with the other change to date_created, I'd suggest mentioning these kinds of changes in the commit summary. This applies to any commit with a change which looks like only the move of code but also has minor adjustments that wouldn't necessarily be noted by a reviewer or someone looking at changes later.
(git tools can show lines moved vs changed, but GitHub doesn't :/)

However, given this and other changes in the fields, both that you've included and that seem present in the updated API docs, I'd suggest splitting those out into a separate commit - unless the changes are necessary for type-checking?

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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to refer back to my suggestion in #1387 (comment) to inline this function since we only use it once, but we may need to reinitialize these values if we lose connection while ZT is running.

Comment on lines +1210 to +1216
def get_stream_message_retention_days(self, stream_id: int) -> Optional[int]:
return self._get_stream_from_id(stream_id).get("message_retention_days")

def set_stream_message_retention_days(
self, stream_id: int, value: Optional[int]
) -> None:
self._get_stream_from_id(stream_id)["message_retention_days"] = value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accessor here has more purpose - but only if we use it to wrap accessing the .cached_retention_text data, which is what is currently accessed externally :) That will require updating the code you introduced above this.

We already have the _get_stream_from_id helper, and there's only one place it's used, and that's inside the model - so I'm unconvinced this is necessary.

Comment on lines +1218 to +1219
def is_stream_invite_only(self, stream_id: int) -> bool:
return self._get_stream_from_id(stream_id)["invite_only"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to is_web_public, we already have a method for this, which you've updated to use this accessor.

There is no use of this data outside of the model in any case, other than through stream_access_type(), so there is no motivation for introducing this accessor.

Comment on lines 1295 to 1300
if controller.model.get_stream_post_policy(self.stream_id):
stream_policy = STREAM_POST_POLICY[
controller.model.get_stream_post_policy(self.stream_id)
]
else:
if stream.get("is_announcement_only"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This existing UI code incorporates implicit checking of the server version, which would be better wrapped into the model under get_stream_post_policy, rather than the two functions introduced in this commit and the next.

@@ -174,7 +174,7 @@ def __init__(self, controller: Any) -> None:

self.users = self.get_all_users()

self.stream_dict: Dict[int, Any] = {}
self.stream_dict: Dict[int, Subscription] = {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change, and possibly others in this commit seem much more related to those earlier in the PR than here at the end?

@@ -1268,7 +1268,7 @@ class StreamInfoView(PopUpView):
def __init__(self, controller: Any, stream_id: int) -> None:
self.stream_id = stream_id
self.controller = controller
stream = controller.model.stream_dict[stream_id]
stream = controller.model._subscribed_streams[stream_id]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests we haven't covered accessing the stream or subscription properties sufficiently?

@neiljp neiljp added PR blocks other PR PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback and removed PR needs review PR requires feedback to proceed labels Jul 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has conflicts PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback PR blocks other PR size: XL [Automatic label added by zulipbot]
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants