Skip to content

Commit 3ddc5dc

Browse files
committed
Add pascals-triangle problem and fix N803 lint
1 parent 56e51eb commit 3ddc5dc

File tree

9 files changed

+569
-1
lines changed

9 files changed

+569
-1
lines changed

add_problems.py

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import json
2+
import os
3+
import re
4+
import shutil
5+
import subprocess
6+
import sys
7+
import time
8+
9+
10+
def get_missing_slugs():
11+
return ["pascals-triangle"]
12+
13+
14+
def transform_scraped_data(data):
15+
"""Transforms scraped data into the format expected by cookiecutter/gen."""
16+
transformed = {}
17+
18+
# Basic fields
19+
# Sanitize problem_name
20+
problem_name = data.get("slug", "").replace("-", "_")
21+
if problem_name and problem_name[0].isdigit():
22+
problem_name = f"_{problem_name}"
23+
transformed["problem_name"] = problem_name
24+
transformed["problem_number"] = data.get("number", "0")
25+
transformed["problem_title"] = data.get("title", "")
26+
transformed["difficulty"] = data.get("difficulty", "Medium")
27+
28+
# Topics: list -> string
29+
topics = data.get("topics", [])
30+
if isinstance(topics, list):
31+
transformed["topics"] = ", ".join(topics)
32+
else:
33+
transformed["topics"] = str(topics)
34+
35+
# Description
36+
transformed["readme_description"] = data.get("description", "")
37+
38+
# Constraints: list -> string
39+
constraints = data.get("constraints", [])
40+
if isinstance(constraints, list):
41+
transformed["readme_constraints"] = "\n".join([f"- {c}" for c in constraints])
42+
else:
43+
transformed["readme_constraints"] = str(constraints)
44+
45+
# Examples
46+
examples = data.get("examples", [])
47+
readme_examples = []
48+
for ex in examples:
49+
# Scraped example might be dict or string?
50+
# 01_matrix.json shows examples as empty list []?
51+
# But raw_content has them.
52+
# If examples is list of dicts with 'text' or 'input'/'output'
53+
if isinstance(ex, dict):
54+
content = ex.get("text", "")
55+
if not content:
56+
inp = ex.get("input", "")
57+
out = ex.get("output", "")
58+
if inp and out:
59+
content = f"```\nInput: {inp}\nOutput: {out}\n```"
60+
if content:
61+
readme_examples.append({"content": content})
62+
63+
transformed["_readme_examples"] = {"list": readme_examples}
64+
65+
# Python Code Parsing
66+
python_code = data.get("python_code", "")
67+
solution_class_name = "Solution"
68+
method_name = "unknown"
69+
method_signature = ""
70+
method_body = " pass"
71+
72+
# Simple regex # Find Solution class name
73+
if "class Solution:" in python_code:
74+
solution_class_name = "Solution"
75+
else:
76+
class_match = re.search(r"class\s+(\w+):", python_code)
77+
if class_match:
78+
solution_class_name = class_match.group(1)
79+
else:
80+
solution_class_name = "Solution"
81+
# Find method def
82+
# def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
83+
# Find class Solution
84+
solution_match = re.search(r"class\s+Solution:\s*", python_code)
85+
if solution_match:
86+
# Search for method AFTER class Solution definition
87+
start_index = solution_match.end()
88+
method_match = re.search(r"def\s+(\w+)\s*\((.*?)\)\s*(->\s*.*?)?:", python_code[start_index:])
89+
else:
90+
# Fallback if no Solution class (unlikely but possible)
91+
method_match = re.search(r"def\s+(\w+)\s*\((.*?)\)\s*(->\s*.*?)?:", python_code)
92+
93+
if method_match:
94+
method_name = method_match.group(1)
95+
params = method_match.group(2)
96+
return_type = method_match.group(3) or ""
97+
98+
# Clean params to remove 'self'
99+
param_list = [p.strip() for p in params.split(",") if p.strip()]
100+
if param_list and param_list[0] == "self":
101+
param_list = param_list[1:]
102+
103+
clean_params = ", ".join(param_list)
104+
105+
# Construct signature
106+
if clean_params:
107+
method_signature = f"(self, {clean_params}){return_type}"
108+
else:
109+
method_signature = f"(self){return_type}"
110+
111+
# Snake case method name
112+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", method_name)
113+
snake_method_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
114+
115+
method_body = f" # TODO: Implement {snake_method_name}\n return "
116+
if "ListNode" in return_type or "TreeNode" in return_type:
117+
method_body += "None"
118+
elif "List" in return_type:
119+
method_body += "[]"
120+
elif "int" in return_type:
121+
method_body += "0"
122+
elif "bool" in return_type:
123+
method_body += "False"
124+
elif "str" in return_type:
125+
method_body += '""'
126+
else:
127+
method_body += "None"
128+
129+
transformed["solution_class_name"] = solution_class_name
130+
131+
transformed["_solution_methods"] = {
132+
"list": [{"name": snake_method_name, "signature": method_signature, "body": method_body}]
133+
}
134+
135+
# Helpers and Tests
136+
transformed["helpers_run_name"] = snake_method_name
137+
138+
if clean_params:
139+
helper_sig = f"(solution_class: type, {clean_params})"
140+
else:
141+
helper_sig = "(solution_class: type)"
142+
143+
transformed["helpers_run_signature"] = helper_sig
144+
145+
# Extract argument names for the call
146+
arg_names = [p.split(":")[0].strip() for p in param_list]
147+
call_args = ", ".join(arg_names)
148+
149+
transformed["helpers_run_body"] = (
150+
f" implementation = solution_class()\n"
151+
f" return implementation.{snake_method_name}({call_args})"
152+
)
153+
154+
transformed["helpers_assert_name"] = snake_method_name
155+
# Infer expected type from return type
156+
expected_type = return_type.replace("->", "").strip()
157+
transformed["helpers_assert_signature"] = (
158+
f"(result: {expected_type}, expected: {expected_type}) -> bool"
159+
)
160+
transformed["helpers_assert_body"] = " assert result == expected\n return True"
161+
162+
transformed["test_class_name"] = "".join(
163+
[word.capitalize() for word in transformed["problem_name"].split("_")]
164+
)
165+
transformed["test_class_content"] = (
166+
" def setup_method(self):\n self.solution = Solution()"
167+
)
168+
169+
# Test Cases
170+
formatted_test_cases = []
171+
raw_test_cases = data.get("test_cases", [])
172+
173+
# Count args
174+
arg_count = len(arg_names)
175+
176+
for tc in raw_test_cases:
177+
# Expected length is arg_count + 1 (for expected output)
178+
if len(tc) == arg_count + 1:
179+
args = ", ".join(tc[:-1])
180+
expected = tc[-1]
181+
formatted_test_cases.append(f"({args}, {expected})")
182+
else:
183+
print(
184+
f"[{data.get('slug')}] Warning: Skipping invalid test case "
185+
f"(len={len(tc)}, expected={arg_count+1}): {tc}"
186+
)
187+
188+
# Parametrize string
189+
if call_args:
190+
parametrize_str = f"{call_args}, expected"
191+
else:
192+
parametrize_str = "expected"
193+
194+
# Test signature
195+
if clean_params:
196+
test_sig = f"(self, {clean_params}, expected: {expected_type})"
197+
else:
198+
test_sig = f"(self, expected: {expected_type})"
199+
200+
transformed["_test_methods"] = {
201+
"list": [
202+
{
203+
"name": f"test_{snake_method_name}",
204+
"signature": test_sig,
205+
"parametrize": parametrize_str,
206+
"test_cases": {"list": formatted_test_cases},
207+
"body": (
208+
f" result = run_{snake_method_name}(Solution, {call_args})\n"
209+
f" assert_{snake_method_name}(result, expected)"
210+
),
211+
}
212+
]
213+
}
214+
215+
# Imports
216+
typing_imports = []
217+
for type_name in ["List", "Optional", "Dict", "Set", "Tuple"]:
218+
if re.search(rf"\b{type_name}\b", method_signature):
219+
typing_imports.append(type_name)
220+
221+
ds_imports = []
222+
if "ListNode" in method_signature:
223+
ds_imports.append("from leetcode_py.data_structures.list_node import ListNode")
224+
if "TreeNode" in method_signature:
225+
ds_imports.append("from leetcode_py.data_structures.tree_node import TreeNode")
226+
227+
imports_list = []
228+
if typing_imports:
229+
imports_list.append(f"from typing import {', '.join(typing_imports)}")
230+
imports_list.extend(ds_imports)
231+
232+
imports_str = "\n".join(imports_list)
233+
234+
transformed["solution_imports"] = imports_str
235+
236+
test_imports_list = ["import pytest", "from leetcode_py import logged_test"]
237+
if imports_str:
238+
test_imports_list.append(imports_str)
239+
test_imports_list.append(
240+
f"from .helpers import assert_{snake_method_name}, run_{snake_method_name}"
241+
)
242+
test_imports_list.append("from .solution import Solution")
243+
244+
transformed["test_imports"] = "\n".join(test_imports_list)
245+
transformed["helpers_imports"] = imports_str
246+
247+
return transformed
248+
249+
250+
def process_problem(slug):
251+
scrape_slug = slug.replace("_", "-")
252+
253+
print(f"\n[{slug}] Starting process...")
254+
255+
try:
256+
# 1. Scrape
257+
print(f"[{slug}] Scraping {scrape_slug}...")
258+
result = subprocess.run(
259+
["uv", "run", "python", "-m", "leetcode_py.cli.main", "scrape", "-s", scrape_slug],
260+
check=False,
261+
capture_output=True,
262+
text=True,
263+
)
264+
265+
if result.returncode != 0:
266+
print(f"[{slug}] Scrape FAILED with code {result.returncode}.")
267+
return False
268+
269+
output = result.stdout
270+
json_start = output.find("{")
271+
if json_start == -1:
272+
print(f"[{slug}] Scrape output does not contain JSON start.")
273+
return False
274+
275+
json_content = output[json_start:]
276+
try:
277+
data = json.loads(json_content)
278+
except json.JSONDecodeError:
279+
print(f"[{slug}] Extracted content is not valid JSON.")
280+
return False
281+
282+
# TRANSFORM DATA
283+
transformed_data = transform_scraped_data(data)
284+
285+
# Convert to snake_case for file naming
286+
snake_slug = slug.replace("-", "_")
287+
288+
# If starts with digit, prepend _
289+
if snake_slug[0].isdigit():
290+
snake_slug = f"_{snake_slug}"
291+
292+
# Save to file
293+
json_dir = "leetcode_py/cli/resources/leetcode/json/problems"
294+
os.makedirs(json_dir, exist_ok=True)
295+
json_path = os.path.join(json_dir, f"{snake_slug}.json")
296+
297+
with open(json_path, "w") as f:
298+
json.dump(transformed_data, f, indent=2)
299+
300+
abs_json_path = os.path.abspath(json_path)
301+
print(f"[{slug}] Scrape successful. Saved to {abs_json_path}")
302+
303+
# 2. Generate
304+
print(f"[{snake_slug}] Generating...")
305+
gen_result = subprocess.run(
306+
[
307+
"uv",
308+
"run",
309+
"python",
310+
"-m",
311+
"leetcode_py.cli.main",
312+
"gen",
313+
"-s",
314+
snake_slug,
315+
"-o",
316+
"leetcode",
317+
"--force",
318+
],
319+
check=False,
320+
capture_output=True,
321+
text=True,
322+
)
323+
324+
if gen_result.returncode != 0:
325+
print(f"[{slug}] Generation FAILED.")
326+
print(f"Error output: {gen_result.stderr}")
327+
print(f"Standard output: {gen_result.stdout}")
328+
return False
329+
330+
print(f"[{slug}] Generation successful.")
331+
return True
332+
333+
except Exception as e:
334+
print(f"[{slug}] Unexpected error: {e}")
335+
return False
336+
337+
338+
def main():
339+
if not shutil.which("uv"):
340+
print("Error: 'uv' executable not found in PATH.")
341+
sys.exit(1)
342+
343+
slugs = get_missing_slugs()
344+
print(f"Found {len(slugs)} problems to process.")
345+
346+
success_count = 0
347+
failed_slugs = []
348+
349+
for i, slug in enumerate(slugs):
350+
print(f"\n--- Processing {i+1}/{len(slugs)}: {slug} ---")
351+
if process_problem(slug):
352+
success_count += 1
353+
else:
354+
failed_slugs.append(slug)
355+
356+
time.sleep(0.5)
357+
358+
print("\n" + "=" * 30)
359+
print(f"Completed. Success: {success_count}, Failed: {len(failed_slugs)}")
360+
361+
if failed_slugs:
362+
print("\nFailed slugs:")
363+
for s in failed_slugs:
364+
print(s)
365+
366+
with open("failed_slugs.txt", "w") as f:
367+
for s in failed_slugs:
368+
f.write(s + "\n")
369+
print("Failed slugs written to failed_slugs.txt")
370+
else:
371+
if os.path.exists("failed_slugs.txt"):
372+
os.remove("failed_slugs.txt")
373+
374+
375+
if __name__ == "__main__":
376+
main()

0 commit comments

Comments
 (0)