Skip to content

Better code style coverage #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/style-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 .
17 changes: 17 additions & 0 deletions .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-check
- name: Run pyre
run: pyre check
4 changes: 3 additions & 1 deletion .pyre_configuration
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"source_directories": [
"."
]
],
"site_package_search_strategy": "all",
"strict": true
}
10 changes: 5 additions & 5 deletions generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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())
47 changes: 36 additions & 11 deletions leetcode_anki/helpers/leetcode.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# pylint: disable=missing-module-docstring
import functools
import json
import logging
import math
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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ asyncio_mode = "strict"
testpaths = [
"test",
]

[tool.pylint]
max-line-length = 88
disable = ["line-too-long"]
42 changes: 40 additions & 2 deletions test/helpers/test_leetcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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(
Expand All @@ -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
Expand Down