From 76034675f710ab36cfabcaab4ce86006b2f4147b Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Sat, 20 Apr 2024 15:36:35 +0100 Subject: [PATCH 1/4] typing: modernise existing hints to Python >= 3.10 `x | y ` style --- spond/club.py | 4 +--- spond/spond.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/spond/club.py b/spond/club.py index 0beaa03..3d45e07 100644 --- a/spond/club.py +++ b/spond/club.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from .base import _SpondBase @@ -12,7 +10,7 @@ def __init__(self, username: str, password: str) -> None: @_SpondBase.require_authentication async def get_transactions( - self, club_id: str, skip: Optional[int] = None, max_items: int = 100 + self, club_id: str, skip: int | None = None, max_items: int = 100 ) -> list[dict]: """ Retrieves a list of transactions/payments for a specified club. diff --git a/spond/spond.py b/spond/spond.py index 7970969..e3ffd2a 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import TYPE_CHECKING, ClassVar from .base import _SpondBase @@ -33,7 +33,7 @@ async def login_chat(self) -> None: self.auth = result["auth"] @_SpondBase.require_authentication - async def get_groups(self) -> Optional[list[dict]]: + async def get_groups(self) -> list[dict] | None: """ Retrieve all groups, subject to authenticated user's access. @@ -118,7 +118,7 @@ async def get_person(self, user: str) -> dict: raise KeyError(errmsg) @_SpondBase.require_authentication - async def get_messages(self, max_chats: int = 100) -> Optional[list[dict]]: + async def get_messages(self, max_chats: int = 100) -> list[dict] | None: """ Retrieve messages (chats). @@ -177,9 +177,9 @@ async def _continue_chat(self, chat_id: str, text: str): async def send_message( self, text: str, - user: Optional[str] = None, - group_uid: Optional[str] = None, - chat_id: Optional[str] = None, + user: str | None = None, + group_uid: str | None = None, + chat_id: str | None = None, ): """ Start a new chat or continue an existing one. @@ -232,15 +232,15 @@ async def send_message( @_SpondBase.require_authentication async def get_events( self, - group_id: Optional[str] = None, - subgroup_id: Optional[str] = None, + group_id: str | None = None, + subgroup_id: str | None = None, include_scheduled: bool = False, - max_end: Optional[datetime] = None, - min_end: Optional[datetime] = None, - max_start: Optional[datetime] = None, - min_start: Optional[datetime] = None, + max_end: datetime | None = None, + min_end: datetime | None = None, + max_start: datetime | None = None, + min_start: datetime | None = None, max_events: int = 100, - ) -> Optional[list[dict]]: + ) -> list[dict] | None: """ Retrieve events. From e07b55c1818882dc52e9643070be9a24a7ba72ed Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:00:47 +0100 Subject: [PATCH 2/4] typing: refactor: extract `JSONDict` type alias --- spond/__init__.py | 11 ++++++++++ spond/club.py | 9 +++++++-- spond/spond.py | 51 +++++++++++++++++++++++++++-------------------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/spond/__init__.py b/spond/__init__.py index e69de29..ba98307 100644 --- a/spond/__init__.py +++ b/spond/__init__.py @@ -0,0 +1,11 @@ +import sys +from typing import Any, Dict + +if sys.version_info < (3, 10): + from typing_extensions import TypeAlias +else: + from typing import TypeAlias + + +JSONDict: TypeAlias = Dict[str, Any] +"""Simple alias for type hinting `dict`s that can be passed to/from JSON-handling functions.""" diff --git a/spond/club.py b/spond/club.py index 3d45e07..09e062c 100644 --- a/spond/club.py +++ b/spond/club.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from .base import _SpondBase +if TYPE_CHECKING: + from . import JSONDict + class SpondClub(_SpondBase): def __init__(self, username: str, password: str) -> None: @@ -11,7 +16,7 @@ def __init__(self, username: str, password: str) -> None: @_SpondBase.require_authentication async def get_transactions( self, club_id: str, skip: int | None = None, max_items: int = 100 - ) -> list[dict]: + ) -> list[JSONDict]: """ Retrieves a list of transactions/payments for a specified club. @@ -31,7 +36,7 @@ async def get_transactions( Returns ------- - list of dict + list[JSONDict] A list of transactions, each represented as a dictionary. """ if self.transactions is None: diff --git a/spond/spond.py b/spond/spond.py index e3ffd2a..280af0d 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -9,6 +9,8 @@ if TYPE_CHECKING: from datetime import datetime + from . import JSONDict + class Spond(_SpondBase): @@ -33,13 +35,13 @@ async def login_chat(self) -> None: self.auth = result["auth"] @_SpondBase.require_authentication - async def get_groups(self) -> list[dict] | None: + async def get_groups(self) -> list[JSONDict] | None: """ Retrieve all groups, subject to authenticated user's access. Returns ------- - list[dict] or None + list[JSONDict] or None A list of groups, each represented as a dictionary, or None if no groups are available. @@ -49,7 +51,7 @@ async def get_groups(self) -> list[dict] | None: self.groups = await r.json() return self.groups - async def get_group(self, uid: str) -> dict: + async def get_group(self, uid: str) -> JSONDict: """ Get a group by unique ID. Subject to authenticated user's access. @@ -61,7 +63,8 @@ async def get_group(self, uid: str) -> dict: Returns ------- - Details of the group. + JSONDict + Details of the group. Raises ------ @@ -71,7 +74,7 @@ async def get_group(self, uid: str) -> dict: return await self._get_entity(self._GROUP, uid) @_SpondBase.require_authentication - async def get_person(self, user: str) -> dict: + async def get_person(self, user: str) -> JSONDict: """ Get a member or guardian by matching various identifiers. Subject to authenticated user's access. @@ -84,7 +87,8 @@ async def get_person(self, user: str) -> dict: Returns ------- - Member or guardian's details. + JSONDict + Member or guardian's details. Raises ------ @@ -118,7 +122,7 @@ async def get_person(self, user: str) -> dict: raise KeyError(errmsg) @_SpondBase.require_authentication - async def get_messages(self, max_chats: int = 100) -> list[dict] | None: + async def get_messages(self, max_chats: int = 100) -> list[JSONDict] | None: """ Retrieve messages (chats). @@ -131,7 +135,7 @@ async def get_messages(self, max_chats: int = 100) -> list[dict] | None: Returns ------- - list[dict] or None + list[JSONDict] or None A list of chats, each represented as a dictionary, or None if no chats are available. @@ -148,7 +152,7 @@ async def get_messages(self, max_chats: int = 100) -> list[dict] | None: return self.messages @_SpondBase.require_authentication - async def _continue_chat(self, chat_id: str, text: str): + async def _continue_chat(self, chat_id: str, text: str) -> JSONDict: """ Send a given text in an existing given chat. Subject to authenticated user's access. @@ -163,7 +167,7 @@ async def _continue_chat(self, chat_id: str, text: str): Returns ------- - dict + JSONDict Result of the sending. """ if not self.auth: @@ -240,7 +244,7 @@ async def get_events( max_start: datetime | None = None, min_start: datetime | None = None, max_events: int = 100, - ) -> list[dict] | None: + ) -> list[JSONDict] | None: """ Retrieve events. @@ -278,7 +282,7 @@ async def get_events( Returns ------- - list[dict] or None + list[JSONDict] or None A list of events, each represented as a dictionary, or None if no events are available. @@ -307,7 +311,7 @@ async def get_events( self.events = await r.json() return self.events - async def get_event(self, uid: str) -> dict: + async def get_event(self, uid: str) -> JSONDict: """ Get an event by unique ID. Subject to authenticated user's access. @@ -319,7 +323,8 @@ async def get_event(self, uid: str) -> dict: Returns ------- - Details of the event. + JSONDict + Details of the event. Raises ------ @@ -329,7 +334,7 @@ async def get_event(self, uid: str) -> dict: return await self._get_entity(self._EVENT, uid) @_SpondBase.require_authentication - async def update_event(self, uid: str, updates: dict): + async def update_event(self, uid: str, updates: JSONDict): """ Updates an existing event. @@ -337,7 +342,7 @@ async def update_event(self, uid: str, updates: dict): ---------- uid : str UID of the event. - updates : dict + updates : JSONDict The changes. e.g. if you want to change the description -> {'description': "New Description with changes"} Returns @@ -353,7 +358,7 @@ async def update_event(self, uid: str, updates: dict): url = f"{self.api_url}sponds/{uid}" - base_event: dict = { + base_event: JSONDict = { "heading": None, "description": None, "spondType": "EVENT", @@ -425,7 +430,7 @@ async def get_event_attendance_xlsx(self, uid: str) -> bytes: return output_data @_SpondBase.require_authentication - async def change_response(self, uid: str, user: str, payload: dict) -> dict: + async def change_response(self, uid: str, user: str, payload: JSONDict) -> JSONDict: """change a user's response for an event Parameters @@ -436,12 +441,13 @@ async def change_response(self, uid: str, user: str, payload: dict) -> dict: user : str UID of the user - payload : dict + payload : JSONDict user response to event, e.g. {"accepted": "true"} Returns ------- - json: event["responses"] with updated info + JSONDict + event["responses"] with updated info """ url = f"{self.api_url}sponds/{uid}/responses/{user}" async with self.clientsession.put( @@ -450,7 +456,7 @@ async def change_response(self, uid: str, user: str, payload: dict) -> dict: return await r.json() @_SpondBase.require_authentication - async def _get_entity(self, entity_type: str, uid: str) -> dict: + async def _get_entity(self, entity_type: str, uid: str) -> JSONDict: """ Get an event or group by unique ID. @@ -465,7 +471,8 @@ async def _get_entity(self, entity_type: str, uid: str) -> dict: Returns ------- - Details of the entity. + JSONDict + Details of the entity. Raises ------ From 337fdac522b353e66cea6da7621815fe37fd4cda Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:25:49 +0100 Subject: [PATCH 3/4] typing: add `list[JSONDict] | None` hint to `Spond`, `SpondClub` attributes; test parameters and fixture returns --- manual_test_functions.py | 12 ++++++++---- spond/club.py | 2 +- spond/spond.py | 6 +++--- tests/test_spond.py | 29 +++++++++++++++++++++-------- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/manual_test_functions.py b/manual_test_functions.py index 2dde9ad..5b51a67 100644 --- a/manual_test_functions.py +++ b/manual_test_functions.py @@ -7,10 +7,14 @@ import asyncio import tempfile +from typing import TYPE_CHECKING from config import club_id, password, username from spond import club, spond +if TYPE_CHECKING: + from spond import JSONDict + DUMMY_ID = "DUMMY_ID" MAX_EVENTS = 10 @@ -70,11 +74,11 @@ async def main() -> None: await sc.clientsession.close() -def _group_summary(group) -> str: +def _group_summary(group: JSONDict) -> str: return f"id='{group['id']}', " f"name='{group['name']}'" -def _event_summary(event) -> str: +def _event_summary(event: JSONDict) -> str: return ( f"id='{event['id']}', " f"heading='{event['heading']}', " @@ -82,7 +86,7 @@ def _event_summary(event) -> str: ) -def _chat_summary(chat) -> str: +def _chat_summary(chat: JSONDict) -> str: msg_text = chat["message"].get("text", "") return ( f"id='{chat['id']}', " @@ -91,7 +95,7 @@ def _chat_summary(chat) -> str: ) -def _transaction_summary(transaction) -> str: +def _transaction_summary(transaction: JSONDict) -> str: return ( f"id='{transaction['id']}', " f"timestamp='{transaction['paidAt']}', " diff --git a/spond/club.py b/spond/club.py index 09e062c..5a5aa18 100644 --- a/spond/club.py +++ b/spond/club.py @@ -11,7 +11,7 @@ class SpondClub(_SpondBase): def __init__(self, username: str, password: str) -> None: super().__init__(username, password, "https://api.spond.com/club/v1/") - self.transactions = None + self.transactions: list[JSONDict] | None = None @_SpondBase.require_authentication async def get_transactions( diff --git a/spond/spond.py b/spond/spond.py index 280af0d..68c0bdc 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -23,9 +23,9 @@ def __init__(self, username: str, password: str) -> None: super().__init__(username, password, "https://api.spond.com/core/v1/") self.chat_url = None self.auth = None - self.groups = None - self.events = None - self.messages = None + self.groups: list[JSONDict] | None = None + self.events: list[JSONDict] | None = None + self.messages: list[JSONDict] | None = None async def login_chat(self) -> None: api_chat_url = f"{self.api_url}chat" diff --git a/tests/test_spond.py b/tests/test_spond.py index 3a38a6f..289d40c 100644 --- a/tests/test_spond.py +++ b/tests/test_spond.py @@ -1,5 +1,8 @@ """Test suite for Spond class.""" +from __future__ import annotations + +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, patch import pytest @@ -7,6 +10,10 @@ from spond.base import _SpondBase from spond.spond import Spond +if TYPE_CHECKING: + from spond import JSONDict + + MOCK_USERNAME, MOCK_PASSWORD = "MOCK_USERNAME", "MOCK_PASSWORD" MOCK_TOKEN = "MOCK_TOKEN" MOCK_PAYLOAD = {"accepted": "false", "declineMessage": "sick cannot make it"} @@ -36,7 +43,7 @@ def mock_payload(): class TestEventMethods: @pytest.fixture - def mock_events(self): + def mock_events(self) -> list[JSONDict]: """Mock a minimal list of events.""" return [ { @@ -50,7 +57,7 @@ def mock_events(self): ] @pytest.mark.asyncio - async def test_get_event__happy_path(self, mock_events, mock_token): + async def test_get_event__happy_path(self, mock_events: list[JSONDict], mock_token): """Test that a valid `id` returns the matching event.""" s = Spond(MOCK_USERNAME, MOCK_PASSWORD) @@ -64,7 +71,9 @@ async def test_get_event__happy_path(self, mock_events, mock_token): } @pytest.mark.asyncio - async def test_get_event__no_match_raises_exception(self, mock_events, mock_token): + async def test_get_event__no_match_raises_exception( + self, mock_events: list[JSONDict], mock_token + ): """Test that a non-matched `id` raises KeyError.""" s = Spond(MOCK_USERNAME, MOCK_PASSWORD) @@ -76,7 +85,7 @@ async def test_get_event__no_match_raises_exception(self, mock_events, mock_toke @pytest.mark.asyncio async def test_get_event__blank_id_match_raises_exception( - self, mock_events, mock_token + self, mock_events: list[JSONDict], mock_token ): """Test that a blank `id` raises KeyError.""" @@ -122,7 +131,7 @@ async def test_change_response(self, mock_put, mock_payload, mock_token): class TestGroupMethods: @pytest.fixture - def mock_groups(self): + def mock_groups(self) -> list[JSONDict]: """Mock a minimal list of groups.""" return [ { @@ -136,7 +145,7 @@ def mock_groups(self): ] @pytest.mark.asyncio - async def test_get_group__happy_path(self, mock_groups, mock_token): + async def test_get_group__happy_path(self, mock_groups: list[JSONDict], mock_token): """Test that a valid `id` returns the matching group.""" s = Spond(MOCK_USERNAME, MOCK_PASSWORD) @@ -150,7 +159,9 @@ async def test_get_group__happy_path(self, mock_groups, mock_token): } @pytest.mark.asyncio - async def test_get_group__no_match_raises_exception(self, mock_groups, mock_token): + async def test_get_group__no_match_raises_exception( + self, mock_groups: list[JSONDict], mock_token + ): """Test that a non-matched `id` raises KeyError.""" s = Spond(MOCK_USERNAME, MOCK_PASSWORD) @@ -161,7 +172,9 @@ async def test_get_group__no_match_raises_exception(self, mock_groups, mock_toke await s.get_group("ID3") @pytest.mark.asyncio - async def test_get_group__blank_id_raises_exception(self, mock_groups, mock_token): + async def test_get_group__blank_id_raises_exception( + self, mock_groups: list[JSONDict], mock_token + ): """Test that a blank `id` raises KeyError.""" s = Spond(MOCK_USERNAME, MOCK_PASSWORD) From 4c11e77ec612413409825a3681c43a5ebbccbe66 Mon Sep 17 00:00:00 2001 From: Elliot <3186037+elliot-100@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:23:45 +0100 Subject: [PATCH 4/4] lint: fix indent issue --- spond/spond.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spond/spond.py b/spond/spond.py index 68c0bdc..a5431ef 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -282,9 +282,9 @@ async def get_events( Returns ------- - list[JSONDict] or None - A list of events, each represented as a dictionary, or None if no events - are available. + list[JSONDict] or None + A list of events, each represented as a dictionary, or None if no events + are available. """ url = f"{self.api_url}sponds/"