From 0ecc367178e26a1bce92321147ae4fac68c6ac56 Mon Sep 17 00:00:00 2001
From: Quantum <quantum2048@gmail.com>
Date: Sun, 27 Sep 2020 00:53:23 -0400
Subject: [PATCH] More mypy annotations

---
 .github/workflows/build.yml |  2 +-
 win2xcur/main.py            |  2 +-
 win2xcur/parser/__init__.py |  7 +++++--
 win2xcur/parser/ani.py      | 17 ++++++++++-------
 win2xcur/parser/base.py     | 18 ++++++++++++++++++
 win2xcur/parser/cur.py      | 12 +++++++-----
 win2xcur/shadow.py          |  2 +-
 7 files changed, 43 insertions(+), 17 deletions(-)
 create mode 100644 win2xcur/parser/base.py

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index efff5ed..be7d8c6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ 3.5, 3.6, 3.7, 3.8 ]
+        python-version: [ 3.6, 3.7, 3.8, 3.9.0-alpha - 3.9 ]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
diff --git a/win2xcur/main.py b/win2xcur/main.py
index 302bc42..e09582a 100644
--- a/win2xcur/main.py
+++ b/win2xcur/main.py
@@ -37,7 +37,7 @@ def main() -> None:
 
     check_xcursorgen()
 
-    def process(file):
+    def process(file) -> None:
         name = file.name
         blob = file.read()
         try:
diff --git a/win2xcur/parser/__init__.py b/win2xcur/parser/__init__.py
index 7071064..ec03937 100644
--- a/win2xcur/parser/__init__.py
+++ b/win2xcur/parser/__init__.py
@@ -1,10 +1,13 @@
+from typing import List, Type
+
 from win2xcur.parser.ani import ANIParser
+from win2xcur.parser.base import BaseParser
 from win2xcur.parser.cur import CURParser
 
-PARSERS = [CURParser, ANIParser]
+PARSERS: List[Type[BaseParser]] = [CURParser, ANIParser]
 
 
-def open_blob(blob):
+def open_blob(blob: bytes) -> BaseParser:
     for parser in PARSERS:
         if parser.can_parse(blob):
             return parser(blob)
diff --git a/win2xcur/parser/ani.py b/win2xcur/parser/ani.py
index 6e1a1da..ba1a08f 100644
--- a/win2xcur/parser/ani.py
+++ b/win2xcur/parser/ani.py
@@ -1,10 +1,13 @@
 import struct
 from copy import copy
+from typing import Any, Iterable, List, Tuple
 
+from win2xcur.cursor import CursorFrame
+from win2xcur.parser.base import BaseParser
 from win2xcur.parser.cur import CURParser
 
 
-class ANIParser:
+class ANIParser(BaseParser):
     SIGNATURE = b'RIFF'
     ANI_TYPE = b'ACON'
     FRAME_TYPE = b'fram'
@@ -16,26 +19,26 @@ class ANIParser:
     ICON_FLAG = 0x1
 
     @classmethod
-    def can_parse(cls, blob):
+    def can_parse(cls, blob: bytes) -> bool:
         signature, size, subtype = cls.RIFF_HEADER.unpack(blob[:cls.RIFF_HEADER.size])
         return signature == cls.SIGNATURE and size == len(blob) - 8 and subtype == cls.ANI_TYPE
 
-    def __init__(self, blob):
-        self.blob = blob
+    def __init__(self, blob: bytes) -> None:
+        super().__init__(blob)
         if not self.can_parse(blob):
             raise ValueError('Not a .ani file')
         self.frames = self._parse(self.RIFF_HEADER.size)
 
-    def _unpack(self, struct_cls, offset):
+    def _unpack(self, struct_cls: struct.Struct, offset: int) -> Tuple[Any, ...]:
         return struct_cls.unpack(self.blob[offset:offset + struct_cls.size])
 
-    def _read_chunk(self, offset, expected):
+    def _read_chunk(self, offset: int, expected: Iterable[bytes]) -> Tuple[int, int]:
         name, size = self._unpack(self.CHUNK_HEADER, offset)
         if name not in expected:
             raise ValueError('Expected chunk %r, found %r' % (expected, name))
         return size, offset + self.CHUNK_HEADER.size
 
-    def _parse(self, offset):
+    def _parse(self, offset: int) -> List[CursorFrame]:
         size, offset = self._read_chunk(offset, expected=[b'anih'])
 
         if size != self.ANIH_HEADER.size:
diff --git a/win2xcur/parser/base.py b/win2xcur/parser/base.py
new file mode 100644
index 0000000..ee3fd7d
--- /dev/null
+++ b/win2xcur/parser/base.py
@@ -0,0 +1,18 @@
+from abc import ABCMeta, abstractmethod
+from typing import List
+
+from win2xcur.cursor import CursorFrame
+
+
+class BaseParser(metaclass=ABCMeta):
+    blob: bytes
+    frames: List[CursorFrame]
+
+    @abstractmethod
+    def __init__(self, blob: bytes) -> None:
+        self.blob = blob
+
+    @classmethod
+    @abstractmethod
+    def can_parse(cls, blob: bytes) -> bool:
+        raise NotImplementedError()
diff --git a/win2xcur/parser/cur.py b/win2xcur/parser/cur.py
index 07e113e..0f7035e 100644
--- a/win2xcur/parser/cur.py
+++ b/win2xcur/parser/cur.py
@@ -1,28 +1,30 @@
 import struct
+from typing import List, Tuple
 
 from wand.image import Image
 
 from win2xcur.cursor import CursorFrame, CursorImage
+from win2xcur.parser.base import BaseParser
 
 
-class CURParser:
+class CURParser(BaseParser):
     MAGIC = b'\0\0\02\0'
     ICON_DIR = struct.Struct('<HHH')
     ICON_DIR_ENTRY = struct.Struct('<BBBBHHII')
 
     @classmethod
-    def can_parse(cls, blob):
+    def can_parse(cls, blob: bytes) -> bool:
         return blob[:len(cls.MAGIC)] == cls.MAGIC
 
-    def __init__(self, blob):
-        self.blob = blob
+    def __init__(self, blob: bytes) -> None:
+        super().__init__(blob)
         self._image = Image(blob=blob, format='cur')
         self._hotspots = self._parse_header()
         self.frames = [CursorFrame([
             CursorImage(image, hotspot) for image, hotspot in zip(self._image.sequence, self._hotspots)
         ])]
 
-    def _parse_header(self):
+    def _parse_header(self) -> List[Tuple[int, int]]:
         reserved, ico_type, image_count = self.ICON_DIR.unpack(self.blob[:self.ICON_DIR.size])
         assert reserved == 0
         assert ico_type == 2
diff --git a/win2xcur/shadow.py b/win2xcur/shadow.py
index 6d32340..0e97661 100644
--- a/win2xcur/shadow.py
+++ b/win2xcur/shadow.py
@@ -22,7 +22,7 @@ def apply_to_image(image: BaseImage, *, color: str, radius: float, sigma: float,
     return result
 
 
-def apply_to_frames(frames: List[CursorFrame], **kwargs):
+def apply_to_frames(frames: List[CursorFrame], **kwargs) -> None:
     for frame in frames:
         for cursor in frame:
             cursor.image = apply_to_image(cursor.image, **kwargs)