Skip to content

Commit 662baa2

Browse files
committed
feat(pypi): Remove isort and use ruff to handle all styling
feat(tests): Make formatting tests more robust to API changes feat(formatter): Fix formatter to remove unecessary list closing tag delete(requirements): Keep requirements only in pyproject.toml feat(workflows): Add publishing workflow
1 parent 9d422e6 commit 662baa2

File tree

13 files changed

+123
-63
lines changed

13 files changed

+123
-63
lines changed

.github/workflows/check-style.yml

+5-8
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,10 @@ jobs:
2323
python-version: ${{ matrix.python-version }}
2424
- name: Install dependencies
2525
run: |
26-
python -m pip install --upgrade pip
27-
pip install -r requirements.txt
28-
- name: Examine formatting with black
26+
python -m pip install -e ".[dev]"
27+
- name: Examine formatting with ruff
2928
run: |
30-
pip install ruff
31-
ruff check .
32-
- name: Examine import ordering with isort
29+
python -m ruff check ./leetcode_study_tool
30+
- name: Check formatting
3331
run: |
34-
pip install isort
35-
isort . --check --profile black
32+
python -m ruff format ./leetcode_study_tool --check

.github/workflows/publish.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Upload Python Package to PyPI when a Release is Created
2+
3+
on:
4+
release:
5+
types: [created]
6+
7+
jobs:
8+
pypi-publish:
9+
name: Publish release to PyPI
10+
runs-on: ubuntu-latest
11+
environment:
12+
name: pypi
13+
url: https://pypi.org/p/<PYPI_PACKAGE_NAME>
14+
permissions:
15+
id-token: write
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: "3.x"
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install setuptools wheel
26+
- name: Build package
27+
run: |
28+
python -m build
29+
- name: Publish package distributions to PyPI
30+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/run-tests.yml

+2-11
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,10 @@ jobs:
2424
python-version: ${{ matrix.python-version }}
2525
- name: Install dependencies
2626
run: |
27-
python -m pip install --upgrade pip
28-
pip install -r requirements.txt
29-
- name: Lint with ruff
30-
run: |
31-
pip install ruff
32-
ruff . --ignore E501
27+
python -m pip install -e ".[dev]"
3328
- name: Type check with MyPy
3429
run: |
35-
pip install mypy
36-
mypy . --ignore-missing-imports --exclude /build/
37-
- name: Install package
38-
run: |
39-
pip install -e ".[dev]"
30+
python -m mypy ./leetcode_study_tool
4031
- name: Test with pytest, ensuring 75% coverage
4132
run: |
4233
pip install pytest pytest-cov

Makefile

+4-5
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ all:
99
$(MAKE) type-check
1010

1111
format:
12-
ruff format .
13-
isort --profile black .
12+
python -m ruff format ./leetcode_study_tool
1413

1514
format-check:
16-
ruff check .
17-
isort --check-only --profile black .
15+
python -m ruff check ./leetcode_study_tool
16+
python -m ruff format ./leetcode_study_tool --check
1817

1918
test:
2019
pytest tests/ --cov --cov-fail-under=85
2120

2221
type-check:
23-
mypy src
22+
python -m mypy ./leetcode_study_tool

README.md

