diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml index bc2e492..e6581f4 100644 --- a/.github/workflows/type-check.yml +++ b/.github/workflows/type-check.yml @@ -19,3 +19,20 @@ jobs: run: pip install mypy - name: Run mypy run: mypy . + pyre: + name: pyre + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install requirements + run: pip install -r requirements.txt + - name: Install test requirements + run: pip install -r test-requirements.txt + - name: Install pyre + run: pip install pyre + - name: Run pyre + run: pyre check diff --git a/.pyre_configuration b/.pyre_configuration index 9573b80..cf73b56 100644 --- a/.pyre_configuration +++ b/.pyre_configuration @@ -1,5 +1,7 @@ { "source_directories": [ "." - ] + ], + "site_package_search_strategy": "all", + "strict": true } diff --git a/generate.py b/generate.py index 884b55b..dafcfc5 100755 --- a/generate.py +++ b/generate.py @@ -8,7 +8,7 @@ import asyncio import logging from pathlib import Path -from typing import Any, Coroutine, List +from typing import Any, Awaitable, Callable, Coroutine, List # https://github.com/kerrickstaley/genanki import genanki # type: ignore @@ -64,7 +64,7 @@ class LeetcodeNote(genanki.Note): """ @property - def guid(self): + def guid(self) -> str: # Hash by leetcode task handle return genanki.guid_for(self.fields[0]) @@ -179,7 +179,7 @@ async def generate( start, stop, page_size, list_id ) - note_generators: List[Coroutine[Any, Any, LeetcodeNote]] = [] + note_generators: List[Awaitable[LeetcodeNote]] = [] task_handles = await leetcode_data.all_problems_handles() @@ -212,5 +212,5 @@ async def main() -> None: if __name__ == "__main__": - loop = asyncio.get_event_loop() + loop: asyncio.events.AbstractEventLoop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/leetcode_anki/helpers/leetcode.py b/leetcode_anki/helpers/leetcode.py index b5486d0..f4ba87b 100644 --- a/leetcode_anki/helpers/leetcode.py +++ b/leetcode_anki/helpers/leetcode.py @@ -6,7 +6,7 @@ import os import time from functools import cached_property -from typing import Callable, Dict, List, Tuple, Type +from typing import Any, Callable, Dict, List, Tuple, Type, TypeVar # https://github.com/prius/python-leetcode import leetcode.api.default_api # type: ignore @@ -49,16 +49,28 @@ def _get_leetcode_api_client() -> leetcode.api.default_api.DefaultApi: return api_instance -def retry(times: int, exceptions: Tuple[Type[Exception]], delay: float) -> Callable: - """ - Retry Decorator - Retries the wrapped function/method `times` times if the exceptions listed - in `exceptions` are thrown - """ +_T = TypeVar("_T") + + +class _RetryDecorator: + _times: int + _exceptions: Tuple[Type[Exception]] + _delay: float + + def __init__( + self, times: int, exceptions: Tuple[Type[Exception]], delay: float + ) -> None: + self._times = times + self._exceptions = exceptions + self._delay = delay + + def __call__(self, func: Callable[..., _T]) -> Callable[..., _T]: + times: int = self._times + exceptions: Tuple[Type[Exception]] = self._exceptions + delay: float = self._delay - def decorator(func): @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> _T: for attempt in range(times - 1): try: return func(*args, **kwargs) @@ -73,7 +85,17 @@ def wrapper(*args, **kwargs): return wrapper - return decorator + +def retry( + times: int, exceptions: Tuple[Type[Exception]], delay: float +) -> _RetryDecorator: + """ + Retry Decorator + Retries the wrapped function/method `times` times if the exceptions listed + in `exceptions` are thrown + """ + + return _RetryDecorator(times, exceptions, delay) class LeetcodeData: diff --git a/test/helpers/test_leetcode.py b/test/helpers/test_leetcode.py index 832f52f..bd74443 100644 --- a/test/helpers/test_leetcode.py +++ b/test/helpers/test_leetcode.py @@ -77,10 +77,14 @@ def dummy_return_question_detail_dict( @mock.patch("os.environ", mock.MagicMock(return_value={"LEETCODE_SESSION_ID": "test"})) @mock.patch("leetcode.auth", mock.MagicMock()) class TestLeetcode: + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio async def test_get_leetcode_api_client(self) -> None: assert leetcode_anki.helpers.leetcode._get_leetcode_api_client() + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio async def test_retry(self) -> None: decorator = leetcode_anki.helpers.leetcode.retry( @@ -134,6 +138,8 @@ def setup(self) -> None: 0, 10000 ) + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -142,6 +148,8 @@ def setup(self) -> None: async def test_init(self) -> None: self._leetcode_data._cache["test"] = QUESTION_DETAIL + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -151,6 +159,8 @@ async def test_get_description(self) -> None: self._leetcode_data._cache["test"] = QUESTION_DETAIL assert (await self._leetcode_data.description("test")) == "test content" + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -162,6 +172,8 @@ async def test_submissions(self) -> None: assert (await self._leetcode_data.submissions_total("test")) == 1 assert (await self._leetcode_data.submissions_accepted("test")) == 1 + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -173,6 +185,8 @@ async def test_difficulty_easy(self) -> None: QUESTION_DETAIL.difficulty = "Easy" assert "Easy" in (await self._leetcode_data.difficulty("test")) + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -184,6 +198,8 @@ async def test_difficulty_medium(self) -> None: QUESTION_DETAIL.difficulty = "Medium" assert "Medium" in (await self._leetcode_data.difficulty("test")) + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -195,6 +211,8 @@ async def test_difficulty_hard(self) -> None: QUESTION_DETAIL.difficulty = "Hard" assert "Hard" in (await self._leetcode_data.difficulty("test")) + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -205,6 +223,8 @@ async def test_paid(self) -> None: assert (await self._leetcode_data.paid("test")) is False + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -215,6 +235,8 @@ async def test_problem_id(self) -> None: assert (await self._leetcode_data.problem_id("test")) == "1" + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -225,6 +247,8 @@ async def test_likes(self) -> None: assert (await self._leetcode_data.likes("test")) == 1 + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -235,6 +259,8 @@ async def test_dislikes(self) -> None: assert (await self._leetcode_data.dislikes("test")) == 1 + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -248,6 +274,8 @@ async def test_tags(self) -> None: "difficulty-hard-tag", ] + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -258,6 +286,8 @@ async def test_freq_bar(self) -> None: assert (await self._leetcode_data.freq_bar("test")) == 1.1 + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data", @@ -267,6 +297,8 @@ async def test_get_problem_data(self) -> None: assert self._leetcode_data._cache["test"] == QUESTION_DETAIL @mock.patch("time.sleep", mock.Mock()) + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio async def test_get_problems_data_page(self) -> None: data = leetcode.models.graphql_data.GraphqlData( @@ -281,14 +313,20 @@ async def test_get_problems_data_page(self) -> None: QUESTION_DETAIL ] + # pyre-fixme[56]: Pyre was not able to infer the type of the decorator + # `pytest.mark.asyncio`. @pytest.mark.asyncio @mock.patch( "leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_count", mock.Mock(return_value=234), ) @mock.patch("leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data_page") - async def test_get_problems_data(self, mock_get_problems_data_page) -> None: - question_list = [QUESTION_DETAIL] * 234 + async def test_get_problems_data( + self, mock_get_problems_data_page: mock.Mock + ) -> None: + question_list: List[ + leetcode.models.graphql_question_detail.GraphqlQuestionDetail + ] = [QUESTION_DETAIL] * 234 def dummy( offset: int, page_size: int, page: int