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

View File

@@ -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

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
* why the fuck does this need to exsits, idk.
*

View File

@@ -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
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
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',