Browse Source

ref(notifications): Add types to Serializers (#25433)

Marcos Gaeta 3 years ago
parent
commit
9025d6d863

+ 6 - 1
mypy.ini

@@ -1,6 +1,11 @@
 [mypy]
 python_version = 3.6
-files = src/sentry/api/serializers/models/notification_setting.py,
+files = src/sentry/api/endpoints/project_codeowners.py,
+        src/sentry/api/serializers/base.py,
+        src/sentry/api/serializers/models/integration.py,
+        src/sentry/api/serializers/models/notification_setting.py,
+        src/sentry/api/serializers/models/organization_member.py,
+        src/sentry/api/serializers/models/team.py,
         src/sentry/api/validators/notifications.py,
         src/sentry/notifications/*.py,
         src/sentry/snuba/outcomes.py,

+ 47 - 36
src/sentry/api/endpoints/project_codeowners.py

@@ -1,8 +1,10 @@
 import logging
+from typing import Any, List, Mapping, MutableMapping, Sequence, Union
 
-from rest_framework import serializers, status
-from rest_framework.exceptions import PermissionDenied
-from rest_framework.response import Response
+from rest_framework import serializers, status  # type: ignore
+from rest_framework.exceptions import PermissionDenied  # type: ignore
+from rest_framework.request import Request  # type: ignore
+from rest_framework.response import Response  # type: ignore
 
 from sentry import analytics, features
 from sentry.api.bases.project import ProjectEndpoint
@@ -13,6 +15,7 @@ from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer
 from sentry.models import (
     ExternalTeam,
     ExternalUser,
+    Project,
     ProjectCodeOwners,
     RepositoryProjectPathConfig,
     UserEmail,
@@ -23,7 +26,27 @@ from sentry.utils import metrics
 logger = logging.getLogger(__name__)
 
 
-class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
+def validate_association(
+    raw_items: Sequence[Union[UserEmail, ExternalUser, ExternalTeam]],
+    associations: Sequence[Union[UserEmail, ExternalUser, ExternalTeam]],
+    type: str,
+) -> Sequence[str]:
+    if type == "emails":
+        # associations are UserEmail objects
+        sentry_items = [item.email for item in associations]
+    else:
+        # associations can be ExternalUser or ExternalTeam objects
+        sentry_items = [item.external_name for item in associations]
+
+    diff = [item for item in raw_items if item not in sentry_items]
+
+    if len(diff):
+        return [f'The following {type} do not have an association in Sentry: {", ".join(diff)}.']
+
+    return []
+
+
+class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):  # type: ignore
     code_mapping_id = serializers.IntegerField(required=True)
     raw = serializers.CharField(required=True)
     organization_integration_id = serializers.IntegerField(required=False)
@@ -32,7 +55,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
         model = ProjectCodeOwners
         fields = ["raw", "code_mapping_id", "organization_integration_id"]
 
-    def validate(self, attrs):
+    def validate(self, attrs: Mapping[str, Any]) -> Mapping[str, Any]:
         # If it already exists, set default attrs with existing values
         if self.instance:
             attrs = {
@@ -43,7 +66,8 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
 
         if not attrs.get("raw", "").strip():
             return attrs
-        external_association_err = []
+
+        external_association_err: List[str] = []
         # Get list of team/user names from CODEOWNERS file
         teamnames, usernames, emails = parse_code_owners(attrs["raw"])
 
@@ -52,7 +76,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
             email__in=emails,
             user__sentry_orgmember_set__organization=self.context["project"].organization,
         )
-        user_emails_diff = self._validate_association(emails, user_emails, "emails")
+        user_emails_diff = validate_association(emails, user_emails, "emails")
 
         external_association_err.extend(user_emails_diff)
 
@@ -62,7 +86,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
             organizationmember__organization=self.context["project"].organization,
         )
 
-        external_users_diff = self._validate_association(usernames, external_users, "usernames")
+        external_users_diff = validate_association(usernames, external_users, "usernames")
 
         external_association_err.extend(external_users_diff)
 
@@ -72,7 +96,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
             team__organization=self.context["project"].organization,
         )
 
-        external_teams_diff = self._validate_association(teamnames, external_teams, "team names")
+        external_teams_diff = validate_association(teamnames, external_teams, "team names")
 
         external_association_err.extend(external_teams_diff)
 
@@ -98,24 +122,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
 
         return {**validated_data, **attrs}
 
-    def _validate_association(self, raw_items, associations, type):
-        if type == "emails":
-            # associations are UserEmail objects
-            sentry_items = [item.email for item in associations]
-        else:
-            # associations can be ExternalUser or ExternalTeam objects
-            sentry_items = [item.external_name for item in associations]
-
-        diff = [item for item in raw_items if item not in sentry_items]
-
-        if len(diff):
-            return [
-                f'The following {type} do not have an association in Sentry: {", ".join(diff)}.'
-            ]
-
-        return []
-
-    def validate_code_mapping_id(self, code_mapping_id):
+    def validate_code_mapping_id(self, code_mapping_id: int) -> RepositoryProjectPathConfig:
         if ProjectCodeOwners.objects.filter(
             repository_project_path_config=code_mapping_id
         ).exists() and (
@@ -131,7 +138,7 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
         except RepositoryProjectPathConfig.DoesNotExist:
             raise serializers.ValidationError("This code mapping does not exist.")
 
-    def create(self, validated_data):
+    def create(self, validated_data: MutableMapping[str, Any]) -> ProjectCodeOwners:
         # Save projectcodeowners record
         repository_project_path_config = validated_data.pop("code_mapping_id", None)
         project = self.context["project"]
@@ -141,7 +148,9 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
             **validated_data,
         )
 
-    def update(self, instance, validated_data):
+    def update(
+        self, instance: ProjectCodeOwners, validated_data: MutableMapping[str, Any]
+    ) -> ProjectCodeOwners:
         if "id" in validated_data:
             validated_data.pop("id")
         for key, value in validated_data.items():
@@ -151,12 +160,14 @@ class ProjectCodeOwnerSerializer(CamelSnakeModelSerializer):
 
 
 class ProjectCodeOwnersMixin:
-    def has_feature(self, request, project):
-        return features.has(
-            "organizations:import-codeowners", project.organization, actor=request.user
+    def has_feature(self, request: Request, project: Project) -> bool:
+        return bool(
+            features.has(
+                "organizations:import-codeowners", project.organization, actor=request.user
+            )
         )
 
-    def track_response_code(self, type, status):
+    def track_response_code(self, type: str, status: str) -> None:
         if type in ["create", "update"]:
             metrics.incr(
                 f"codeowners.{type}.http_response",
@@ -165,8 +176,8 @@ class ProjectCodeOwnersMixin:
             )
 
 
-class ProjectCodeOwnersEndpoint(ProjectEndpoint, ProjectOwnershipMixin, ProjectCodeOwnersMixin):
-    def get(self, request, project):
+class ProjectCodeOwnersEndpoint(ProjectEndpoint, ProjectOwnershipMixin, ProjectCodeOwnersMixin):  # type: ignore
+    def get(self, request: Request, project: Project) -> Response:
         """
         Retrieve List of CODEOWNERS configurations for a project
         ````````````````````````````````````````````
@@ -191,7 +202,7 @@ class ProjectCodeOwnersEndpoint(ProjectEndpoint, ProjectOwnershipMixin, ProjectC
             status.HTTP_200_OK,
         )
 
-    def post(self, request, project):
+    def post(self, request: Request, project: Project) -> Response:
         """
         Upload a CODEWONERS for project
         `````````````

+ 4 - 5
src/sentry/api/endpoints/team_notification_settings_details.py

@@ -1,6 +1,5 @@
-from typing import Any
-
 from rest_framework import status
+from rest_framework.request import Request
 from rest_framework.response import Response
 
 from sentry import features
@@ -9,7 +8,7 @@ from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models.notification_setting import NotificationSettingsSerializer
 from sentry.api.validators.notifications import validate, validate_type_option
-from sentry.models.notificationsetting import NotificationSetting
+from sentry.models import NotificationSetting, Team
 
 
 class TeamNotificationSettingsDetailsEndpoint(TeamEndpoint):
@@ -18,7 +17,7 @@ class TeamNotificationSettingsDetailsEndpoint(TeamEndpoint):
     NotificationSettings table via the API.
     """
 
-    def get(self, request: Any, team: Any) -> Response:
+    def get(self, request: Request, team: Team) -> Response:
         """
         Get the Notification Settings for a given User.
         ````````````````````````````````
@@ -42,7 +41,7 @@ class TeamNotificationSettingsDetailsEndpoint(TeamEndpoint):
             ),
         )
 
-    def put(self, request: Any, team: Any) -> Response:
+    def put(self, request: Request, team: Team) -> Response:
         """
         Update the Notification Settings for a given Team.
         ````````````````````````````````

+ 5 - 6
src/sentry/api/endpoints/user_notification_settings_details.py

@@ -1,6 +1,5 @@
-from typing import Any
-
 from rest_framework import status
+from rest_framework.request import Request
 from rest_framework.response import Response
 
 from sentry import features
@@ -9,10 +8,10 @@ from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models.notification_setting import NotificationSettingsSerializer
 from sentry.api.validators.notifications import validate, validate_type_option
-from sentry.models.notificationsetting import NotificationSetting
+from sentry.models import NotificationSetting, User
 
 
-def validate_has_feature(user: Any) -> None:
+def validate_has_feature(user: User) -> None:
     if not any(
         [
             features.has("organizations:notification-platform", organization, actor=user)
@@ -31,7 +30,7 @@ class UserNotificationSettingsDetailsEndpoint(UserEndpoint):
      be able to translate legacy values from UserOptions.
     """
 
-    def get(self, request: Any, user: Any) -> Response:
+    def get(self, request: Request, user: User) -> Response:
         """
         Get the Notification Settings for a given User.
         ````````````````````````````````
@@ -53,7 +52,7 @@ class UserNotificationSettingsDetailsEndpoint(UserEndpoint):
             ),
         )
 
-    def put(self, request: Any, user: Any) -> Response:
+    def put(self, request: Request, user: User) -> Response:
         """
         Update the Notification Settings for a given User.
         ````````````````````````````````

+ 29 - 10
src/sentry/api/serializers/base.py

@@ -1,15 +1,30 @@
-from typing import Any, Mapping, Optional, Sequence, Union
+from typing import (
+    Any,
+    Callable,
+    List,
+    Mapping,
+    MutableMapping,
+    Optional,
+    Sequence,
+    Type,
+    TypeVar,
+    Union,
+)
 
 import sentry_sdk
 from django.contrib.auth.models import AnonymousUser
 
-registry = {}
+from sentry.utils.json import JSONData
 
+K = TypeVar("K")
 
-def register(type: Any):
+registry: MutableMapping[Any, Any] = {}
+
+
+def register(type: Any) -> Callable[[Type[K]], Type[K]]:
     """ A wrapper that adds the wrapped Serializer to the Serializer registry (see above) for the key `type`. """
 
-    def wrapped(cls):
+    def wrapped(cls: Type[K]) -> Type[K]:
         registry[type] = cls()
         return cls
 
@@ -20,8 +35,8 @@ def serialize(
     objects: Union[Any, Sequence[Any]],
     user: Optional[Any] = None,
     serializer: Optional[Any] = None,
-    **kwargs,
-):
+    **kwargs: Any,
+) -> Any:
     """
     Turn a model (or list of models) into a python object made entirely of primitives.
 
@@ -73,13 +88,15 @@ def serialize(
 class Serializer:
     """ A Serializer class contains the logic to serialize a specific type of object. """
 
-    def __call__(self, obj, attrs, user, **kwargs):
+    def __call__(
+        self, obj: Any, attrs: Mapping[Any, Any], user: Any, **kwargs: Any
+    ) -> Optional[MutableMapping[str, Any]]:
         """ See documentation for `serialize`. """
         if obj is None:
-            return
+            return None
         return self.serialize(obj, attrs, user, **kwargs)
 
-    def get_attrs(self, item_list: Sequence[Any], user: Any, **kwargs) -> Mapping[Any, Any]:
+    def get_attrs(self, item_list: List[Any], user: Any, **kwargs: Any) -> MutableMapping[Any, Any]:
         """
         Fetch all of the associated data needed to serialize the objects in `item_list`.
 
@@ -90,7 +107,9 @@ class Serializer:
         """
         return {}
 
-    def serialize(self, obj: any, attrs: Mapping[Any, Any], user: Any, **kwargs):
+    def serialize(
+        self, obj: Any, attrs: Mapping[Any, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         """
         Convert an arbitrary python object `obj` to an object that only contains primitives.
 

+ 58 - 22
src/sentry/api/serializers/models/integration.py

@@ -1,23 +1,28 @@
 import logging
 from collections import defaultdict
+from typing import Any, Mapping, MutableMapping, Optional, Sequence
 
 from sentry.api.serializers import Serializer, register, serialize
+from sentry.integrations import IntegrationProvider
 from sentry.models import (
     ExternalIssue,
     ExternalTeam,
     ExternalUser,
+    Group,
     GroupLink,
     Integration,
     OrganizationIntegration,
+    User,
 )
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.types.integrations import get_provider_string
+from sentry.utils.json import JSONData
 
 logger = logging.getLogger(__name__)
 
 
 # converts the provider to JSON
-def serialize_provider(provider):
+def serialize_provider(provider: IntegrationProvider) -> Mapping[str, Any]:
     return {
         "key": provider.key,
         "slug": provider.key,
@@ -30,8 +35,10 @@ def serialize_provider(provider):
 
 
 @register(Integration)
-class IntegrationSerializer(Serializer):
-    def serialize(self, obj, attrs, user):
+class IntegrationSerializer(Serializer):  # type: ignore
+    def serialize(
+        self, obj: Integration, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         provider = obj.get_provider()
         return {
             "id": str(obj.id),
@@ -45,11 +52,20 @@ class IntegrationSerializer(Serializer):
 
 
 class IntegrationConfigSerializer(IntegrationSerializer):
-    def __init__(self, organization_id=None, params=None):
+    def __init__(
+        self, organization_id: Optional[int] = None, params: Optional[Mapping[str, Any]] = None
+    ) -> None:
         self.organization_id = organization_id
         self.params = params or {}
 
-    def serialize(self, obj, attrs, user, include_config=True):
+    def serialize(
+        self,
+        obj: Integration,
+        attrs: Mapping[str, Any],
+        user: User,
+        include_config: bool = True,
+        **kwargs: Any,
+    ) -> MutableMapping[str, JSONData]:
         data = super().serialize(obj, attrs, user)
 
         if not include_config:
@@ -76,16 +92,18 @@ class IntegrationConfigSerializer(IntegrationSerializer):
 
 
 @register(OrganizationIntegration)
-class OrganizationIntegrationSerializer(Serializer):
-    def __init__(self, params=None):
+class OrganizationIntegrationSerializer(Serializer):  # type: ignore
+    def __init__(self, params: Optional[Mapping[str, Any]] = None) -> None:
         self.params = params
 
-    def serialize(self, obj, attrs, user, include_config=True):
+    def serialize(
+        self, obj: Integration, attrs: Mapping[str, Any], user: User, include_config: bool = True
+    ) -> MutableMapping[str, JSONData]:
         # XXX(epurkhiser): This is O(n) for integrations, especially since
         # we're using the IntegrationConfigSerializer which pulls in the
         # integration installation config object which very well may be making
         # API request for config options.
-        integration = serialize(
+        integration: MutableMapping[str, Any] = serialize(
             objects=obj.integration,
             user=user,
             serializer=IntegrationConfigSerializer(obj.organization.id, params=self.params),
@@ -107,7 +125,9 @@ class OrganizationIntegrationSerializer(Serializer):
                 config_data = installation.get_config_data() if include_config else None
                 dynamic_display_information = installation.get_dynamic_display_information()
             except ApiError as e:
-                # If there is an ApiError from our 3rd party integration providers, assume there is an problem with the configuration and set it to disabled.
+                # If there is an ApiError from our 3rd party integration
+                # providers, assume there is an problem with the configuration
+                # and set it to disabled.
                 integration.update({"status": "disabled"})
                 name = "sentry.serializers.model.organizationintegration"
                 log_info = {
@@ -125,8 +145,11 @@ class OrganizationIntegrationSerializer(Serializer):
         return integration
 
 
-class IntegrationProviderSerializer(Serializer):
-    def serialize(self, obj, attrs, user, organization):
+class IntegrationProviderSerializer(Serializer):  # type: ignore
+    def serialize(
+        self, obj: Integration, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
+        org_slug = kwargs.pop("organization").slug
         metadata = obj.metadata
         metadata = metadata and metadata._asdict() or None
 
@@ -139,20 +162,25 @@ class IntegrationProviderSerializer(Serializer):
             "canDisable": obj.can_disable,
             "features": [f.value for f in obj.features],
             "setupDialog": dict(
-                url=f"/organizations/{organization.slug}/integrations/{obj.key}/setup/",
+                url=f"/organizations/{org_slug}/integrations/{obj.key}/setup/",
                 **obj.setup_dialog_config,
             ),
         }
 
 
 class IntegrationIssueConfigSerializer(IntegrationSerializer):
-    def __init__(self, group, action, params=None):
+    def __init__(
+        self, group: Group, action: str, params: Optional[Mapping[str, Any]] = None
+    ) -> None:
         self.group = group
         self.action = action
         self.params = params
 
-    def serialize(self, obj, attrs, user, organization_id=None):
+    def serialize(
+        self, obj: Integration, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         data = super().serialize(obj, attrs, user)
+        organization_id = kwargs.pop("organization_id")
         installation = obj.get_installation(organization_id)
 
         if self.action == "link":
@@ -167,10 +195,12 @@ class IntegrationIssueConfigSerializer(IntegrationSerializer):
 
 
 class IntegrationIssueSerializer(IntegrationSerializer):
-    def __init__(self, group):
+    def __init__(self, group: Group) -> None:
         self.group = group
 
-    def get_attrs(self, item_list, user, **kwargs):
+    def get_attrs(
+        self, item_list: Sequence[Integration], user: User, **kwargs: Any
+    ) -> MutableMapping[Integration, MutableMapping[str, Any]]:
         external_issues = ExternalIssue.objects.filter(
             id__in=GroupLink.objects.filter(
                 group_id=self.group.id,
@@ -203,15 +233,19 @@ class IntegrationIssueSerializer(IntegrationSerializer):
             item: {"external_issues": issues_by_integration.get(item.id, [])} for item in item_list
         }
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: Integration, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         data = super().serialize(obj, attrs, user)
         data["externalIssues"] = attrs.get("external_issues", [])
         return data
 
 
 @register(ExternalTeam)
-class ExternalTeamSerializer(Serializer):
-    def serialize(self, obj, attrs, user):
+class ExternalTeamSerializer(Serializer):  # type: ignore
+    def serialize(
+        self, obj: Any, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         provider = get_provider_string(obj.provider)
         return {
             "id": str(obj.id),
@@ -222,8 +256,10 @@ class ExternalTeamSerializer(Serializer):
 
 
 @register(ExternalUser)
-class ExternalUserSerializer(Serializer):
-    def serialize(self, obj, attrs, user):
+class ExternalUserSerializer(Serializer):  # type: ignore
+    def serialize(
+        self, obj: Any, attrs: Mapping[str, Any], user: User, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         provider = get_provider_string(obj.provider)
         return {
             "id": str(obj.id),

+ 90 - 49
src/sentry/api/serializers/models/organization_member.py

@@ -1,21 +1,66 @@
 from collections import defaultdict
+from typing import Any, List, Mapping, MutableMapping, Optional, Sequence, Set
 
 from sentry import roles
 from sentry.api.serializers import Serializer, register, serialize
-from sentry.models import ExternalUser, OrganizationMember, OrganizationMemberTeam, Team, TeamStatus
+from sentry.models import (
+    ExternalUser,
+    OrganizationMember,
+    OrganizationMemberTeam,
+    Team,
+    TeamStatus,
+    User,
+)
+from sentry.utils.json import JSONData
+
+
+def get_serialized_users_by_id(users_set: Set[User], user: User) -> Mapping[str, User]:
+    serialized_users = serialize(users_set, user)
+    return {user["id"]: user for user in serialized_users}
+
+
+def get_team_slugs_by_organization_member_id(
+    organization_members: Sequence[OrganizationMember],
+) -> Mapping[int, List[str]]:
+    """ @returns a map of member id -> team_slug[] """
+    organization_member_tuples = list(
+        OrganizationMemberTeam.objects.filter(
+            team__status=TeamStatus.VISIBLE, organizationmember__in=organization_members
+        ).values_list("organizationmember_id", "team_id")
+    )
+    team_ids_by_organization_member_id = {
+        organization_member_id: team_id
+        for organization_member_id, team_id in organization_member_tuples
+    }
+    teams = Team.objects.filter(id__in=team_ids_by_organization_member_id.values())
+    teams_by_id = {team.id: team for team in teams}
+
+    results = defaultdict(list)
+    for member_id, team_id in team_ids_by_organization_member_id.items():
+        results[member_id].append(teams_by_id[team_id].slug)
+    return results
 
 
 @register(OrganizationMember)
-class OrganizationMemberSerializer(Serializer):
-    def __init__(
-        self,
-        expand=None,
-    ):
+class OrganizationMemberSerializer(Serializer):  # type: ignore
+    def __init__(self, expand: Optional[Sequence[str]] = None) -> None:
         self.expand = expand or []
 
-    def get_attrs(self, item_list, user):
-        # TODO(dcramer): assert on relations
-        users = {d["id"]: d for d in serialize({i.user for i in item_list if i.user_id}, user)}
+    def get_attrs(
+        self, item_list: Sequence[OrganizationMember], user: User, **kwargs: Any
+    ) -> MutableMapping[OrganizationMember, MutableMapping[str, Any]]:
+        """
+        Fetch all of the associated Users and ExternalUsers needed to serialize
+        the organization_members in `item_list`.
+        TODO(dcramer): assert on relations
+        """
+
+        users_set = {
+            organization_member.user
+            for organization_member in item_list
+            if organization_member.user_id
+        }
+        users_by_id = get_serialized_users_by_id(users_set, user)
         external_users_map = defaultdict(list)
 
         if "externalUsers" in self.expand:
@@ -25,17 +70,19 @@ class OrganizationMemberSerializer(Serializer):
                 serialized = serialize(external_user, user)
                 external_users_map[external_user.organizationmember_id].append(serialized)
 
-        attrs = {
-            item: {
-                "user": users[str(item.user_id)] if item.user_id else None,
-                "externalUsers": external_users_map[item.id],
+        attrs: MutableMapping[OrganizationMember, MutableMapping[str, Any]] = {}
+        for item in item_list:
+            user = users_by_id.get(str(item.user_id), None)
+            attrs[item] = {
+                "user": user,
+                "externalUsers": external_users_map.get(item.id),
             }
-            for item in item_list
-        }
 
         return attrs
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: OrganizationMember, attrs: Mapping[str, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         d = {
             "id": str(obj.id),
             "email": obj.get_email(),
@@ -61,27 +108,14 @@ class OrganizationMemberSerializer(Serializer):
 
 
 class OrganizationMemberWithTeamsSerializer(OrganizationMemberSerializer):
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[OrganizationMember], user: User, **kwargs: Any
+    ) -> MutableMapping[OrganizationMember, MutableMapping[str, Any]]:
         attrs = super().get_attrs(item_list, user)
 
-        member_team_map = list(
-            OrganizationMemberTeam.objects.filter(
-                team__status=TeamStatus.VISIBLE, organizationmember__in=item_list
-            ).values_list("organizationmember_id", "team_id")
-        )
-
-        teams = {
-            team.id: team
-            for team in Team.objects.filter(id__in=[team_id for _, team_id in member_team_map])
-        }
-        results = defaultdict(list)
-
-        # results is a map of member id -> team_slug[]
-        for member_id, team_id in member_team_map:
-            results[member_id].append(teams[team_id].slug)
-
+        team_ids_by_organization_member_id = get_team_slugs_by_organization_member_id(item_list)
         for item in item_list:
-            teams = results.get(item.id, [])
+            teams = team_ids_by_organization_member_id.get(item.id, [])
             try:
                 attrs[item]["teams"] = teams
             except KeyError:
@@ -89,27 +123,32 @@ class OrganizationMemberWithTeamsSerializer(OrganizationMemberSerializer):
 
         return attrs
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: OrganizationMember, attrs: Mapping[str, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         d = super().serialize(obj, attrs, user)
-
         d["teams"] = attrs.get("teams", [])
-
         return d
 
 
 class OrganizationMemberWithProjectsSerializer(OrganizationMemberSerializer):
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         self.project_ids = set(kwargs.pop("project_ids", []))
         super().__init__(*args, **kwargs)
 
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[OrganizationMember], user: User, **kwargs: Any
+    ) -> MutableMapping[OrganizationMember, MutableMapping[str, Any]]:
+        """
+        Note: For this to be efficient, call
+        `.prefetch_related(
+              'teams',
+              'teams__projectteam_set',
+              'teams__projectteam_set__project',
+        )` on your queryset before using this serializer
+        """
+
         attrs = super().get_attrs(item_list, user)
-        # Note: For this to be efficient, call
-        # `.prefetch_related(
-        #       'teams',
-        #       'teams__projectteam_set',
-        #       'teams__projectteam_set__project',
-        # )` on your queryset before using this serializer
         for org_member in item_list:
             projects = set()
             for team in org_member.teams.all():
@@ -124,12 +163,14 @@ class OrganizationMemberWithProjectsSerializer(OrganizationMemberSerializer):
                     ):
                         projects.add(project_team.project.slug)
 
-            projects = list(projects)
-            projects.sort()
-            attrs[org_member]["projects"] = projects
+            projects_list = list(projects)
+            projects_list.sort()
+            attrs[org_member]["projects"] = projects_list
         return attrs
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: OrganizationMember, attrs: Mapping[str, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         d = super().serialize(obj, attrs, user)
         d["projects"] = attrs.get("projects", [])
         return d

+ 109 - 91
src/sentry/api/serializers/models/project.py

@@ -1,5 +1,6 @@
 from collections import defaultdict
 from datetime import timedelta
+from typing import Any, List, MutableMapping, Optional, Sequence
 
 import sentry_sdk
 from django.db import connection
@@ -29,6 +30,7 @@ from sentry.models import (
     ProjectStatus,
     ProjectTeam,
     Release,
+    User,
     UserReport,
 )
 from sentry.notifications.helpers import transform_to_notification_settings_by_parent_id
@@ -57,6 +59,90 @@ _PROJECT_SCOPE_PREFIX = "projects:"
 LATEST_DEPLOYS_KEY = "latestDeploys"
 
 
+def get_access_by_project(
+    projects: Sequence[Project], user: User
+) -> MutableMapping[Project, MutableMapping[str, Any]]:
+    request = env.request
+
+    project_teams = list(ProjectTeam.objects.filter(project__in=projects).select_related("team"))
+
+    project_team_map = defaultdict(list)
+
+    for pt in project_teams:
+        project_team_map[pt.project_id].append(pt.team)
+
+    team_memberships = get_team_memberships([pt.team for pt in project_teams], user)
+    org_roles = get_org_roles({i.organization_id for i in projects}, user)
+
+    is_superuser = request and is_active_superuser(request) and request.user == user
+    result = {}
+    for project in projects:
+        is_member = any(t.id in team_memberships for t in project_team_map.get(project.id, []))
+        org_role = org_roles.get(project.organization_id)
+        if is_member:
+            has_access = True
+        elif is_superuser:
+            has_access = True
+        elif project.organization.flags.allow_joinleave:
+            has_access = True
+        elif org_role and roles.get(org_role).is_global:
+            has_access = True
+        else:
+            has_access = False
+        result[project] = {"is_member": is_member, "has_access": has_access}
+    return result
+
+
+def get_features_for_projects(
+    all_projects: Sequence[Project], user: User
+) -> MutableMapping[Project, List[str]]:
+    # Arrange to call features.has_for_batch rather than features.has
+    # for performance's sake
+    projects_by_org = defaultdict(list)
+    for project in all_projects:
+        projects_by_org[project.organization].append(project)
+
+    features_by_project = defaultdict(list)
+    project_features = [
+        feature
+        for feature in features.all(feature_type=ProjectFeature).keys()
+        if feature.startswith(_PROJECT_SCOPE_PREFIX)
+    ]
+
+    batch_checked = set()
+    for (organization, projects) in projects_by_org.items():
+        batch_features = features.batch_has(
+            project_features, actor=user, projects=projects, organization=organization
+        )
+
+        # batch_has has found some features
+        if batch_features:
+            for project in projects:
+                for feature_name, active in batch_features.get(f"project:{project.id}", {}).items():
+                    if active:
+                        features_by_project[project].append(
+                            feature_name[len(_PROJECT_SCOPE_PREFIX) :]
+                        )
+
+                    batch_checked.add(feature_name)
+
+    for feature_name in project_features:
+        if feature_name in batch_checked:
+            continue
+        abbreviated_feature = feature_name[len(_PROJECT_SCOPE_PREFIX) :]
+        for (organization, projects) in projects_by_org.items():
+            result = features.has_for_batch(feature_name, organization, projects, user)
+            for (project, flag) in result.items():
+                if flag:
+                    features_by_project[project].append(abbreviated_feature)
+
+    for project in all_projects:
+        if project.flags.has_releases:
+            features_by_project[project].append("releases")
+
+    return features_by_project
+
+
 @register(Project)
 class ProjectSerializer(Serializer):
     """
@@ -64,7 +150,12 @@ class ProjectSerializer(Serializer):
     such as "show all projects for this organization", and its attributes be kept to a minimum.
     """
 
-    def __init__(self, environment_id=None, stats_period=None, transaction_stats=None):
+    def __init__(
+        self,
+        environment_id: Optional[str] = None,
+        stats_period: Optional[str] = None,
+        transaction_stats: Optional[str] = None,
+    ) -> None:
         if stats_period is not None:
             assert stats_period in STATS_PERIOD_CHOICES
 
@@ -72,40 +163,9 @@ class ProjectSerializer(Serializer):
         self.stats_period = stats_period
         self.transaction_stats = transaction_stats
 
-    def get_access_by_project(self, item_list, user):
-        request = env.request
-
-        project_teams = list(
-            ProjectTeam.objects.filter(project__in=item_list).select_related("team")
-        )
-
-        project_team_map = defaultdict(list)
-
-        for pt in project_teams:
-            project_team_map[pt.project_id].append(pt.team)
-
-        team_memberships = get_team_memberships([pt.team for pt in project_teams], user)
-        org_roles = get_org_roles([i.organization_id for i in item_list], user)
-
-        is_superuser = request and is_active_superuser(request) and request.user == user
-        result = {}
-        for project in item_list:
-            is_member = any(t.id in team_memberships for t in project_team_map.get(project.id, []))
-            org_role = org_roles.get(project.organization_id)
-            if is_member:
-                has_access = True
-            elif is_superuser:
-                has_access = True
-            elif project.organization.flags.allow_joinleave:
-                has_access = True
-            elif org_role and roles.get(org_role).is_global:
-                has_access = True
-            else:
-                has_access = False
-            result[project] = {"is_member": is_member, "has_access": has_access}
-        return result
-
-    def get_attrs(self, item_list, user, **kwargs):
+    def get_attrs(
+        self, item_list: Sequence[Project], user: User, **kwargs: Any
+    ) -> MutableMapping[Project, MutableMapping[str, Any]]:
         def measure_span(op_tag):
             span = sentry_sdk.start_span(op=f"serialize.get_attrs.project.{op_tag}")
             span.set_data("Object Count", len(item_list))
@@ -158,10 +218,10 @@ class ProjectSerializer(Serializer):
             platforms_by_project[project_id].append(platform)
 
         with measure_span("access"):
-            result = self.get_access_by_project(item_list, user)
+            result = get_access_by_project(item_list, user)
 
         with measure_span("features"):
-            features_by_project = self._get_features_for_projects(item_list, user)
+            features_by_project = get_features_for_projects(item_list, user)
             for project, serialized in result.items():
                 serialized["features"] = features_by_project[project]
 
@@ -222,56 +282,6 @@ class ProjectSerializer(Serializer):
             results[project_id] = serialized
         return results
 
-    @staticmethod
-    def _get_features_for_projects(all_projects, user):
-        # Arrange to call features.has_for_batch rather than features.has
-        # for performance's sake
-        projects_by_org = defaultdict(list)
-        for project in all_projects:
-            projects_by_org[project.organization].append(project)
-
-        features_by_project = defaultdict(list)
-        project_features = [
-            feature
-            for feature in features.all(feature_type=ProjectFeature).keys()
-            if feature.startswith(_PROJECT_SCOPE_PREFIX)
-        ]
-
-        batch_checked = set()
-        for (organization, projects) in projects_by_org.items():
-            batch_features = features.batch_has(
-                project_features, actor=user, projects=projects, organization=organization
-            )
-
-            # batch_has has found some features
-            if batch_features:
-                for project in projects:
-                    for feature_name, active in batch_features.get(
-                        f"project:{project.id}", {}
-                    ).items():
-                        if active:
-                            features_by_project[project].append(
-                                feature_name[len(_PROJECT_SCOPE_PREFIX) :]
-                            )
-
-                        batch_checked.add(feature_name)
-
-        for feature_name in project_features:
-            if feature_name in batch_checked:
-                continue
-            abbreviated_feature = feature_name[len(_PROJECT_SCOPE_PREFIX) :]
-            for (organization, projects) in projects_by_org.items():
-                result = features.has_for_batch(feature_name, organization, projects, user)
-                for (project, flag) in result.items():
-                    if flag:
-                        features_by_project[project].append(abbreviated_feature)
-
-        for project in all_projects:
-            if project.flags.has_releases:
-                features_by_project[project].append("releases")
-
-        return features_by_project
-
     def serialize(self, obj, attrs, user):
         status_label = STATUS_LABELS.get(obj.status, "unknown")
 
@@ -309,7 +319,9 @@ class ProjectSerializer(Serializer):
 
 
 class ProjectWithOrganizationSerializer(ProjectSerializer):
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[Project], user: User, **kwargs: Any
+    ) -> MutableMapping[Project, MutableMapping[str, Any]]:
         attrs = super().get_attrs(item_list, user)
 
         orgs = {d["id"]: d for d in serialize(list({i.organization for i in item_list}), user)}
@@ -324,7 +336,9 @@ class ProjectWithOrganizationSerializer(ProjectSerializer):
 
 
 class ProjectWithTeamSerializer(ProjectSerializer):
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[Project], user: User, **kwargs: Any
+    ) -> MutableMapping[Project, MutableMapping[str, Any]]:
         attrs = super().get_attrs(item_list, user)
 
         project_teams = list(
@@ -420,7 +434,9 @@ class ProjectSummarySerializer(ProjectWithTeamSerializer):
 
         return deploys_by_project
 
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[Project], user: User, **kwargs: Any
+    ) -> MutableMapping[Project, MutableMapping[str, Any]]:
         attrs = super().get_attrs(item_list, user)
 
         projects_with_user_reports = set(
@@ -588,7 +604,9 @@ class DetailedProjectSerializer(ProjectWithTeamSerializer):
         ]
     )
 
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[Project], user: User, **kwargs: Any
+    ) -> MutableMapping[Project, MutableMapping[str, Any]]:
         attrs = super().get_attrs(item_list, user)
 
         project_ids = [i.id for i in item_list]

+ 49 - 34
src/sentry/api/serializers/models/team.py

@@ -1,4 +1,5 @@
 from collections import defaultdict
+from typing import AbstractSet, Any, Iterable, Mapping, MutableMapping, Sequence, Set
 
 from django.db.models import Count
 
@@ -16,48 +17,54 @@ from sentry.models import (
     ProjectTeam,
     Team,
     TeamAvatar,
+    User,
 )
 from sentry.utils.compat import zip
+from sentry.utils.json import JSONData
 
 
-def get_team_memberships(team_list, user):
+def get_team_memberships(team_list: Sequence[Team], user: User) -> Iterable[int]:
     """Get memberships the user has in the provided team list"""
-    if user.is_authenticated():
-        return OrganizationMemberTeam.objects.filter(
-            organizationmember__user=user, team__in=team_list
-        ).values_list("team", flat=True)
-    return []
+    if not user.is_authenticated():
+        return []
+
+    team_ids: Iterable[int] = OrganizationMemberTeam.objects.filter(
+        organizationmember__user=user, team__in=team_list
+    ).values_list("team", flat=True)
+    return team_ids
 
 
-def get_member_totals(team_list, user):
+def get_member_totals(team_list: Sequence[Team], user: User) -> Mapping[str, int]:
     """Get the total number of members in each team"""
-    if user.is_authenticated():
-        query = (
-            Team.objects.filter(
-                id__in=[t.pk for t in team_list],
-                organizationmember__invite_status=InviteStatus.APPROVED.value,
-            )
-            .annotate(member_count=Count("organizationmemberteam"))
-            .values("id", "member_count")
+    if not user.is_authenticated():
+        return {}
+
+    query = (
+        Team.objects.filter(
+            id__in=[t.pk for t in team_list],
+            organizationmember__invite_status=InviteStatus.APPROVED.value,
         )
-        return {item["id"]: item["member_count"] for item in query}
-    return {}
+        .annotate(member_count=Count("organizationmemberteam"))
+        .values("id", "member_count")
+    )
+    return {item["id"]: item["member_count"] for item in query}
 
 
-def get_org_roles(org_ids, user):
+def get_org_roles(org_ids: Set[int], user: User) -> Mapping[int, str]:
     """Get the role the user has in each org"""
-    if user.is_authenticated():
-        # map of org id to role
-        return {
-            om["organization_id"]: om["role"]
-            for om in OrganizationMember.objects.filter(
-                user=user, organization__in=set(org_ids)
-            ).values("role", "organization_id")
-        }
-    return {}
+    if not user.is_authenticated():
+        return {}
+
+    # map of org id to role
+    return {
+        om["organization_id"]: om["role"]
+        for om in OrganizationMember.objects.filter(
+            user=user, organization__in=set(org_ids)
+        ).values("role", "organization_id")
+    }
 
 
-def get_access_requests(item_list, user):
+def get_access_requests(item_list: Sequence[Team], user: User) -> AbstractSet[Team]:
     if user.is_authenticated():
         return frozenset(
             OrganizationAccessRequest.objects.filter(
@@ -68,8 +75,10 @@ def get_access_requests(item_list, user):
 
 
 @register(Team)
-class TeamSerializer(Serializer):
-    def get_attrs(self, item_list, user):
+class TeamSerializer(Serializer):  # type: ignore
+    def get_attrs(
+        self, item_list: Sequence[Team], user: User, **kwargs: Any
+    ) -> MutableMapping[Team, MutableMapping[str, Any]]:
         request = env.request
         org_ids = {t.organization_id for t in item_list}
 
@@ -82,7 +91,7 @@ class TeamSerializer(Serializer):
         avatars = {a.team_id: a for a in TeamAvatar.objects.filter(team__in=item_list)}
 
         is_superuser = request and is_active_superuser(request) and request.user == user
-        result = {}
+        result: MutableMapping[Team, MutableMapping[str, Any]] = {}
 
         for team in item_list:
             is_member = team.id in memberships
@@ -106,7 +115,9 @@ class TeamSerializer(Serializer):
             }
         return result
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: Team, attrs: Mapping[str, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         if attrs.get("avatar"):
             avatar = {
                 "avatarType": attrs["avatar"].get_avatar_type_display(),
@@ -128,7 +139,9 @@ class TeamSerializer(Serializer):
 
 
 class TeamWithProjectsSerializer(TeamSerializer):
-    def get_attrs(self, item_list, user):
+    def get_attrs(
+        self, item_list: Sequence[Team], user: Any, **kwargs: Any
+    ) -> MutableMapping[Team, MutableMapping[str, Any]]:
         project_teams = list(
             ProjectTeam.objects.filter(team__in=item_list, project__status=ProjectStatus.VISIBLE)
             .order_by("project__name", "project__slug")
@@ -163,7 +176,9 @@ class TeamWithProjectsSerializer(TeamSerializer):
             result[team]["externalTeams"] = external_teams_map[team.id]
         return result
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: Team, attrs: Mapping[str, Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[str, JSONData]:
         d = super().serialize(obj, attrs, user)
         d["projects"] = attrs["projects"]
         d["externalTeams"] = attrs["externalTeams"]

+ 2 - 2
src/sentry/features/manager.py

@@ -124,7 +124,7 @@ class FeatureManager(RegisteredFeatureManager):
         except KeyError:
             raise FeatureNotRegistered(name)
 
-    def get(self, name, *args, **kwargs):
+    def get(self, name: str, *args, **kwargs):
         """
         Lookup a registered feature context scope given the feature name.
 
@@ -139,7 +139,7 @@ class FeatureManager(RegisteredFeatureManager):
         """
         self._entity_handler = handler
 
-    def has(self, name, *args, **kwargs):
+    def has(self, name: str, *args, **kwargs) -> bool:
         """
         Determine if a feature is enabled. If a handler returns None, then the next
         mechanism is used for feature checking.

Some files were not shown because too many files changed in this diff