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(' 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)