From 03b3c421e35edc56d3da185d85b03865945fc118 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Wed, 3 Jan 2024 17:55:26 +0530 Subject: [PATCH] Resolve typing errors in membership mixin --- funnel/models/membership_mixin.py | 50 ++++++++++++++-------------- funnel/models/proposal.py | 2 +- funnel/models/proposal_membership.py | 6 ++-- funnel/models/sponsor_membership.py | 12 +++---- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/funnel/models/membership_mixin.py b/funnel/models/membership_mixin.py index aeb328e1a..f906716d2 100644 --- a/funnel/models/membership_mixin.py +++ b/funnel/models/membership_mixin.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Iterable from datetime import datetime as datetime_type from enum import ReprEnum from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, ClassVar, Generic, Self, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, Self, TypeVar from uuid import UUID from sqlalchemy import event @@ -44,9 +44,18 @@ # --- Typing --------------------------------------------------------------------------- MembershipType = TypeVar('MembershipType', bound='ImmutableMembershipMixin') -FrozenAttributionType = TypeVar( - 'FrozenAttributionType', bound='FrozenAttributionProtoMixin' -) + + +class MembershipMixinProtocol(Protocol): + _title: declared_attr[str | None] + member: declared_attr[Account] + _local_data_only: bool + + def replace(self, actor: Account, **data: Any) -> Self: + ... + + +MembershipMixinType = TypeVar('MembershipMixinType', bound=MembershipMixinProtocol) # --- Enum ----------------------------------------------------------------------------- @@ -248,14 +257,12 @@ def revoke(self, actor: Account) -> None: self.revoked_at = sa.func.utcnow() self.revoked_by = actor - def copy_template(self: MembershipType, **kwargs) -> MembershipType: + def copy_template(self, **kwargs) -> Self: """Make a copy of self for customization.""" raise NotImplementedError("Subclasses must implement copy_template") @with_roles(call={'editor'}) - def replace( - self: MembershipType, actor: Account, _accept: bool = False, **data: Any - ) -> MembershipType: + def replace(self, actor: Account, _accept: bool = False, **data: Any) -> Self: """Replace this membership record with changes to role columns.""" if self.revoked_at is not None: raise MembershipRevokedError( @@ -315,9 +322,7 @@ def amend_by(self, actor: Account): """Amend a membership in a `with` context.""" return AmendMembership(self, actor) - def merge_and_replace( - self: MembershipType, actor: Account, other: MembershipType - ) -> MembershipType: + def merge_and_replace(self, actor: Account, other: Self) -> Self: """Replace this record by merging data from an independent record.""" if self.__class__ is not other.__class__: raise TypeError("Merger requires membership records of the same type") @@ -353,7 +358,7 @@ def merge_and_replace( return replacement @with_roles(call={'member'}) - def accept(self: MembershipType, actor: Account) -> MembershipType: + def accept(self, actor: Account) -> Self: """Accept a membership invitation.""" if self.record_type != MembershipRecordTypeEnum.INVITE: raise MembershipRecordTypeError("This membership record is not an invite") @@ -362,9 +367,7 @@ def accept(self: MembershipType, actor: Account) -> MembershipType: return self.replace(actor, _accept=True) @with_roles(call={'owner', 'member'}) - def freeze_member_attribution( - self: MembershipType, actor: Account - ) -> MembershipType: + def freeze_member_attribution(self, actor: Account) -> Self: """ Freeze member attribution and return a replacement record. @@ -577,14 +580,9 @@ def parent_scoped_reorder_query_filter(self) -> ColumnElement: @declarative_mixin -class FrozenAttributionProtoMixin: +class FrozenAttributionMixin: """Provides a `title` data column and support method to freeze it.""" - if TYPE_CHECKING: - member: Mapped[Account] - replace: Callable[..., Self] - _local_data_only: bool - @declared_attr @classmethod def _title(cls) -> Mapped[str | None]: @@ -596,7 +594,7 @@ def _title(cls) -> Mapped[str | None]: ) @property - def title(self) -> str: + def title(self: MembershipMixinProtocol) -> str: """Attribution title for this record.""" if self._local_data_only: # self._title may be None when returning local data @@ -611,12 +609,14 @@ def title(self, value: str | None) -> None: self._title = value or None # Don't set empty string @property - def pickername(self) -> str: + def pickername(self: MembershipMixinProtocol) -> str: """Return member's pickername, but only if attribution isn't frozen.""" return self._title if self._title else self.member.pickername @with_roles(call={'owner', 'member'}) - def freeze_member_attribution(self, actor: Account) -> Self: + def freeze_member_attribution( + self: MembershipMixinType, actor: Account + ) -> MembershipMixinType: """Freeze member attribution and return a replacement record.""" if self._title is None: membership = self.replace(actor=actor, title=self.member.title) diff --git a/funnel/models/proposal.py b/funnel/models/proposal.py index fd808db47..8775374ac 100644 --- a/funnel/models/proposal.py +++ b/funnel/models/proposal.py @@ -254,7 +254,7 @@ class Proposal(UuidMixin, BaseScopedIdNameMixin, VideoMixin, ReorderProtoMixin, lazy='dynamic', primaryjoin=lambda: sa.and_( ProposalSponsorMembership.proposal_id == Proposal.id, - ProposalSponsorMembership.is_active, + ProposalSponsorMembership.is_active, # type: ignore[has-type] # FIXME ), order_by=lambda: ProposalSponsorMembership.seq, viewonly=True, diff --git a/funnel/models/proposal_membership.py b/funnel/models/proposal_membership.py index deebd1a8a..91d938e7c 100644 --- a/funnel/models/proposal_membership.py +++ b/funnel/models/proposal_membership.py @@ -10,7 +10,7 @@ from . import Mapped, Model, relationship, sa, sa_orm from .membership_mixin import ( - FrozenAttributionProtoMixin, + FrozenAttributionMixin, ImmutableUserMembershipMixin, ReorderMembershipProtoMixin, ) @@ -19,9 +19,9 @@ __all__ = ['ProposalMembership'] -class ProposalMembership( # type: ignore[misc] +class ProposalMembership( # type: ignore[misc] # FIXME + FrozenAttributionMixin, ImmutableUserMembershipMixin, - FrozenAttributionProtoMixin, ReorderMembershipProtoMixin, Model, ): diff --git a/funnel/models/sponsor_membership.py b/funnel/models/sponsor_membership.py index dd14c0ecb..87b491f1e 100644 --- a/funnel/models/sponsor_membership.py +++ b/funnel/models/sponsor_membership.py @@ -10,7 +10,7 @@ from . import Mapped, Model, relationship, sa, sa_orm from .membership_mixin import ( - FrozenAttributionProtoMixin, + FrozenAttributionMixin, ImmutableUserMembershipMixin, ReorderMembershipProtoMixin, ) @@ -20,9 +20,9 @@ __all__ = ['ProjectSponsorMembership', 'ProposalSponsorMembership'] -class ProjectSponsorMembership( # type: ignore[misc] +class ProjectSponsorMembership( # type: ignore[misc] # FIXME + FrozenAttributionMixin, ImmutableUserMembershipMixin, - FrozenAttributionProtoMixin, ReorderMembershipProtoMixin, Model, ): @@ -116,10 +116,10 @@ def offered_roles(self) -> set[str]: # FIXME: Replace this with existing proposal collaborator as they're now both related # to "account" -class ProposalSponsorMembership( # type: ignore[misc] - FrozenAttributionProtoMixin, - ReorderMembershipProtoMixin, +class ProposalSponsorMembership( # type: ignore[misc] # FIXME + FrozenAttributionMixin, ImmutableUserMembershipMixin, + ReorderMembershipProtoMixin, Model, ): """Sponsor of a proposal."""