Your fixed commit message here
This commit is contained in:
569
pcreater.py
569
pcreater.py
@@ -1,569 +0,0 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import webbrowser
|
||||
try:
|
||||
from tkinterweb import HtmlFrame # For markdown preview
|
||||
except ImportError:
|
||||
# Fallback if tkinterweb is not available
|
||||
HtmlFrame = None
|
||||
|
||||
# For syntax highlighting
|
||||
try:
|
||||
from pygments import lex
|
||||
from pygments.lexers import PythonLexer
|
||||
from pygments.styles import get_style_by_name
|
||||
PYGMENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYGMENTS_AVAILABLE = False
|
||||
|
||||
class CodeEditor(scrolledtext.ScrolledText):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bind('<Tab>', self._tab)
|
||||
self.bind('<Shift-Tab>', self._shift_tab)
|
||||
self.bind('<KeyRelease>', self._on_key_release)
|
||||
self.configure(font=('Consolas', 10))
|
||||
|
||||
# Setup tags for syntax highlighting if available
|
||||
if PYGMENTS_AVAILABLE:
|
||||
self._setup_tags()
|
||||
|
||||
def _tab(self, event):
|
||||
self.insert(tk.INSERT, " " * 4)
|
||||
return "break" # Prevent default behavior
|
||||
|
||||
def _shift_tab(self, event):
|
||||
# Remove indentation
|
||||
sel_start = self.index(tk.SEL_FIRST)
|
||||
sel_end = self.index(tk.SEL_LAST)
|
||||
|
||||
if sel_start and sel_end:
|
||||
# Get the lines in the selection
|
||||
start_line = int(sel_start.split('.')[0])
|
||||
end_line = int(sel_end.split('.')[0])
|
||||
|
||||
for line_num in range(start_line, end_line + 1):
|
||||
line_start = f"{line_num}.0"
|
||||
line_end = f"{line_num}.end"
|
||||
line_text = self.get(line_start, line_end)
|
||||
|
||||
if line_text.startswith(" " * 4):
|
||||
self.delete(line_start, f"{line_num}.4")
|
||||
elif line_text.startswith("\t"):
|
||||
self.delete(line_start, f"{line_num}.1")
|
||||
|
||||
return "break"
|
||||
|
||||
def _on_key_release(self, event):
|
||||
if event.keysym == 'Return':
|
||||
# Auto-indent new line
|
||||
current_line = self.get("insert linestart", "insert")
|
||||
indent = len(current_line) - len(current_line.lstrip())
|
||||
self.insert("insert", " " * indent)
|
||||
|
||||
# Syntax highlighting if available
|
||||
if PYGMENTS_AVAILABLE:
|
||||
self.highlight_syntax()
|
||||
|
||||
def _setup_tags(self):
|
||||
try:
|
||||
self.style = get_style_by_name('default')
|
||||
|
||||
# Create tags for different token types
|
||||
for token, style in self.style:
|
||||
color = style.get('color', '')
|
||||
if color and self._is_valid_color(color):
|
||||
self.tag_configure(str(token), foreground=f'#{color}')
|
||||
except Exception as e:
|
||||
print(f"Error setting up syntax highlighting: {e}")
|
||||
|
||||
def _is_valid_color(self, color_str):
|
||||
"""Check if a string is a valid hex color"""
|
||||
if not color_str:
|
||||
return False
|
||||
# Remove # if present
|
||||
if color_str.startswith('#'):
|
||||
color_str = color_str[1:]
|
||||
# Check if it's a valid hex color (3 or 6 digits)
|
||||
if len(color_str) not in (3, 6):
|
||||
return False
|
||||
try:
|
||||
int(color_str, 16)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def highlight_syntax(self):
|
||||
if not PYGMENTS_AVAILABLE:
|
||||
return
|
||||
|
||||
try:
|
||||
# Remove previous highlighting
|
||||
for tag in self.tag_names():
|
||||
if tag != "sel":
|
||||
self.tag_remove(tag, "1.0", "end")
|
||||
|
||||
# Get the text
|
||||
code = self.get("1.0", "end-1c")
|
||||
|
||||
# Lex the code and apply tags
|
||||
pos = 0
|
||||
for token, text in lex(code, PythonLexer()):
|
||||
if text.strip(): # Only apply tags to non-whitespace text
|
||||
start = f"1.0+{pos}c"
|
||||
end = f"1.0+{pos+len(text)}c"
|
||||
self.tag_add(str(token), start, end)
|
||||
pos += len(text)
|
||||
except Exception as e:
|
||||
print(f"Error during syntax highlighting: {e}")
|
||||
|
||||
class MarkdownEditor:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
# Create a paned window for split view
|
||||
self.paned = ttk.PanedWindow(parent, orient=tk.HORIZONTAL)
|
||||
self.paned.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Left side - text editor
|
||||
self.editor_frame = ttk.Frame(self.paned)
|
||||
self.editor = scrolledtext.ScrolledText(
|
||||
self.editor_frame, wrap=tk.WORD, font=('Consolas', 10)
|
||||
)
|
||||
self.editor.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Right side - preview (if available)
|
||||
self.preview_frame = ttk.Frame(self.paned)
|
||||
if HtmlFrame:
|
||||
self.preview = HtmlFrame(self.preview_frame)
|
||||
self.preview.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Add both frames to the paned window
|
||||
self.paned.add(self.editor_frame, weight=1)
|
||||
self.paned.add(self.preview_frame, weight=1)
|
||||
|
||||
# Bind key events
|
||||
self.editor.bind('<KeyRelease>', self.update_preview)
|
||||
else:
|
||||
# If tkinterweb is not available, just show the editor
|
||||
self.paned.add(self.editor_frame, weight=1)
|
||||
self.preview = None
|
||||
|
||||
def update_preview(self, event=None):
|
||||
if not self.preview:
|
||||
return
|
||||
|
||||
# Get the markdown text
|
||||
markdown_text = self.editor.get("1.0", "end-1c")
|
||||
|
||||
# Convert to HTML (simple conversion for demonstration)
|
||||
# In a real application, you might want to use a markdown library
|
||||
html_content = f"""
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; padding: 10px; }}
|
||||
code {{ background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 5px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{self._simple_markdown_to_html(markdown_text)}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Update the preview
|
||||
self.preview.load_html(html_content)
|
||||
|
||||
def _simple_markdown_to_html(self, text):
|
||||
# Very basic markdown to HTML conversion
|
||||
# For a real application, consider using a proper markdown library
|
||||
html = text
|
||||
html = html.replace("# ", "<h1>").replace("\n# ", "</h1>\n<h1>") + "</h1>"
|
||||
html = html.replace("## ", "<h2>").replace("\n## ", "</h2>\n<h2>") + "</h2>"
|
||||
html = html.replace("### ", "<h3>").replace("\n### ", "</h3>\n<h3>") + "</h3>"
|
||||
html = html.replace("**", "<strong>").replace("**", "</strong>")
|
||||
html = html.replace("*", "<em>").replace("*", "</em>")
|
||||
html = html.replace("`", "<code>").replace("`", "</code>")
|
||||
html = html.replace("\n", "<br>")
|
||||
return html
|
||||
|
||||
def get(self, start, end):
|
||||
return self.editor.get(start, end)
|
||||
|
||||
def insert(self, index, text):
|
||||
self.editor.insert(index, text)
|
||||
if self.preview:
|
||||
self.update_preview()
|
||||
|
||||
def delete(self, start, end):
|
||||
self.editor.delete(start, end)
|
||||
if self.preview:
|
||||
self.update_preview()
|
||||
|
||||
class ProblemCreatorApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Coding Problem Creator")
|
||||
self.root.geometry("1000x800")
|
||||
self.root.minsize(800, 600)
|
||||
|
||||
# Configure style
|
||||
style = ttk.Style()
|
||||
style.theme_use('clam')
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# Create notebook for tabs
|
||||
self.notebook = ttk.Notebook(self.root)
|
||||
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Main info tab
|
||||
self.main_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(self.main_frame, text="Problem Info")
|
||||
|
||||
# Configure grid weights for responsiveness
|
||||
self.main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(self.main_frame, text="Coding Problem Creator",
|
||||
font=('Arial', 16, 'bold'))
|
||||
title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
|
||||
|
||||
# Problem Name
|
||||
ttk.Label(self.main_frame, text="Problem Name:", font=('Arial', 10, 'bold')).grid(
|
||||
row=1, column=0, sticky=tk.W, pady=(0, 5))
|
||||
self.problem_name = ttk.Entry(self.main_frame, width=40, font=('Arial', 10))
|
||||
self.problem_name.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
|
||||
# Difficulty
|
||||
ttk.Label(self.main_frame, text="Difficulty:", font=('Arial', 10, 'bold')).grid(
|
||||
row=2, column=0, sticky=tk.W, pady=(0, 5))
|
||||
self.difficulty = ttk.Combobox(self.main_frame, values=["easy", "medium", "hard"],
|
||||
state="readonly", width=15)
|
||||
self.difficulty.set("medium")
|
||||
self.difficulty.grid(row=2, column=1, sticky=tk.W, pady=(0, 10))
|
||||
|
||||
# Description tab
|
||||
self.desc_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(self.desc_frame, text="Description")
|
||||
|
||||
# Create markdown editor for description
|
||||
self.description = MarkdownEditor(self.desc_frame)
|
||||
|
||||
# Test Code tab
|
||||
self.test_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(self.test_frame, text="Test Code")
|
||||
|
||||
# Create code editor for test code
|
||||
self.test_code = CodeEditor(self.test_frame, width=80, height=20)
|
||||
self.test_code.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Insert template code
|
||||
template_code = '''import unittest
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
def test(self):
|
||||
# Write your test cases here
|
||||
# Example:
|
||||
# from solution import your_function
|
||||
# self.assertEqual(your_function("input"), "expected_output")
|
||||
pass'''
|
||||
|
||||
self.test_code.insert("1.0", template_code)
|
||||
if PYGMENTS_AVAILABLE:
|
||||
self.test_code.highlight_syntax()
|
||||
|
||||
# Buttons frame at the bottom
|
||||
button_frame = ttk.Frame(self.root)
|
||||
button_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
create_button = ttk.Button(button_frame, text="Create Problem",
|
||||
command=self.create_problem)
|
||||
create_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
clear_button = ttk.Button(button_frame, text="Clear All",
|
||||
command=self.clear_all)
|
||||
clear_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
load_button = ttk.Button(button_frame, text="Load Existing",
|
||||
command=self.load_existing)
|
||||
load_button.pack(side=tk.LEFT)
|
||||
|
||||
# Status bar
|
||||
self.status_var = tk.StringVar()
|
||||
self.status_var.set("Ready to create a new problem...")
|
||||
status_bar = ttk.Label(self.root, textvariable=self.status_var,
|
||||
relief=tk.SUNKEN, anchor=tk.W)
|
||||
status_bar.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||
|
||||
def show_help(self):
|
||||
help_text = """Test Code Help:
|
||||
|
||||
Your test code should follow this structure:
|
||||
|
||||
import unittest
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
def test(self):
|
||||
# Import your solution function
|
||||
from solution import your_function_name
|
||||
|
||||
# Write test assertions
|
||||
self.assertEqual(your_function_name(input), expected_output)
|
||||
self.assertTrue(condition)
|
||||
self.assertFalse(condition)
|
||||
|
||||
Tips:
|
||||
- Replace 'your_function_name' with the actual function name
|
||||
- Add multiple test cases with different inputs
|
||||
- Use descriptive variable names
|
||||
- Test edge cases (empty inputs, large inputs, etc.)
|
||||
|
||||
Example for a palindrome checker:
|
||||
from solution import is_palindrome
|
||||
self.assertTrue(is_palindrome("racecar"))
|
||||
self.assertFalse(is_palindrome("hello"))
|
||||
self.assertTrue(is_palindrome(""))
|
||||
"""
|
||||
messagebox.showinfo("Test Code Help", help_text)
|
||||
|
||||
def validate_inputs(self):
|
||||
if not self.problem_name.get().strip():
|
||||
messagebox.showerror("Error", "Problem name is required!")
|
||||
return False
|
||||
|
||||
if not self.description.get("1.0", tk.END).strip():
|
||||
messagebox.showerror("Error", "Description is required!")
|
||||
return False
|
||||
|
||||
test_code = self.test_code.get("1.0", tk.END).strip()
|
||||
if not test_code or test_code == "pass" or len(test_code) < 50:
|
||||
messagebox.showerror("Error", "Please provide proper test code!")
|
||||
return False
|
||||
|
||||
# Validate problem name (should be filesystem-safe)
|
||||
name = self.problem_name.get().strip()
|
||||
if not name.replace("_", "").replace("-", "").replace(" ", "").isalnum():
|
||||
messagebox.showerror("Error", "Problem name should only contain letters, numbers, spaces, hyphens, and underscores!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_problem(self):
|
||||
if not self.validate_inputs():
|
||||
return
|
||||
|
||||
try:
|
||||
# Get values
|
||||
problem_name = self.problem_name.get().strip()
|
||||
description_text = self.description.get("1.0", tk.END).strip()
|
||||
difficulty = self.difficulty.get()
|
||||
test_code = self.test_code.get("1.0", tk.END).strip()
|
||||
|
||||
# Create safe folder name (replace spaces with underscores)
|
||||
folder_name = problem_name.replace(" ", "_")
|
||||
|
||||
# Create directory structure
|
||||
base_path = Path("src/problems")
|
||||
problem_path = base_path / folder_name
|
||||
|
||||
# Create directories if they don't exist
|
||||
problem_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create manifest.json
|
||||
manifest = {
|
||||
"title": problem_name,
|
||||
"description": description_text,
|
||||
"description_md": f"problems/{folder_name}/description.md",
|
||||
"test_code": f"problems/{folder_name}/test.py",
|
||||
"difficulty": difficulty
|
||||
}
|
||||
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(manifest, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Create description.md
|
||||
description_md_path = problem_path / "description.md"
|
||||
with open(description_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(description_text)
|
||||
|
||||
# Create test.py
|
||||
test_py_path = problem_path / "test.py"
|
||||
with open(test_py_path, 'w', encoding='utf-8') as f:
|
||||
f.write(test_code)
|
||||
|
||||
# Create empty solution.py template
|
||||
solution_py_path = problem_path / "solution.py"
|
||||
if not solution_py_path.exists():
|
||||
with open(solution_py_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# {problem_name} Solution\n")
|
||||
f.write("# Implement your solution here\n\n")
|
||||
f.write("def your_function():\n")
|
||||
f.write(" pass\n")
|
||||
|
||||
self.status_var.set(f"✓ Problem '{problem_name}' created successfully in {problem_path}")
|
||||
|
||||
result = messagebox.askyesno("Success",
|
||||
f"Problem '{problem_name}' created successfully!\n\n"
|
||||
f"Location: {problem_path}\n\n"
|
||||
"Would you like to open the folder?")
|
||||
|
||||
if result:
|
||||
self.open_folder(problem_path)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error creating problem: {str(e)}"
|
||||
self.status_var.set(error_msg)
|
||||
messagebox.showerror("Error", error_msg)
|
||||
|
||||
def open_folder(self, path):
|
||||
"""Cross-platform folder opening"""
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(path)
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
subprocess.run(["open", path])
|
||||
else: # Linux and other Unix-like
|
||||
subprocess.run(["xdg-open", path])
|
||||
except Exception as e:
|
||||
messagebox.showwarning("Warning", f"Could not open folder: {str(e)}")
|
||||
|
||||
def clear_all(self):
|
||||
result = messagebox.askyesno("Confirm", "Clear all fields?")
|
||||
if result:
|
||||
self.problem_name.delete(0, tk.END)
|
||||
self.description.delete("1.0", tk.END)
|
||||
self.difficulty.set("medium")
|
||||
self.test_code.delete("1.0", tk.END)
|
||||
# Re-insert template
|
||||
template_code = '''import unittest
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
def test(self):
|
||||
# Write your test cases here
|
||||
# Example:
|
||||
# from solution import your_function
|
||||
# self.assertEqual(your_function("input"), "expected_output")
|
||||
pass'''
|
||||
self.test_code.insert("1.0", template_code)
|
||||
if PYGMENTS_AVAILABLE:
|
||||
self.test_code.highlight_syntax()
|
||||
self.status_var.set("All fields cleared.")
|
||||
|
||||
def load_existing(self):
|
||||
try:
|
||||
base_path = Path("src/problems")
|
||||
if not base_path.exists():
|
||||
messagebox.showwarning("Warning", "No problems directory found!")
|
||||
return
|
||||
|
||||
# Get list of existing problems
|
||||
problems = [d.name for d in base_path.iterdir() if d.is_dir()]
|
||||
|
||||
if not problems:
|
||||
messagebox.showinfo("Info", "No existing problems found!")
|
||||
return
|
||||
|
||||
# Create selection dialog
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Load Existing Problem")
|
||||
dialog.geometry("400x300")
|
||||
dialog.transient(self.root)
|
||||
dialog.grab_set()
|
||||
|
||||
ttk.Label(dialog, text="Select a problem to load:",
|
||||
font=('Arial', 10, 'bold')).pack(pady=10)
|
||||
|
||||
# Listbox with scrollbar
|
||||
frame = ttk.Frame(dialog)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
|
||||
|
||||
scrollbar = ttk.Scrollbar(frame)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
listbox = tk.Listbox(frame, yscrollcommand=scrollbar.set)
|
||||
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.config(command=listbox.yview)
|
||||
|
||||
for problem in sorted(problems):
|
||||
listbox.insert(tk.END, problem)
|
||||
|
||||
def load_selected():
|
||||
selection = listbox.curselection()
|
||||
if not selection:
|
||||
messagebox.showwarning("Warning", "Please select a problem!")
|
||||
return
|
||||
|
||||
selected_problem = problems[selection[0]]
|
||||
self.load_problem_data(selected_problem)
|
||||
dialog.destroy()
|
||||
|
||||
button_frame = ttk.Frame(dialog)
|
||||
button_frame.pack(pady=10)
|
||||
|
||||
ttk.Button(button_frame, text="Load", command=load_selected).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Error loading existing problems: {str(e)}")
|
||||
|
||||
def load_problem_data(self, problem_name):
|
||||
try:
|
||||
problem_path = Path("src/problems") / problem_name
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
test_path = problem_path / "test.py"
|
||||
desc_path = problem_path / "description.md"
|
||||
|
||||
# Load manifest
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# Load test code
|
||||
test_code = ""
|
||||
if test_path.exists():
|
||||
with open(test_path, 'r', encoding='utf-8') as f:
|
||||
test_code = f.read()
|
||||
|
||||
# Load description
|
||||
description = ""
|
||||
if desc_path.exists():
|
||||
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||
description = f.read()
|
||||
|
||||
# Populate fields
|
||||
self.problem_name.delete(0, tk.END)
|
||||
self.problem_name.insert(0, manifest["title"])
|
||||
|
||||
self.description.delete("1.0", tk.END)
|
||||
self.description.insert("1.0", description)
|
||||
|
||||
self.difficulty.set(manifest["difficulty"])
|
||||
|
||||
self.test_code.delete("1.0", tk.END)
|
||||
self.test_code.insert("1.0", test_code)
|
||||
if PYGMENTS_AVAILABLE:
|
||||
self.test_code.highlight_syntax()
|
||||
|
||||
self.status_var.set(f"Loaded problem: {problem_name}")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Error loading problem data: {str(e)}")
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = ProblemCreatorApp(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
902
qtc.py
Normal file
902
qtc.py
Normal file
@@ -0,0 +1,902 @@
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
|
||||
QLabel, QLineEdit, QComboBox, QTextEdit, QPushButton, QStatusBar,
|
||||
QMessageBox, QDialog, QListWidget, QSplitter, QFrame, QSizePolicy,
|
||||
QScrollArea, QFileDialog, QToolTip
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QSize, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QFont, QTextOption, QSyntaxHighlighter, QTextCharFormat, QColor, QTextCursor, QKeyEvent, QTextDocument
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("ProblemCreator")
|
||||
|
||||
# Optional imports with proper error handling
|
||||
OPTIONAL_DEPENDENCIES = {}
|
||||
|
||||
try:
|
||||
import markdown
|
||||
OPTIONAL_DEPENDENCIES['markdown'] = True
|
||||
except ImportError:
|
||||
logger.warning("Markdown library not available, using basic conversion")
|
||||
OPTIONAL_DEPENDENCIES['markdown'] = False
|
||||
|
||||
try:
|
||||
from pygments import lex
|
||||
from pygments.lexers import PythonLexer
|
||||
from pygments.styles import get_style_by_name
|
||||
OPTIONAL_DEPENDENCIES['pygments'] = True
|
||||
except ImportError:
|
||||
logger.warning("Pygments not available, syntax highlighting will be disabled")
|
||||
OPTIONAL_DEPENDENCIES['pygments'] = False
|
||||
|
||||
|
||||
class CodeEditor(QTextEdit):
|
||||
"""A code editor with syntax highlighting and advanced auto-indentation."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFont(QFont("Consolas", 10))
|
||||
self.indent_width = 4
|
||||
self.setTabStopDistance(QFont("Consolas", 10).pointSize() * self.indent_width)
|
||||
|
||||
# Setup syntax highlighting if available
|
||||
if OPTIONAL_DEPENDENCIES.get('pygments', False):
|
||||
self.highlighter = PythonHighlighter(self.document())
|
||||
|
||||
# Tips for test case editing
|
||||
self.tips = [
|
||||
"💡 Tip: Use descriptive test method names like test_empty_input()",
|
||||
"💡 Tip: Test edge cases like empty inputs, large inputs, and invalid inputs",
|
||||
"💡 Tip: Use assertEqual for exact matches, assertTrue/False for boolean checks",
|
||||
"💡 Tip: Test both valid and invalid inputs to ensure robustness",
|
||||
"💡 Tip: Consider using setUp() method for common test setup",
|
||||
"💡 Tip: Use parameterized tests if you have many similar test cases",
|
||||
"💡 Tip: Make sure your tests are independent of each other",
|
||||
"💡 Tip: Test not only for correct outputs but also for proper error handling"
|
||||
]
|
||||
self.current_tip_index = 0
|
||||
self.tip_timer = QTimer(self)
|
||||
self.tip_timer.timeout.connect(self.show_next_tip)
|
||||
self.tip_timer.start(10000) # Show a new tip every 10 seconds
|
||||
|
||||
def show_next_tip(self):
|
||||
"""Show the next tip in the status bar."""
|
||||
if self.parent() and hasattr(self.parent().parent().parent().parent(), 'statusBar'):
|
||||
status_bar = self.parent().parent().parent().parent().statusBar()
|
||||
if status_bar:
|
||||
tip = self.tips[self.current_tip_index]
|
||||
status_bar.showMessage(tip)
|
||||
self.current_tip_index = (self.current_tip_index + 1) % len(self.tips)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent):
|
||||
"""Handle key press events for auto-indentation and pairing."""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
cursor = self.textCursor()
|
||||
|
||||
# Tab key
|
||||
if key == Qt.Key.Key_Tab:
|
||||
if cursor.hasSelection():
|
||||
self.indentSelection()
|
||||
else:
|
||||
# Insert spaces
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
return
|
||||
|
||||
# Shift+Tab key
|
||||
elif key == Qt.Key.Key_Backtab:
|
||||
if cursor.hasSelection():
|
||||
self.dedentSelection()
|
||||
else:
|
||||
self.dedentLine()
|
||||
return
|
||||
|
||||
# Return key
|
||||
elif key == Qt.Key.Key_Return:
|
||||
# Get current line
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Calculate indentation
|
||||
indent = len(line_text) - len(line_text.lstrip())
|
||||
|
||||
# Check if line ends with colon
|
||||
ends_with_colon = line_text.rstrip().endswith(':')
|
||||
|
||||
# Insert newline with indentation
|
||||
cursor = self.textCursor()
|
||||
cursor.insertText("\n" + " " * indent)
|
||||
|
||||
# Add extra indentation if line ended with colon
|
||||
if ends_with_colon:
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
return
|
||||
|
||||
# Auto-pairing
|
||||
elif key == Qt.Key.Key_ParenLeft:
|
||||
cursor.insertText("()")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_BracketLeft:
|
||||
cursor.insertText("[]")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_BraceLeft:
|
||||
cursor.insertText("{}")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_QuoteDbl:
|
||||
cursor.insertText('""')
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_Apostrophe:
|
||||
cursor.insertText("''")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_Colon and modifiers == Qt.KeyboardModifier.NoModifier:
|
||||
# Check if we're at the end of the line
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine)
|
||||
if self.textCursor().position() == cursor.position():
|
||||
cursor.insertText(":")
|
||||
return
|
||||
|
||||
# Default behavior
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def indentSelection(self):
|
||||
"""Indent all selected lines."""
|
||||
cursor = self.textCursor()
|
||||
start = cursor.selectionStart()
|
||||
end = cursor.selectionEnd()
|
||||
|
||||
# Move to start of selection
|
||||
cursor.setPosition(start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Indent each line in selection
|
||||
while cursor.position() <= end:
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
end += self.indent_width
|
||||
if not cursor.movePosition(QTextCursor.MoveOperation.Down):
|
||||
break
|
||||
|
||||
# Restore selection
|
||||
cursor.setPosition(start)
|
||||
cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def dedentSelection(self):
|
||||
"""Dedent all selected lines."""
|
||||
cursor = self.textCursor()
|
||||
start = cursor.selectionStart()
|
||||
end = cursor.selectionEnd()
|
||||
|
||||
# Move to start of selection
|
||||
cursor.setPosition(start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Dedent each line in selection
|
||||
while cursor.position() <= end:
|
||||
# Check for spaces at beginning of line
|
||||
line_start = cursor.position()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Count leading spaces
|
||||
leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width)
|
||||
|
||||
if leading_spaces > 0:
|
||||
# Remove leading spaces
|
||||
cursor.setPosition(line_start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces)
|
||||
cursor.removeSelectedText()
|
||||
end -= leading_spaces
|
||||
|
||||
if not cursor.movePosition(QTextCursor.MoveOperation.Down):
|
||||
break
|
||||
|
||||
# Restore selection
|
||||
cursor.setPosition(max(0, start - self.indent_width))
|
||||
cursor.setPosition(max(0, end - self.indent_width), QTextCursor.MoveMode.KeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def dedentLine(self):
|
||||
"""Dedent the current line."""
|
||||
cursor = self.textCursor()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Check for spaces at beginning of line
|
||||
line_start = cursor.position()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Count leading spaces
|
||||
leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width)
|
||||
|
||||
if leading_spaces > 0:
|
||||
# Remove leading spaces
|
||||
cursor.setPosition(line_start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
|
||||
class PythonHighlighter(QSyntaxHighlighter):
|
||||
"""Syntax highlighter for Python code using Pygments."""
|
||||
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self._setup_formats()
|
||||
|
||||
def _setup_formats(self):
|
||||
"""Setup text formats for different token types."""
|
||||
self.formats = {}
|
||||
|
||||
# Define syntax highlighting formats
|
||||
keyword_format = QTextCharFormat()
|
||||
keyword_format.setForeground(QColor("#0000FF"))
|
||||
keyword_format.setFontWeight(QFont.Weight.Bold)
|
||||
self.formats['keyword'] = keyword_format
|
||||
|
||||
string_format = QTextCharFormat()
|
||||
string_format.setForeground(QColor("#008000"))
|
||||
self.formats['string'] = string_format
|
||||
|
||||
comment_format = QTextCharFormat()
|
||||
comment_format.setForeground(QColor("#808080"))
|
||||
comment_format.setFontItalic(True)
|
||||
self.formats['comment'] = comment_format
|
||||
|
||||
function_format = QTextCharFormat()
|
||||
function_format.setForeground(QColor("#000080"))
|
||||
function_format.setFontWeight(QFont.Weight.Bold)
|
||||
self.formats['function'] = function_format
|
||||
|
||||
number_format = QTextCharFormat()
|
||||
number_format.setForeground(QColor("#FF8C00"))
|
||||
self.formats['number'] = number_format
|
||||
|
||||
# Python keywords
|
||||
self.keywords = [
|
||||
'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del',
|
||||
'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global',
|
||||
'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or',
|
||||
'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'
|
||||
]
|
||||
|
||||
# unittest keywords
|
||||
self.unittest_keywords = [
|
||||
'TestCase', 'setUp', 'tearDown', 'setUpClass', 'tearDownClass',
|
||||
'assertEqual', 'assertTrue', 'assertFalse', 'assertRaises',
|
||||
'assertAlmostEqual', 'assertNotEqual', 'assertIn', 'assertNotIn',
|
||||
'assertIs', 'assertIsNot', 'assertIsNone', 'assertIsNotNone',
|
||||
'assertIsInstance', 'assertNotIsInstance', 'assertDictEqual',
|
||||
'assertListEqual', 'assertTupleEqual', 'assertSetEqual',
|
||||
'assertSequenceEqual', 'assertMultiLineEqual', 'assertGreater',
|
||||
'assertGreaterEqual', 'assertLess', 'assertLessEqual', 'assertRegex',
|
||||
'assertNotRegex', 'assertCountEqual'
|
||||
]
|
||||
|
||||
def highlightBlock(self, text):
|
||||
"""Apply syntax highlighting to the current text block."""
|
||||
# Check if we should use pygments
|
||||
if OPTIONAL_DEPENDENCIES.get('pygments', False):
|
||||
self._highlight_with_pygments(text)
|
||||
else:
|
||||
self._highlight_with_basic_rules(text)
|
||||
|
||||
def _highlight_with_pygments(self, text):
|
||||
"""Use pygments for syntax highlighting if available."""
|
||||
try:
|
||||
# Get the text from the current block
|
||||
block = self.currentBlock()
|
||||
start_pos = block.position()
|
||||
end_pos = start_pos + len(text)
|
||||
full_text = self.document().toPlainText()
|
||||
|
||||
# Lex the code and apply formats
|
||||
for token, value in lex(full_text, PythonLexer()):
|
||||
token_str = str(token)
|
||||
token_start = full_text.find(value, start_pos)
|
||||
|
||||
# Skip if token is not in current block
|
||||
if token_start < start_pos or token_start >= end_pos:
|
||||
continue
|
||||
|
||||
# Calculate length within current block
|
||||
token_len = min(len(value), end_pos - token_start)
|
||||
|
||||
# Apply appropriate format
|
||||
if 'Keyword' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['keyword'])
|
||||
elif 'String' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['string'])
|
||||
elif 'Comment' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['comment'])
|
||||
elif 'Name' in token_str and 'Function' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['function'])
|
||||
elif 'Number' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['number'])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during pygments highlighting: {e}")
|
||||
# Fall back to basic highlighting
|
||||
self._highlight_with_basic_rules(text)
|
||||
|
||||
def _highlight_with_basic_rules(self, text):
|
||||
"""Use basic rules for syntax highlighting."""
|
||||
# Highlight keywords
|
||||
for keyword in self.keywords + self.unittest_keywords:
|
||||
pattern = r'\b' + keyword + r'\b'
|
||||
index = 0
|
||||
while index < len(text):
|
||||
index = text.find(keyword, index)
|
||||
if index == -1:
|
||||
break
|
||||
|
||||
# Check if it's really a word (not part of another word)
|
||||
if (index == 0 or not text[index-1].isalnum()) and \
|
||||
(index + len(keyword) >= len(text) or not text[index + len(keyword)].isalnum()):
|
||||
if keyword in self.keywords:
|
||||
self.setFormat(index, len(keyword), self.formats['keyword'])
|
||||
else:
|
||||
self.setFormat(index, len(keyword), self.formats['function'])
|
||||
|
||||
index += len(keyword)
|
||||
|
||||
# Highlight strings
|
||||
import re
|
||||
string_pattern = re.compile(r'(\".*?\")|(\'.*?\')')
|
||||
for match in string_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['string'])
|
||||
|
||||
# Highlight comments
|
||||
comment_pattern = re.compile(r'#.*')
|
||||
for match in comment_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['comment'])
|
||||
|
||||
# Highlight numbers
|
||||
number_pattern = re.compile(r'\b\d+\b')
|
||||
for match in number_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['number'])
|
||||
|
||||
|
||||
class MarkdownEditor(QWidget):
|
||||
"""A markdown editor with live preview."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Create split view
|
||||
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.splitter)
|
||||
|
||||
# Left side - text editor
|
||||
self.editor = QTextEdit()
|
||||
self.editor.setFont(QFont("Consolas", 10))
|
||||
self.editor.textChanged.connect(self.update_preview)
|
||||
self.splitter.addWidget(self.editor)
|
||||
|
||||
# Right side - preview
|
||||
self.preview = QWebEngineView()
|
||||
self.splitter.addWidget(self.preview)
|
||||
|
||||
# Set initial sizes
|
||||
self.splitter.setSizes([400, 400])
|
||||
|
||||
def update_preview(self):
|
||||
"""Update the markdown preview."""
|
||||
# Get the markdown text
|
||||
markdown_text = self.editor.toPlainText()
|
||||
|
||||
# Convert to HTML
|
||||
html_content = self._markdown_to_html(markdown_text)
|
||||
|
||||
# Update the preview
|
||||
self.preview.setHtml(html_content)
|
||||
|
||||
def _markdown_to_html(self, text):
|
||||
"""Convert markdown text to HTML."""
|
||||
if OPTIONAL_DEPENDENCIES.get('markdown', False):
|
||||
# Use the markdown library if available
|
||||
html = markdown.markdown(text)
|
||||
else:
|
||||
# Fallback to basic conversion
|
||||
html = text
|
||||
html = html.replace("# ", "<h1>").replace("\n# ", "</h1>\n<h1>") + "</h1>"
|
||||
html = html.replace("## ", "<h2>").replace("\n## ", "</h2>\n<h2>") + "</h2>"
|
||||
html = html.replace("### ", "<h3>").replace("\n### ", "</h3>\n<h3>") + "</h3>"
|
||||
html = html.replace("**", "<strong>").replace("**", "</strong>")
|
||||
html = html.replace("*", "<em>").replace("*", "</em>")
|
||||
html = html.replace("`", "<code>").replace("`", "</code>")
|
||||
html = html.replace("\n", "<br>")
|
||||
|
||||
# Wrap in proper HTML structure
|
||||
return f"""
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
code {{
|
||||
background-color: #f6f8fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', monospace;
|
||||
}}
|
||||
pre {{
|
||||
background-color: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
}}
|
||||
pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
}}
|
||||
blockquote {{
|
||||
border-left: 4px solid #ddd;
|
||||
margin-left: 0;
|
||||
padding-left: 16px;
|
||||
color: #666;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def setPlainText(self, text):
|
||||
"""Set the editor text."""
|
||||
self.editor.setPlainText(text)
|
||||
|
||||
def toPlainText(self):
|
||||
"""Get the editor text."""
|
||||
return self.editor.toPlainText()
|
||||
|
||||
|
||||
class LoadProblemDialog(QDialog):
|
||||
"""Dialog for loading existing problems."""
|
||||
|
||||
def __init__(self, problems, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Load Existing Problem")
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
label = QLabel("Select a problem to load:")
|
||||
label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
layout.addWidget(label)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.addItems(sorted(problems))
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.load_button = QPushButton("Load")
|
||||
self.load_button.clicked.connect(self.accept)
|
||||
button_layout.addWidget(self.load_button)
|
||||
|
||||
self.cancel_button = QPushButton("Cancel")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def selected_problem(self):
|
||||
"""Get the selected problem name."""
|
||||
items = self.list_widget.selectedItems()
|
||||
return items[0].text() if items else None
|
||||
|
||||
|
||||
class ProblemCreatorApp(QMainWindow):
|
||||
"""Main application for creating coding problems."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Coding Problem Creator")
|
||||
self.setGeometry(100, 100, 1200, 900)
|
||||
|
||||
# Set default paths
|
||||
self.base_path = Path("src/problems")
|
||||
|
||||
# Initialize UI
|
||||
self.create_widgets()
|
||||
self.statusBar().showMessage("Ready to create a new problem...")
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create all UI widgets."""
|
||||
# Central widget
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
|
||||
# Create tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Problem Info tab
|
||||
self.info_tab = QWidget()
|
||||
self.tab_widget.addTab(self.info_tab, "Problem Info")
|
||||
self.create_info_tab()
|
||||
|
||||
# Markdown Description tab
|
||||
self.markdown_tab = QWidget()
|
||||
self.tab_widget.addTab(self.markdown_tab, "Markdown Description")
|
||||
self.create_markdown_tab()
|
||||
|
||||
# Test Code tab
|
||||
self.test_tab = QWidget()
|
||||
self.tab_widget.addTab(self.test_tab, "Test Code")
|
||||
self.create_test_tab()
|
||||
|
||||
# Buttons at the bottom
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.create_button = QPushButton("Create Problem")
|
||||
self.create_button.clicked.connect(self.create_problem)
|
||||
button_layout.addWidget(self.create_button)
|
||||
|
||||
self.clear_button = QPushButton("Clear All")
|
||||
self.clear_button.clicked.connect(self.clear_all)
|
||||
button_layout.addWidget(self.clear_button)
|
||||
|
||||
self.load_button = QPushButton("Load Existing")
|
||||
self.load_button.clicked.connect(self.load_existing)
|
||||
button_layout.addWidget(self.load_button)
|
||||
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
def create_info_tab(self):
|
||||
"""Create the Problem Info tab."""
|
||||
layout = QVBoxLayout(self.info_tab)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Coding Problem Creator")
|
||||
title_font = QFont("Arial", 16, QFont.Weight.Bold)
|
||||
title_label.setFont(title_font)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Problem Name
|
||||
name_layout = QHBoxLayout()
|
||||
name_label = QLabel("Problem Name:")
|
||||
name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
name_layout.addWidget(name_label)
|
||||
|
||||
self.problem_name = QLineEdit()
|
||||
self.problem_name.setFont(QFont("Arial", 10))
|
||||
name_layout.addWidget(self.problem_name)
|
||||
layout.addLayout(name_layout)
|
||||
|
||||
# Difficulty
|
||||
difficulty_layout = QHBoxLayout()
|
||||
difficulty_label = QLabel("Difficulty:")
|
||||
difficulty_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
difficulty_layout.addWidget(difficulty_label)
|
||||
|
||||
self.difficulty = QComboBox()
|
||||
self.difficulty.addItems(["easy", "medium", "hard"])
|
||||
self.difficulty.setCurrentText("medium")
|
||||
difficulty_layout.addWidget(self.difficulty)
|
||||
difficulty_layout.addStretch()
|
||||
layout.addLayout(difficulty_layout)
|
||||
|
||||
# Plain Text Description
|
||||
desc_label = QLabel("Plain Text Description:")
|
||||
desc_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
self.description_text = QTextEdit()
|
||||
self.description_text.setFont(QFont("Arial", 10))
|
||||
self.description_text.setAcceptRichText(False)
|
||||
layout.addWidget(self.description_text)
|
||||
|
||||
def create_markdown_tab(self):
|
||||
"""Create the Markdown Description tab."""
|
||||
layout = QVBoxLayout(self.markdown_tab)
|
||||
self.description_editor = MarkdownEditor()
|
||||
layout.addWidget(self.description_editor)
|
||||
|
||||
def create_test_tab(self):
|
||||
"""Create the Test Code tab."""
|
||||
layout = QVBoxLayout(self.test_tab)
|
||||
|
||||
# Add tips label
|
||||
tips_label = QLabel("💡 Tips for writing good test cases will appear in the status bar")
|
||||
tips_label.setFont(QFont("Arial", 9))
|
||||
tips_label.setStyleSheet("color: #666; padding: 5px;")
|
||||
layout.addWidget(tips_label)
|
||||
|
||||
self.test_code_editor = CodeEditor()
|
||||
layout.addWidget(self.test_code_editor)
|
||||
|
||||
# Insert template code
|
||||
self._insert_template_code()
|
||||
|
||||
def _insert_template_code(self):
|
||||
"""Insert template test code into the editor."""
|
||||
template_code = '''import unittest
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_example_case(self):
|
||||
"""
|
||||
Test the provided example case.
|
||||
"""
|
||||
solution = Solution()
|
||||
result = solution.solve("input")
|
||||
self.assertEqual(result, "expected_output")
|
||||
|
||||
def test_edge_case_empty_input(self):
|
||||
"""
|
||||
Test with empty input.
|
||||
"""
|
||||
solution = Solution()
|
||||
result = solution.solve("")
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_edge_case_large_input(self):
|
||||
"""
|
||||
Test with a large input to check performance.
|
||||
"""
|
||||
solution = Solution()
|
||||
large_input = "a" * 1000
|
||||
result = solution.solve(large_input)
|
||||
self.assertTrue(result) # Adjust based on expected behavior
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
'''
|
||||
self.test_code_editor.setPlainText(template_code)
|
||||
|
||||
def validate_inputs(self):
|
||||
"""Validate all form inputs."""
|
||||
if not self.problem_name.text().strip():
|
||||
QMessageBox.critical(self, "Error", "Problem name is required!")
|
||||
return False
|
||||
|
||||
if not self.description_text.toPlainText().strip():
|
||||
QMessageBox.critical(self, "Error", "Plain text description is required!")
|
||||
return False
|
||||
|
||||
if not self.description_editor.toPlainText().strip():
|
||||
QMessageBox.critical(self, "Error", "Markdown description is required!")
|
||||
return False
|
||||
|
||||
test_code = self.test_code_editor.toPlainText().strip()
|
||||
if not test_code or "pass" in test_code and len(test_code) < 100:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm",
|
||||
"The test code seems minimal. Are you sure you want to proceed?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return False
|
||||
|
||||
# Validate problem name (should be filesystem-safe)
|
||||
name = self.problem_name.text().strip()
|
||||
if not name.replace("_", "").replace("-", "").replace(" ", "").isalnum():
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Error",
|
||||
"Problem name should only contain letters, numbers, spaces, hyphens, and underscores!"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_problem(self):
|
||||
"""Create a new problem from the form data."""
|
||||
if not self.validate_inputs():
|
||||
return
|
||||
|
||||
try:
|
||||
# Get values
|
||||
problem_name = self.problem_name.text().strip()
|
||||
description_text = self.description_text.toPlainText().strip() # Plain text
|
||||
description_md = self.description_editor.toPlainText().strip() # Markdown
|
||||
difficulty = self.difficulty.currentText()
|
||||
test_code = self.test_code_editor.toPlainText().strip()
|
||||
|
||||
# Create safe folder name (replace spaces with underscores)
|
||||
folder_name = problem_name.replace(" ", "_").lower()
|
||||
|
||||
# Create directory structure
|
||||
problem_path = self.base_path / folder_name
|
||||
|
||||
# Create directories if they don't exist
|
||||
problem_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create manifest.json - Include both description fields
|
||||
manifest = {
|
||||
"title": problem_name,
|
||||
"description": description_text, # Plain text description
|
||||
"description_md": f"problems/{folder_name}/description.md", # Markdown file path
|
||||
"test_code": f"problems/{folder_name}/test.py",
|
||||
"difficulty": difficulty
|
||||
}
|
||||
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(manifest, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Create description.md
|
||||
description_md_path = problem_path / "description.md"
|
||||
with open(description_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(description_md)
|
||||
|
||||
# Create test.py
|
||||
test_py_path = problem_path / "test.py"
|
||||
with open(test_py_path, 'w', encoding='utf-8') as f:
|
||||
f.write(test_code)
|
||||
|
||||
self.statusBar().showMessage(f"✓ Problem '{problem_name}' created successfully in {problem_path}")
|
||||
logger.info(f"Created problem: {problem_name} at {problem_path}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Success",
|
||||
f"Problem '{problem_name}' created successfully!\n\n"
|
||||
f"Location: {problem_path}\n\n"
|
||||
"Would you like to open the folder?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.open_folder(problem_path)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error creating problem: {str(e)}"
|
||||
self.statusBar().showMessage(error_msg)
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
def open_folder(self, path):
|
||||
"""Cross-platform folder opening."""
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.startfile(path)
|
||||
elif sys.platform == "darwin": # macOS
|
||||
os.system(f"open '{path}'")
|
||||
else: # Linux and other Unix-like
|
||||
os.system(f"xdg-open '{path}'")
|
||||
except Exception as e:
|
||||
error_msg = f"Could not open folder: {str(e)}"
|
||||
logger.warning(error_msg)
|
||||
QMessageBox.warning(self, "Warning", error_msg)
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear all form fields."""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm",
|
||||
"Clear all fields?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.problem_name.clear()
|
||||
self.description_text.clear()
|
||||
self.description_editor.setPlainText("")
|
||||
self.difficulty.setCurrentText("medium")
|
||||
self.test_code_editor.clear()
|
||||
|
||||
# Re-insert template
|
||||
self._insert_template_code()
|
||||
|
||||
self.statusBar().showMessage("All fields cleared.")
|
||||
logger.info("Cleared all form fields")
|
||||
|
||||
def load_existing(self):
|
||||
"""Load an existing problem for editing."""
|
||||
try:
|
||||
if not self.base_path.exists():
|
||||
QMessageBox.warning(self, "Warning", "No problems directory found!")
|
||||
return
|
||||
|
||||
# Get list of existing problems
|
||||
problems = [d.name for d in self.base_path.iterdir() if d.is_dir()]
|
||||
|
||||
if not problems:
|
||||
QMessageBox.information(self, "Info", "No existing problems found!")
|
||||
return
|
||||
|
||||
# Create and show selection dialog
|
||||
dialog = LoadProblemDialog(problems, self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
selected_problem = dialog.selected_problem()
|
||||
if selected_problem:
|
||||
self.load_problem_data(selected_problem)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error loading existing problems: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
def load_problem_data(self, problem_name):
|
||||
"""Load problem data into the form."""
|
||||
try:
|
||||
problem_path = self.base_path / problem_name
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
test_path = problem_path / "test.py"
|
||||
desc_path = problem_path / "description.md"
|
||||
|
||||
# Load manifest
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# Load test code
|
||||
test_code = ""
|
||||
if test_path.exists():
|
||||
with open(test_path, 'r', encoding='utf-8') as f:
|
||||
test_code = f.read()
|
||||
|
||||
# Load markdown description
|
||||
description_md = ""
|
||||
if desc_path.exists():
|
||||
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||
description_md = f.read()
|
||||
|
||||
# Load plain text description from manifest
|
||||
description_text = manifest.get('description', '')
|
||||
|
||||
# Populate fields
|
||||
self.problem_name.setText(manifest["title"])
|
||||
self.description_text.setPlainText(description_text)
|
||||
self.description_editor.setPlainText(description_md)
|
||||
self.difficulty.setCurrentText(manifest["difficulty"])
|
||||
self.test_code_editor.setPlainText(test_code)
|
||||
|
||||
self.statusBar().showMessage(f"Loaded problem: {problem_name}")
|
||||
logger.info(f"Loaded problem: {problem_name}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error loading problem data: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main application entry point."""
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set application style
|
||||
app.setStyle('Fusion')
|
||||
|
||||
window = ProblemCreatorApp()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -165,3 +165,10 @@ if __name__ == "__main__":
|
||||
4. Include **edge cases** (empty strings, wrong formats)
|
||||
5. **Print results** clearly for easier debugging
|
||||
6. **Catch exceptions** and display failing input before raising
|
||||
|
||||
|
||||
### What has changed for ease of use:
|
||||
|
||||
If you want to really easily create or edit programs then you should look at the Qt Programm.
|
||||
It basically acts as a "VsCode" of this platform. After editing / creating i would suggest you look over everything in a serious
|
||||
editor. Its still realtively new.
|
||||
@@ -1,9 +1,11 @@
|
||||
Flask>=3.0
|
||||
Flask-SQLAlchemy>=3.1
|
||||
Flask-Caching>=2.3.1
|
||||
Markdown>=3.6
|
||||
MarkupSafe>=2.1
|
||||
watchdog>=4.0
|
||||
gunicorn>=23.0.0
|
||||
waitress>=3.0.2
|
||||
tkinterweb_html>=1.1.4
|
||||
pillow>=11.3.0
|
||||
pygments>=2.19.2
|
||||
pyqt6>=6.9.1
|
||||
PyQt6-WebEngine>=6.9.0
|
||||
@@ -1,5 +1,19 @@
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Please read as a Developer:
|
||||
* @file script.js
|
||||
* @author rattatwinko
|
||||
* @description "This is the JavaScript "frontend" File for the website.
|
||||
* This handleds nearly every frontend logic / interaction
|
||||
* if you want to change this, then you should be cautious.
|
||||
* This is a complete mess. And its too complex to refactor.
|
||||
* Atleast for me.
|
||||
* @license MIT
|
||||
* You can freely modify this file and distribute it as you wish.
|
||||
*
|
||||
* @todo
|
||||
* - [] Refactor the jeriatric piece of shit code.
|
||||
* ------------------------------------------------------------------
|
||||
* This is the stupid fucking JavaScript, i hate this so fucking much
|
||||
* why the fuck does this need to exsits, idk.
|
||||
*
|
||||
|
||||
17
src/app.py
17
src/app.py
@@ -1,20 +1,27 @@
|
||||
# API endpoint to get problem manifest (description) by folder
|
||||
from markupsafe import Markup
|
||||
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify
|
||||
from flask_caching import Cache
|
||||
import markdown as md
|
||||
import ast
|
||||
from src.models import db, Problem, Solution
|
||||
from src.utils import run_code_against_tests
|
||||
from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
||||
|
||||
|
||||
import os
|
||||
from src.problem_scanner import start_problem_scanner
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__)
|
||||
# Config cache
|
||||
config = {
|
||||
"DEBUG": True,
|
||||
"CACHE_TYPE": "SimpleCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_mapping(config)
|
||||
cache = Cache(app)
|
||||
BASE_DIR = Path(__file__).parent
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{BASE_DIR / 'database' / 'db.sqlite3'}"
|
||||
|
||||
@@ -47,18 +54,22 @@ def api_problem_manifest(folder):
|
||||
|
||||
# I introduce you to the fucking JavaScript shit routes, fuck javascripts
|
||||
@app.route('/JavaScript/<path:filename>')
|
||||
@cache.cached(timeout=300)
|
||||
def serve_js(filename):
|
||||
return send_from_directory('JavaScript', filename)
|
||||
|
||||
@app.route("/script.js")
|
||||
@cache.cached(timeout=300)
|
||||
def script():
|
||||
return send_from_directory("JavaScript", "script.js")
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
@cache.cached()
|
||||
def favicon():
|
||||
return send_from_directory("templates", "favicon.ico")
|
||||
|
||||
@app.route('/')
|
||||
@cache.cached(timeout=300)
|
||||
def index():
|
||||
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
|
||||
222
src/cache.py
Normal file
222
src/cache.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
High-performance in-memory caching module with LRU eviction policy.
|
||||
"""
|
||||
import time
|
||||
from typing import Any, Callable, Optional, Dict, List, Tuple
|
||||
import threading
|
||||
import functools
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CacheEntry:
|
||||
"""Represents a single cache entry with metadata."""
|
||||
|
||||
__slots__ = ('value', 'timestamp', 'expires_at', 'hits')
|
||||
|
||||
def __init__(self, value: Any, timeout: int):
|
||||
self.value = value
|
||||
self.timestamp = time.time()
|
||||
self.expires_at = self.timestamp + timeout
|
||||
self.hits = 0
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if the cache entry has expired."""
|
||||
return time.time() >= self.expires_at
|
||||
|
||||
def hit(self) -> None:
|
||||
"""Increment the hit counter."""
|
||||
self.hits += 1
|
||||
|
||||
class FastMemoryCache:
|
||||
"""
|
||||
High-performance in-memory cache with LRU eviction policy.
|
||||
Thread-safe and optimized for frequent reads.
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = 1000, default_timeout: int = 300):
|
||||
"""
|
||||
Initialize the cache.
|
||||
|
||||
Args:
|
||||
max_size: Maximum number of items to store in cache
|
||||
default_timeout: Default expiration time in seconds
|
||||
"""
|
||||
self.max_size = max_size
|
||||
self.default_timeout = default_timeout
|
||||
self._cache: Dict[str, CacheEntry] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
self._evictions = 0
|
||||
|
||||
# Start background cleaner thread
|
||||
self._cleaner_thread = threading.Thread(target=self._clean_expired, daemon=True)
|
||||
self._cleaner_thread.start()
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
Get a value from the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
Cached value or None if not found/expired
|
||||
"""
|
||||
with self._lock:
|
||||
entry = self._cache.get(key)
|
||||
|
||||
if entry is None:
|
||||
self._misses += 1
|
||||
return None
|
||||
|
||||
if entry.is_expired():
|
||||
del self._cache[key]
|
||||
self._misses += 1
|
||||
self._evictions += 1
|
||||
return None
|
||||
|
||||
entry.hit()
|
||||
self._hits += 1
|
||||
return entry.value
|
||||
|
||||
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> None:
|
||||
"""
|
||||
Set a value in the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
value: Value to cache
|
||||
timeout: Optional timeout in seconds (uses default if None)
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
|
||||
with self._lock:
|
||||
# Evict if cache is full (LRU policy)
|
||||
if len(self._cache) >= self.max_size and key not in self._cache:
|
||||
self._evict_lru()
|
||||
|
||||
self._cache[key] = CacheEntry(value, timeout)
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""
|
||||
Delete a key from the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key to delete
|
||||
|
||||
Returns:
|
||||
True if key was deleted, False if not found
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
self._evictions += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all items from the cache."""
|
||||
with self._lock:
|
||||
self._cache.clear()
|
||||
self._evictions += len(self._cache)
|
||||
|
||||
def _evict_lru(self) -> None:
|
||||
"""Evict the least recently used item from the cache."""
|
||||
if not self._cache:
|
||||
return
|
||||
|
||||
# Find the entry with the fewest hits (simplified LRU)
|
||||
lru_key = min(self._cache.keys(), key=lambda k: self._cache[k].hits)
|
||||
del self._cache[lru_key]
|
||||
self._evictions += 1
|
||||
|
||||
def _clean_expired(self) -> None:
|
||||
"""Background thread to clean expired entries."""
|
||||
while True:
|
||||
time.sleep(60) # Clean every minute
|
||||
with self._lock:
|
||||
expired_keys = [
|
||||
key for key, entry in self._cache.items()
|
||||
if entry.is_expired()
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self._cache[key]
|
||||
self._evictions += 1
|
||||
|
||||
if expired_keys:
|
||||
logger.info(f"Cleaned {len(expired_keys)} expired cache entries")
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get cache statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with cache statistics
|
||||
"""
|
||||
with self._lock:
|
||||
return {
|
||||
'size': len(self._cache),
|
||||
'hits': self._hits,
|
||||
'misses': self._misses,
|
||||
'hit_ratio': self._hits / (self._hits + self._misses) if (self._hits + self._misses) > 0 else 0,
|
||||
'evictions': self._evictions,
|
||||
'max_size': self.max_size
|
||||
}
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
"""Get all cache keys."""
|
||||
with self._lock:
|
||||
return list(self._cache.keys())
|
||||
|
||||
# Global cache instance
|
||||
cache = FastMemoryCache(max_size=2000, default_timeout=300)
|
||||
|
||||
def cached(timeout: Optional[int] = None, unless: Optional[Callable] = None):
|
||||
"""
|
||||
Decorator for caching function results.
|
||||
|
||||
Args:
|
||||
timeout: Cache timeout in seconds
|
||||
unless: Callable that returns True to bypass cache
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Bypass cache if unless condition is met
|
||||
if unless and unless():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Create cache key from function name and arguments
|
||||
key_parts = [func.__module__, func.__name__]
|
||||
key_parts.extend(str(arg) for arg in args)
|
||||
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
||||
key = "|".join(key_parts)
|
||||
|
||||
# Try to get from cache
|
||||
cached_result = cache.get(key)
|
||||
if cached_result is not None:
|
||||
logger.info(f"Cache hit for {func.__name__}")
|
||||
return cached_result
|
||||
|
||||
# Call function and cache result
|
||||
result = func(*args, **kwargs)
|
||||
cache.set(key, result, timeout)
|
||||
logger.info(f"Cache miss for {func.__name__}, caching result")
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def cache_clear() -> None:
|
||||
"""Clear the entire cache."""
|
||||
cache.clear()
|
||||
logger.info("Cache cleared")
|
||||
|
||||
def cache_stats() -> Dict[str, Any]:
|
||||
"""Get cache statistics."""
|
||||
return cache.get_stats()
|
||||
50
src/problems/Hashmaps/description.md
Normal file
50
src/problems/Hashmaps/description.md
Normal file
@@ -0,0 +1,50 @@
|
||||
## 🏷️ Problem: Lost & Found Office
|
||||
|
||||
You are designing a system for a **Lost-and-Found office**.
|
||||
|
||||
* People can **report lost items**, where each item is mapped to the owner’s name.
|
||||
* People can later **claim their item**.
|
||||
* If the item is not found, return `"No item found!"`.
|
||||
|
||||
---
|
||||
|
||||
### Function Signature
|
||||
|
||||
```python
|
||||
class LostAndFound:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def add_item(self, owner: str, item: str) -> None:
|
||||
"""
|
||||
Stores the item with the owner's name.
|
||||
"""
|
||||
|
||||
def claim_item(self, owner: str) -> str:
|
||||
"""
|
||||
Returns the owner's item if it exists, otherwise
|
||||
returns 'No item found!'.
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
office = LostAndFound()
|
||||
office.add_item("Alice", "Umbrella")
|
||||
office.add_item("Bob", "Backpack")
|
||||
|
||||
print(office.claim_item("Alice")) # Output: "Umbrella"
|
||||
print(office.claim_item("Alice")) # Output: "No item found!"
|
||||
print(office.claim_item("Charlie")) # Output: "No item found!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Constraints
|
||||
|
||||
* `1 <= len(owner), len(item) <= 100`
|
||||
* You may assume only **strings** are used for owner and item.
|
||||
* An owner can only have **one item** at a time.
|
||||
7
src/problems/Hashmaps/manifest.json
Normal file
7
src/problems/Hashmaps/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Hashmaps",
|
||||
"description": "DSA - Hashmap. With Lost & Found",
|
||||
"description_md": "problems/Hashmaps/description.md",
|
||||
"test_code": "problems/Hashmaps/test.py",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
54
src/problems/Hashmaps/test.py
Normal file
54
src/problems/Hashmaps/test.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#class LostAndFound:
|
||||
# def __init__(self):
|
||||
# self.items = {} # hashmap: owner -> item
|
||||
#
|
||||
# def add_item(self, owner: str, item: str) -> None:
|
||||
# self.items[owner] = item
|
||||
#
|
||||
# def claim_item(self, owner: str) -> str:
|
||||
# return self.items.pop(owner, "No item found!")
|
||||
|
||||
import unittest
|
||||
|
||||
class TestLostAndFound(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
office = LostAndFound()
|
||||
office.add_item("Alice", "Umbrella")
|
||||
office.add_item("Bob", "Backpack")
|
||||
|
||||
test_cases = [
|
||||
("Alice", "Umbrella", "First claim for Alice"),
|
||||
("Alice", "No item found!", "Alice claims again (should fail)"),
|
||||
("Charlie", "No item found!", "Charlie never added an item"),
|
||||
]
|
||||
|
||||
print("\nTEST: Basic LostAndFound Behavior")
|
||||
for name, expected, description in test_cases:
|
||||
try:
|
||||
actual = office.claim_item(name)
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | {description} | Input: {name} -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | {description} | Input: {name} -> Exception: {e}")
|
||||
raise
|
||||
|
||||
def test_overwrite_item(self):
|
||||
office = LostAndFound()
|
||||
office.add_item("Bob", "Hat")
|
||||
office.add_item("Bob", "Shoes") # overwrite
|
||||
|
||||
print("\nTEST: Overwriting Items")
|
||||
try:
|
||||
actual = office.claim_item("Bob")
|
||||
expected = "Shoes"
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Overwritten item claim | Input: Bob -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Overwritten item claim | Input: Bob -> Exception: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -75,8 +75,10 @@ DANGEROUS_PATTERNS = [
|
||||
# System exit
|
||||
r'exit\s*\(', r'quit\s*\(', r'sys\.exit\s*\(',
|
||||
|
||||
# Dunder methods that could be dangerous
|
||||
r'__.*__\s*\(.*\)', r'\.__.*__',
|
||||
# Dunder methods are dangerous if misused, for us we allow classes
|
||||
# specifically the constructor
|
||||
# del i dont allow tho
|
||||
r'__del__\s*\(',
|
||||
|
||||
# Import tricks
|
||||
r'importlib', r'imp\s', r'pkgutil',
|
||||
|
||||
Reference in New Issue
Block a user