From 9017efc7beb5e74bfcba32bf40386d4ff682c4e1 Mon Sep 17 00:00:00 2001 From: "Mike A." Date: Sun, 3 Aug 2025 21:42:08 +0200 Subject: [PATCH] refactor: specialize `Serializable` subclasses for stronger type safety --- findmy/accessory.py | 2 +- findmy/keys.py | 2 +- findmy/reports/account.py | 2 +- findmy/reports/anisette.py | 4 ++-- findmy/reports/reports.py | 2 +- findmy/util/abc.py | 11 ++++++----- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/findmy/accessory.py b/findmy/accessory.py index 2082165..544170a 100644 --- a/findmy/accessory.py +++ b/findmy/accessory.py @@ -82,7 +82,7 @@ class RollingKeyPairSource(ABC): return keys -class FindMyAccessory(RollingKeyPairSource, Serializable): +class FindMyAccessory(RollingKeyPairSource, Serializable[FindMyAccessoryMapping]): """A findable Find My-accessory using official key rollover.""" def __init__( # noqa: PLR0913 diff --git a/findmy/keys.py b/findmy/keys.py index 0db2f6b..76b849f 100644 --- a/findmy/keys.py +++ b/findmy/keys.py @@ -127,7 +127,7 @@ class HasPublicKey(HasHashedPublicKey, ABC): ) -class KeyPair(HasPublicKey, Serializable): +class KeyPair(HasPublicKey, Serializable[KeyPairMapping]): """A private-public keypair for a trackable FindMy accessory.""" def __init__( diff --git a/findmy/reports/account.py b/findmy/reports/account.py index 0d7460a..343bcfd 100644 --- a/findmy/reports/account.py +++ b/findmy/reports/account.py @@ -141,7 +141,7 @@ def _extract_phone_numbers(html: str) -> list[dict]: return data.get("direct", {}).get("phoneNumberVerification", {}).get("trustedPhoneNumbers", []) -class BaseAppleAccount(Closable, Serializable, ABC): +class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC): """Base class for an Apple account.""" @property diff --git a/findmy/reports/anisette.py b/findmy/reports/anisette.py index 2fb0f87..3c5efaf 100644 --- a/findmy/reports/anisette.py +++ b/findmy/reports/anisette.py @@ -188,7 +188,7 @@ class BaseAnisetteProvider(Closable, Serializable, ABC): return cpd -class RemoteAnisetteProvider(BaseAnisetteProvider): +class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMapping]): """Anisette provider. Fetches headers from a remote Anisette server.""" _ANISETTE_DATA_VALID_FOR = 30 @@ -269,7 +269,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider): await self._http.close() -class LocalAnisetteProvider(BaseAnisetteProvider): +class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapping]): """Anisette provider. Generates headers without a remote server using the `anisette` library.""" def __init__( diff --git a/findmy/reports/reports.py b/findmy/reports/reports.py index cd3dd59..68c97f3 100644 --- a/findmy/reports/reports.py +++ b/findmy/reports/reports.py @@ -51,7 +51,7 @@ class LocationReportDecryptedMapping(TypedDict): LocationReportMapping = Union[LocationReportEncryptedMapping, LocationReportDecryptedMapping] -class LocationReport(HasHashedPublicKey, Serializable): +class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]): """Location report corresponding to a certain `HasHashedPublicKey`.""" def __init__( diff --git a/findmy/util/abc.py b/findmy/util/abc.py index 7b77d3e..a88da3b 100644 --- a/findmy/util/abc.py +++ b/findmy/util/abc.py @@ -5,12 +5,13 @@ from __future__ import annotations import asyncio import logging from abc import ABC, abstractmethod +from collections.abc import Mapping from typing import TYPE_CHECKING, Generic, Self, TypeVar if TYPE_CHECKING: from pathlib import Path -logging.getLogger(__name__) +logger = logging.getLogger(__name__) class Closable(ABC): @@ -42,14 +43,14 @@ class Closable(ABC): pass -T = TypeVar("T", bound=dict) +_T = TypeVar("_T", bound=Mapping) -class Serializable(Generic[T], ABC): +class Serializable(Generic[_T], ABC): """ABC for serializable classes.""" @abstractmethod - def to_json(self, dst: str | Path | None = None, /) -> T: + def to_json(self, dst: str | Path | None = None, /) -> _T: """ Export the current state of the object as a JSON-serializable dictionary. @@ -66,7 +67,7 @@ class Serializable(Generic[T], ABC): @classmethod @abstractmethod - def from_json(cls, val: str | Path | T, /) -> Self: + def from_json(cls, val: str | Path | _T, /) -> Self: """ Restore state from a previous `Closable.to_json` export.