Your fixed commit message here

This commit is contained in:
2025-08-22 19:55:24 +02:00
parent 1ef6bdfd80
commit 4ae5f12633
11 changed files with 1279 additions and 577 deletions

View File

@@ -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
View 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()

View File

@@ -165,3 +165,10 @@ if __name__ == "__main__":
4. Include **edge cases** (empty strings, wrong formats) 4. Include **edge cases** (empty strings, wrong formats)
5. **Print results** clearly for easier debugging 5. **Print results** clearly for easier debugging
6. **Catch exceptions** and display failing input before raising 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.

View File

@@ -1,9 +1,11 @@
Flask>=3.0 Flask>=3.0
Flask-SQLAlchemy>=3.1 Flask-SQLAlchemy>=3.1
Flask-Caching>=2.3.1
Markdown>=3.6 Markdown>=3.6
MarkupSafe>=2.1 MarkupSafe>=2.1
watchdog>=4.0 watchdog>=4.0
gunicorn>=23.0.0 gunicorn>=23.0.0
waitress>=3.0.2 waitress>=3.0.2
tkinterweb_html>=1.1.4 pygments>=2.19.2
pillow>=11.3.0 pyqt6>=6.9.1
PyQt6-WebEngine>=6.9.0

View File

@@ -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 * This is the stupid fucking JavaScript, i hate this so fucking much
* why the fuck does this need to exsits, idk. * why the fuck does this need to exsits, idk.
* *

View File

@@ -1,20 +1,27 @@
# API endpoint to get problem manifest (description) by folder # API endpoint to get problem manifest (description) by folder
from markupsafe import Markup from markupsafe import Markup
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify
from flask_caching import Cache
import markdown as md import markdown as md
import ast import ast
from src.models import db, Problem, Solution from src.models import db, Problem, Solution
from src.utils import run_code_against_tests from src.utils import run_code_against_tests
from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
import os import os
from src.problem_scanner import start_problem_scanner from src.problem_scanner import start_problem_scanner
import sqlite3 import sqlite3
from pathlib import Path 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 BASE_DIR = Path(__file__).parent
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{BASE_DIR / 'database' / 'db.sqlite3'}" 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 # I introduce you to the fucking JavaScript shit routes, fuck javascripts
@app.route('/JavaScript/<path:filename>') @app.route('/JavaScript/<path:filename>')
@cache.cached(timeout=300)
def serve_js(filename): def serve_js(filename):
return send_from_directory('JavaScript', filename) return send_from_directory('JavaScript', filename)
@app.route("/script.js") @app.route("/script.js")
@cache.cached(timeout=300)
def script(): def script():
return send_from_directory("JavaScript", "script.js") return send_from_directory("JavaScript", "script.js")
@app.route('/favicon.ico') @app.route('/favicon.ico')
@cache.cached()
def favicon(): def favicon():
return send_from_directory("templates", "favicon.ico") return send_from_directory("templates", "favicon.ico")
@app.route('/') @app.route('/')
@cache.cached(timeout=300)
def index(): def index():
db_path = Path(__file__).parent / 'database/problems.sqlite3' db_path = Path(__file__).parent / 'database/problems.sqlite3'
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)

222
src/cache.py Normal file
View 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()

View 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 owners 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.

View 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"
}

View 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)

View File

@@ -75,8 +75,10 @@ DANGEROUS_PATTERNS = [
# System exit # System exit
r'exit\s*\(', r'quit\s*\(', r'sys\.exit\s*\(', r'exit\s*\(', r'quit\s*\(', r'sys\.exit\s*\(',
# Dunder methods that could be dangerous # Dunder methods are dangerous if misused, for us we allow classes
r'__.*__\s*\(.*\)', r'\.__.*__', # specifically the constructor
# del i dont allow tho
r'__del__\s*\(',
# Import tricks # Import tricks
r'importlib', r'imp\s', r'pkgutil', r'importlib', r'imp\s', r'pkgutil',