+8-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![PyPi](https://img.shields.io/pypi/v/leetcode-study-tool)](https://pypi.org/project/leetcode-study-tool/)
66
![contributions welcome](https://img.shields.io/badge/contributions-welcome-blue.svg?style=flat)
77

8-
![Leetcode Study Tool Diagram](./static/leetcode_study_tool_diagram.png)
8+
![Leetcode Study Tool Diagram](https://github.com/johnsutor/leetcode-study-tool/raw/main/static/leetcode_study_tool_diagram.png)
99

1010
This package lets you get grokking as quickly as possible with Leetcode. It provides a command-line tool for interracting with Leetcode to create either an Excel file or Anki flashcards for study. Currently, this tool supports taking in a list of leetcode question slugs or URLs or popular study sets (including the [Blind 75](https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-75-LeetCode-Questions-to-Save-Your-Time-OaM1orEU), [Grind 75](https://www.techinterviewhandbook.org/grind75), and [Neetcode 150](https://neetcode.io/practice)).
1111

@@ -19,20 +19,16 @@ $ pip install leetcode-study-tool
1919

2020
## 💻 Usage
2121
```shell
22-
usage: leetcode-study-tool [-h]
23-
(--url URL | --file FILE | --preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_all})
24-
[--format {anki,excel}] [--csrf CSRF] [--output OUTPUT]
25-
[--language LANGUAGE]
22+
usage: leetcode-study-tool [-h] (--url URL | --file FILE | --preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}) [--format {anki,excel}]
23+
[--csrf CSRF] [--output OUTPUT] [--language LANGUAGE]
2624

2725
Generates problems from LeetCode questions in a desired format.
2826

2927
options:
3028
-h, --help show this help message and exit
31-
--url URL, -u URL The URL(s) or slug(s) of the LeetCode question(s) to generate
32-
problem(s) for. (default: None)
33-
--file FILE, -f FILE The file containing the URL(s) or slug(s) of the LeetCode question(s)
34-
to generate problem(s) for. (default: None)
35-
--preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_all}, -p {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}
29+
--url URL, -u URL The URL(s) or slug(s) of the LeetCode question(s) to generate problem(s) for. (default: None)
30+
--file FILE, -f FILE The file containing the URL(s) or slug(s) of the LeetCode question(s) to generate problem(s) for. (default: None)
31+
--preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}, -p {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}
3632
The preset to use to generate problem(s) for. (default: None)
3733
--format {anki,excel}, -F {anki,excel}
3834
The format to save the Leetcode problem(s) in. (default: anki)
@@ -78,11 +74,12 @@ When generating an Excel output, the resulting questions are saved in an `.xlsx`
7874
- [X] Add support for exporting to an excel sheet
7975
- [X] Add support for showing neetcode solutions on the back of the card as a
8076
- [X] Add support for getting the difficulty of questions
77+
- [ ] Add support for Jinja templating formatters
78+
- [ ] Add NeetCode shorts
8179
- [ ] Add support for fetching premium questions via authentification
8280
- [ ] Add support for importing cards into Quizlet
8381
- [ ] Add support for fetching questions by topic or tag
8482
link
85-
- [ ] Allow for the definition of custom formatters and outputs (including which fields are included or excluded)
8683
- [ ] Reach 90% test coverage
8784

8885
## 🔎 Other Usefull Stuff

leetcode_study_tool/creator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,4 @@ def _generate_problem(self, url: str) -> Union[str, None]:
124124

125125
data = {k: self._sanitize(v) for k, v in data.items()}
126126

127-
return FORMAT_MAP[self.format](url, slug, data)
127+
return FORMAT_MAP[self.format](url, slug, data) # type: ignore

leetcode_study_tool/formatters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def format_anki(url: str, slug: str, data: dict):
9999
if str(data["id"]) in LEETCODE_TO_NEETCODE:
100100
neetcode = LEETCODE_TO_NEETCODE[str(data["id"])]
101101
problem += "<strong>NeetCode Solution:</strong><br>"
102-
problem += f"<a href=\"{neetcode['url']}\">{neetcode['title']}</a></li><br><br>"
102+
problem += f"<a href=\"{neetcode['url']}\">{neetcode['title']}</a><br><br>"
103103

104104
if data["solutions"]:
105105
problem += format_list_element(

leetcode_study_tool/queries.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def query(
134134
requests.exceptions.HTTPError
135135
If the response from the LeetCode GraphQL API is not 200.
136136
"""
137-
assert content in MAPPINGS.keys(), f"Invalid query content: {content}"
137+
assert content in MAPPINGS, f"Invalid query content: {content}"
138138
if not session:
139139
session = generate_session()
140140

@@ -146,7 +146,7 @@ def query(
146146
},
147147
)
148148
if response.status_code == 200:
149-
return json.loads(response.content.decode("utf-8")).get("data")
149+
return dict(json.loads(response.content.decode("utf-8")).get("data"))
150150
else:
151151
raise requests.exceptions.HTTPError(
152152
f"LeetCode GraphQL API returned {response.status_code}"

pyproject.toml

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
[build-system]
2-
requires = ["setuptools", "wheel"]
2+
requires = ["setuptools>=62.6.0", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "leetcode-study-tool"
7-
version = "1.3.3"
7+
version = "1.3.4"
88
description = "A tool for studying Leetcode with Python"
99
authors = [{name="John Sutor", email="johnsutor3@gmail.com" }]
1010
license = {file = "LICENSE.txt"}
1111
readme = "README.md"
12-
13-
dependencies = ["requests", "XlsxWriter", "p_tqdm"]
1412
keywords = ["leetcode", "leet", "study", "Anki"]
1513
classifiers=[
1614
# Development status
@@ -36,15 +34,20 @@ classifiers=[
3634
'Topic :: Software Development',
3735
'Topic :: Education',
3836
]
37+
dependencies = [
38+
"requests==2.32.3",
39+
"XlsxWriter==3.2.0",
40+
"p-tqdm==1.4.2"
41+
]
3942

4043
[project.optional-dependencies]
4144
dev = [
42-
"ruff",
43-
"types-requests",
44-
"google-api-python-client",
45-
"isort",
46-
"pytest",
47-
"pytest-cov"
45+
"ruff==0.8.6",
46+
"mypy==1.14.1",
47+
"types-requests>=2.32.0",
48+
"google-api-python-client==2.157.0",
49+
"pytest==8.3.4",
50+
"pytest-cov>=5.0.0"
4851
]
4952

5053
[tool.black]
@@ -62,15 +65,17 @@ homepage = "https://github.com/johnsutor/leetcode-study-tool"
6265
repository = "https://github.com/johnsutor/leetcode-study-tool"
6366
changelog = "https://github.com/johnsutor/leetcode-study-tool/blob/main/CHANGELOG.md"
6467

68+
[tool.ruff.lint]
69+
select = ["E4", "E7", "E9", "F", "I", "B", "SIM"]
70+
6571
[tool.mypy]
6672
exclude = [
6773
"build",
6874
"scripts"
6975
]
76+
warn_return_any = true
77+
warn_unused_configs = true
7078
ignore_missing_imports = true
7179

7280
[tool.setuptools.packages.find]
73-
include = ["leetcode_study_tool"]
74-
75-
# [project.optional_dependencies]
76-
# scripts = ["google-api-python-client", "google-auth-oauthlib", "google-auth-httplib2"]
81+
include = ["leetcode_study_tool"]

requirements.txt

-6
This file was deleted.

requirements/base.txt

Whitespace-only changes.

requirements/dev.txt

Whitespace-only changes.

tests/test_formatters.py

+51-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import unittest
22
from datetime import date
33
from textwrap import dedent
4+
from typing import Any, Dict
5+
import re
46

57
import leetcode_study_tool.formatters as formatters
68
from leetcode_study_tool.queries import get_data, get_url
@@ -9,7 +11,33 @@
911
class TestFormatters(unittest.TestCase):
1012
def setUp(self) -> None:
1113
super().setUp()
12-
self.correct_anki_formatted_two_sum_problem = ' <h1> <a href="https://leetcode.com/problems/two-sum/">1. Two Sum</a> </h1> <p> <p>Given an array of integers <code>nums</code>&nbsp;and an integer <code>target</code>, return <em>indices of the two numbers such that they add up to <code>target</code></em>.</p><p>You may assume that each input would have <strong><em>exactly</em> one solution</strong>, and you may not use the <em>same</em> element twice.</p><p>You can return the answer in any order.</p><p>&nbsp;</p><p><strong class="example">Example 1:</strong></p><pre><strong>Input:</strong> nums = [2,7,11,15], target = 9<strong>Output:</strong> [0,1]<strong>Explanation:</strong> Because nums[0] + nums[1] == 9, we return [0, 1].</pre><p><strong class="example">Example 2:</strong></p><pre><strong>Input:</strong> nums = [3,2,4], target = 6<strong>Output:</strong> [1,2]</pre><p><strong class="example">Example 3:</strong></p><pre><strong>Input:</strong> nums = [3,3], target = 6<strong>Output:</strong> [0,1]</pre><p>&nbsp;</p><p><strong>Constraints:</strong></p><ul>\t<li><code>2 &lt;= nums.length &lt;= 10<sup>4</sup></code></li>\t<li><code>-10<sup>9</sup> &lt;= nums[i] &lt;= 10<sup>9</sup></code></li>\t<li><code>-10<sup>9</sup> &lt;= target &lt;= 10<sup>9</sup></code></li>\t<li><strong>Only one valid answer exists.</strong></li></ul><p>&nbsp;</p><strong>Follow-up:&nbsp;</strong>Can you come up with an algorithm that is less than <code>O(n<sup>2</sup>)</code><font face="monospace">&nbsp;</font>time complexity? <p> <strong>Tags:</strong><br> <ul> <li>Array</li><li>Hash Table</li> </ul> <strong>Difficulty:</strong><br><p>Easy</p>;<strong>NeetCode Solution:</strong><br><a href="https://youtube.com/watch?v=KLlXCFG5TnA">Two Sum - Leetcode 1 - HashMap - Python</a></li><br><br> <strong>LeetCode User Solutions:</strong><br> <ul> <li><a href=https://leetcode.com/problems/two-sum/solutions/2/1/>https://leetcode.com/problems/two-sum/solutions/2/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/3/1/>https://leetcode.com/problems/two-sum/solutions/3/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/4/1/>https://leetcode.com/problems/two-sum/solutions/4/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/6/1/>https://leetcode.com/problems/two-sum/solutions/6/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/7/1/>https://leetcode.com/problems/two-sum/solutions/7/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/8/1/>https://leetcode.com/problems/two-sum/solutions/8/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/9/1/>https://leetcode.com/problems/two-sum/solutions/9/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/10/1/>https://leetcode.com/problems/two-sum/solutions/10/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/11/1/>https://leetcode.com/problems/two-sum/solutions/11/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/12/1/>https://leetcode.com/problems/two-sum/solutions/12/1/</a></li> </ul> ;array hash-table'
14+
self.maxDiff = None
15+
16+
def assertAnkiCardStructure(
17+
self, anki_html: str, problem_slug: str, problem_data: Dict[Any, Any]
18+
):
19+
"""
20+
Instead of comparing exact strings, verify the structure and key components
21+
of the Anki card HTML.
22+
"""
23+
self.assertTrue(f"https://leetcode.com/problems/{problem_slug}" in anki_html)
24+
25+
self.assertTrue(f'<p>{problem_data["difficulty"]}</p>' in anki_html)
26+
27+
for tag in problem_data["tags"]:
28+
self.assertTrue(tag["name"] in anki_html)
29+
30+
self.assertRegex(anki_html, r"<strong>LeetCode User Solutions:</strong>")
31+
32+
solution_links = re.findall(
33+
r"https://leetcode\.com/problems/[^/]+/solutions/\d+/1/", anki_html
34+
)
35+
self.assertGreater(
36+
len(solution_links), 0, "Should have at least one solution link"
37+
)
38+
39+
if problem_data.get("neetcode_video_id"):
40+
self.assertTrue("youtube.com/watch?" in anki_html)
1341

1442
def test_format_list_element(self):
1543
self.assertEqual(
@@ -33,10 +61,29 @@ def test_format_solution_link(self):
3361
)
3462

3563
def test_format_anki(self):
36-
data = get_data("two-sum")
64+
"""Test the Anki card formatter with actual LeetCode data"""
65+
problem_slug = "two-sum"
66+
data = get_data(problem_slug)
67+
formatted_anki = formatters.format_anki(
68+
get_url(problem_slug), problem_slug, data
69+
)
70+
71+
print(formatted_anki)
72+
73+
self.assertAnkiCardStructure(formatted_anki, problem_slug, data)
74+
75+
self.assertTrue(formatted_anki.startswith(" <h1>"))
76+
self.assertTrue("</ul>" in formatted_anki)
77+
78+
self.assertEqual(
79+
formatted_anki.count("<ul>"),
80+
formatted_anki.count("</ul>"),
81+
"Mismatched <ul> tags",
82+
)
3783
self.assertEqual(
38-
formatters.format_anki(get_url("two-sum"), "two-sum", data),
39-
self.correct_anki_formatted_two_sum_problem,
84+
formatted_anki.count("<li>"),
85+
formatted_anki.count("</li>"),
86+
"Mismatched <li> tags",
4087
)
4188

4289
def test_format_excel(self):

0 commit comments

Comments
 (0)