diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml index 45dc0a1..b0ab073 100644 --- a/.github/workflows/style-check.yml +++ b/.github/workflows/style-check.yml @@ -13,10 +13,12 @@ jobs: 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 pylint run: pip install pylint - name: Run pylint - run: pylint -E generate.py + run: find . -type f -name "*.py" | xargs pylint -E black: name: black runs-on: ubuntu-latest @@ -46,4 +48,4 @@ jobs: - name: Install isort run: pip install isort - name: Run isort - run: isort --ensure-newline-before-comments --diff generate.py + run: isort --ensure-newline-before-comments --diff -v . 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 91a516a..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 @@ -34,7 +34,7 @@ def parse_args() -> argparse.Namespace: "--start", type=int, help="Start generation from this problem", default=0 ) parser.add_argument( - "--stop", type=int, help="Stop generation on this problem", default=2 ** 64 + "--stop", type=int, help="Stop generation on this problem", default=2**64 ) parser.add_argument( "--page-size", @@ -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 af318cb..f4ba87b 100644 --- a/leetcode_anki/helpers/leetcode.py +++ b/leetcode_anki/helpers/leetcode.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-module-docstring import functools import json import logging @@ -5,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 @@ -48,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) @@ -72,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: @@ -230,7 +253,7 @@ def _get_problems_data( leetcode.models.graphql_question_detail.GraphqlQuestionDetail ] = [] - logging.info(f"Fetching {stop - start + 1} problems {page_size} per page") + logging.info("Fetching %s problems %s per page", stop - start + 1, page_size) for page in tqdm( range(math.ceil((stop - start + 1) / page_size)), @@ -261,6 +284,8 @@ def _get_problem_data( if problem_slug in cache: return cache[problem_slug] + raise ValueError(f"Problem {problem_slug} is not in cache") + async def _get_description(self, problem_slug: str) -> str: """ Problem description diff --git a/pyproject.toml b/pyproject.toml index 00a86b1..953fe9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,3 +3,7 @@ asyncio_mode = "strict" testpaths = [ "test", ] + +[tool.pylint] +max-line-length = 88 +disable = ["line-too-long"] 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