Skip to content

Commit 2295587

Browse files
committed
feat: dev
1 parent 19ef0ef commit 2295587

File tree

7 files changed

+243
-41
lines changed

7 files changed

+243
-41
lines changed
File renamed without changes.
File renamed without changes.

.templates/leetcode/gen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def generate(
1818
"""Generate LeetCode problem from JSON using cookiecutter."""
1919
generator = TemplateGenerator()
2020
template_dir = Path(__file__).parent
21-
generator.generate_problem(json_file, force, template_dir)
21+
output_dir = template_dir.parent.parent / "leetcode"
22+
generator.generate_problem(json_file, template_dir, output_dir, force)
2223

2324

2425
if __name__ == "__main__":

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ leetcode/two_sum/
4848
└── __init__.py # Package marker
4949
```
5050

51-
## 🎯 Supported Problem Categories
51+
## 🎯 Supported Problem Categories (ongoing)
5252

5353
- **Arrays & Hashing** - Two Sum, Group Anagrams, Top K Elements
5454
- **Two Pointers** - Valid Palindrome, Container With Most Water
@@ -66,7 +66,7 @@ leetcode/two_sum/
6666
- **Intervals** - Merge Intervals, Meeting Rooms
6767
- **Math & Geometry** - Rotate Image, Spiral Matrix
6868

69-
Includes problems from **Blind 75**, **Grind 75**, **NeetCode 150**, and **Top Interview Questions**. This is an ongoing project - contributions are welcome!
69+
Includes problems from **Blind 75**, **Grind 75**, **NeetCode 150**, and **Top Interview Questions**.
7070

7171
## 🎨 Visualizations
7272

@@ -101,9 +101,12 @@ _Interactive multi-cell playground for each problem_
101101

102102
```bash
103103
# Work on a specific problem
104-
make p-test PROBLEM=two_sum
105-
# Edit leetcode/two_sum/solution.py
104+
make p-test PROBLEM=lru_cache
105+
# Edit leetcode/lru_cache/solution.py
106106
# Run tests to verify
107+
108+
# Or use make p-test if default problem is set in Makefile
109+
make p-test
107110
```
108111

109112
**Add new problems**:
@@ -127,7 +130,7 @@ make gen-all-problems
127130
make lint
128131
```
129132

130-
## 🧰 Helper Classes
133+
## 🧰 Helper Classes (ongoing)
131134

132135
- **TreeNode**: `from leetcode_py import TreeNode`
133136
- Beautiful tree visualization with anytree rendering
@@ -139,6 +142,4 @@ make lint
139142
- Perfect for debugging linked list problems
140143
- New helpers: Add to `leetcode_py/`
141144

142-
This is an ongoing project - contributions are welcome!
143-
144145
Perfect for interview preparation with professional-grade tooling and beautiful visualizations.

leetcode_py/tools/generator.py

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,60 @@
33
import json
44
import sys
55
from pathlib import Path
6-
from typing import Any, Dict
6+
from typing import Any, Dict, Protocol
77

88
import typer
99
from cookiecutter.main import cookiecutter
1010

1111

12+
class FileOperations(Protocol):
13+
"""Protocol for file operations to enable testing."""
14+
15+
def read_json(self, path: Path) -> Dict[str, Any]:
16+
"""Read JSON from file."""
17+
...
18+
19+
def write_json(self, path: Path, data: Dict[str, Any]) -> None:
20+
"""Write JSON to file."""
21+
...
22+
23+
def exists(self, path: Path) -> bool:
24+
"""Check if path exists."""
25+
...
26+
27+
28+
class DefaultFileOperations:
29+
"""Default file operations implementation."""
30+
31+
def read_json(self, path: Path) -> Dict[str, Any]:
32+
"""Read JSON from file."""
33+
try:
34+
with open(path) as f:
35+
return json.load(f)
36+
except (json.JSONDecodeError, OSError) as e:
37+
typer.echo(f"Error reading {path}: {e}", err=True)
38+
raise typer.Exit(1)
39+
40+
def write_json(self, path: Path, data: Dict[str, Any]) -> None:
41+
"""Write JSON to file."""
42+
try:
43+
with open(path, "w") as f:
44+
json.dump(data, f)
45+
except OSError as e:
46+
typer.echo(f"Error writing {path}: {e}", err=True)
47+
raise typer.Exit(1)
48+
49+
def exists(self, path: Path) -> bool:
50+
"""Check if path exists."""
51+
return path.exists()
52+
53+
1254
class TemplateGenerator:
1355
"""Generator for LeetCode problem templates using cookiecutter."""
1456

15-
def __init__(self):
57+
def __init__(self, file_ops: FileOperations | None = None):
1658
self.common_tags = ["grind-75", "blind-75", "neetcode-150", "top-interview"]
59+
self.file_ops = file_ops or DefaultFileOperations()
1760

1861
def check_and_prompt_tags(self, data: Dict[str, Any]) -> Dict[str, Any]:
1962
"""Check and prompt for tags if empty."""
@@ -85,18 +128,19 @@ def convert_arrays_to_nested(self, data: Dict[str, Any]) -> Dict[str, Any]:
85128
del extra_context[field]
86129
return extra_context
87130

88-
def check_overwrite_permission(self, problem_name: str, force: bool, template_dir: Path) -> None:
131+
def check_overwrite_permission(self, problem_name: str, force: bool, output_dir: Path) -> None:
89132
"""Check if problem exists and get overwrite permission."""
90133
if force:
91134
return
92135

93-
output_dir = template_dir.parent.parent / "leetcode"
94136
problem_dir = output_dir / problem_name
95137

96-
if not problem_dir.exists():
138+
if not self.file_ops.exists(problem_dir):
97139
return
98140

99-
typer.echo(f"⚠️ Warning: Problem '{problem_name}' already exists in leetcode/", err=True)
141+
typer.echo(
142+
f"⚠️ Warning: Problem '{problem_name}' already exists in {output_dir.name}/", err=True
143+
)
100144
typer.echo("This will overwrite existing files. Use --force to skip this check.", err=True)
101145

102146
if sys.stdin.isatty(): # Interactive terminal
@@ -109,47 +153,43 @@ def check_overwrite_permission(self, problem_name: str, force: bool, template_di
109153
raise typer.Exit(1)
110154

111155
def generate_problem(
112-
self, json_file: str, force: bool = False, template_dir: Path | None = None
156+
self, json_file: str, template_dir: Path, output_dir: Path, force: bool = False
113157
) -> None:
114158
"""Generate problem from JSON using cookiecutter."""
115159
json_path = Path(json_file)
116-
if not json_path.exists():
160+
if not self.file_ops.exists(json_path):
117161
typer.echo(f"Error: {json_file} not found", err=True)
118162
raise typer.Exit(1)
119163

120164
# Load JSON data
121-
with open(json_path) as f:
122-
data = json.load(f)
165+
data = self.file_ops.read_json(json_path)
123166

124167
# Process data
125168
data = self.check_and_prompt_tags(data)
126169
data = self.auto_set_dummy_return(data)
127170

128171
# Save updated data back to JSON file
129-
with open(json_path, "w") as f:
130-
json.dump(data, f)
172+
self.file_ops.write_json(json_path, data)
131173

132174
# Convert arrays to cookiecutter-friendly nested format
133175
extra_context = self.convert_arrays_to_nested(data)
134176

135177
# Check if problem already exists
136178
problem_name = extra_context.get("problem_name", "unknown")
137179

138-
# Use provided template_dir or default
139-
if template_dir is None:
140-
template_dir = Path(__file__).parent.parent.parent / ".templates" / "leetcode"
141-
142-
self.check_overwrite_permission(problem_name, force, template_dir)
180+
self.check_overwrite_permission(problem_name, force, output_dir)
143181

144182
# Generate project using cookiecutter
145-
output_dir = template_dir.parent.parent / "leetcode"
146-
147-
cookiecutter(
148-
str(template_dir),
149-
extra_context=extra_context,
150-
no_input=True,
151-
overwrite_if_exists=True,
152-
output_dir=str(output_dir),
153-
)
183+
try:
184+
cookiecutter(
185+
str(template_dir),
186+
extra_context=extra_context,
187+
no_input=True,
188+
overwrite_if_exists=True,
189+
output_dir=str(output_dir),
190+
)
191+
except Exception as e:
192+
typer.echo(f"Error generating template: {e}", err=True)
193+
raise typer.Exit(1)
154194

155195
typer.echo(f"✅ Generated problem: {problem_name}")

tests/tools/test_generator.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ def test_convert_arrays_to_nested_non_list_values(self):
106106

107107
def test_check_overwrite_permission_force(self):
108108
"""Test overwrite permission with force flag."""
109-
template_dir = Path("/fake/template")
109+
output_dir = Path("/fake/output")
110110
# Should not raise exception with force=True
111-
self.generator.check_overwrite_permission("test_problem", True, template_dir)
111+
self.generator.check_overwrite_permission("test_problem", True, output_dir)
112112

113113
def test_check_overwrite_permission_nonexistent_problem(self):
114114
"""Test overwrite permission when problem doesn't exist."""
115-
template_dir = Path("/nonexistent/template")
115+
output_dir = Path("/nonexistent/output")
116116
# Should not raise exception when problem doesn't exist
117-
self.generator.check_overwrite_permission("nonexistent_problem", False, template_dir)
117+
self.generator.check_overwrite_permission("nonexistent_problem", False, output_dir)
118118

119119
def test_check_and_prompt_tags_with_existing_tags(self):
120120
"""Test check_and_prompt_tags when tags already exist."""
@@ -157,3 +157,82 @@ def test_generate_problem_components(self):
157157
nested_data = self.generator.convert_arrays_to_nested(processed_data)
158158
assert "_tags" in nested_data
159159
assert nested_data["_tags"] == {"list": []}
160+
161+
def test_file_operations_injection(self):
162+
"""Test that file operations can be injected for testing."""
163+
from unittest.mock import Mock
164+
165+
from leetcode_py.tools.generator import FileOperations
166+
167+
mock_file_ops = Mock(spec=FileOperations)
168+
generator = TemplateGenerator(file_ops=mock_file_ops)
169+
assert generator.file_ops is mock_file_ops
170+
171+
def test_check_and_prompt_tags_interactive_valid_choices(self):
172+
"""Test interactive tag selection with valid choices."""
173+
from unittest.mock import patch
174+
175+
data: dict[str, Any] = {"tags": []}
176+
177+
with (
178+
patch("sys.stdin.isatty", return_value=True),
179+
patch("typer.prompt", return_value="1,2"),
180+
patch("typer.echo"),
181+
):
182+
result = self.generator.check_and_prompt_tags(data)
183+
assert "grind-75" in result["tags"]
184+
assert "blind-75" in result["tags"]
185+
186+
def test_check_and_prompt_tags_interactive_skip(self):
187+
"""Test interactive tag selection with skip option."""
188+
from unittest.mock import patch
189+
190+
data: dict[str, Any] = {"tags": []}
191+
192+
with (
193+
patch("sys.stdin.isatty", return_value=True),
194+
patch("typer.prompt", return_value="0"),
195+
patch("typer.echo"),
196+
):
197+
result = self.generator.check_and_prompt_tags(data)
198+
assert result["tags"] == []
199+
200+
def test_check_and_prompt_tags_interactive_invalid_input(self):
201+
"""Test interactive tag selection with invalid input."""
202+
from unittest.mock import patch
203+
204+
data: dict[str, Any] = {"tags": []}
205+
206+
with (
207+
patch("sys.stdin.isatty", return_value=True),
208+
patch("typer.prompt", return_value="invalid"),
209+
patch("typer.echo"),
210+
):
211+
result = self.generator.check_and_prompt_tags(data)
212+
assert result["tags"] == []
213+
214+
def test_generate_problem_success(self):
215+
"""Test successful problem generation."""
216+
from unittest.mock import Mock, patch
217+
218+
from leetcode_py.tools.generator import FileOperations
219+
220+
mock_file_ops = Mock(spec=FileOperations)
221+
mock_file_ops.exists.side_effect = lambda path: str(path).endswith("test.json")
222+
mock_file_ops.read_json.return_value = {
223+
"problem_name": "test_problem",
224+
"return_type": "bool",
225+
"tags": [],
226+
}
227+
228+
generator = TemplateGenerator(file_ops=mock_file_ops)
229+
230+
template_dir = Path("/test/template")
231+
output_dir = Path("/test/output")
232+
233+
with patch("leetcode_py.tools.generator.cookiecutter", return_value=None) as mock_cookiecutter:
234+
generator.generate_problem("test.json", template_dir, output_dir, force=True)
235+
236+
mock_file_ops.read_json.assert_called_once()
237+
mock_file_ops.write_json.assert_called_once()
238+
mock_cookiecutter.assert_called_once()

0 commit comments

Comments
 (0)