diff --git a/pcreater.py b/pcreater.py deleted file mode 100644 index df1b176..0000000 --- a/pcreater.py +++ /dev/null @@ -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('', self._tab) - self.bind('', self._shift_tab) - self.bind('', 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('', 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""" - - - - - - {self._simple_markdown_to_html(markdown_text)} - - - """ - - # 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("# ", "

").replace("\n# ", "

\n

") + "

" - html = html.replace("## ", "

").replace("\n## ", "

\n

") + "

" - html = html.replace("### ", "

").replace("\n### ", "

\n

") + "

" - html = html.replace("**", "").replace("**", "") - html = html.replace("*", "").replace("*", "") - html = html.replace("`", "").replace("`", "") - html = html.replace("\n", "
") - 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() \ No newline at end of file diff --git a/qtc.py b/qtc.py new file mode 100644 index 0000000..77b2c1d --- /dev/null +++ b/qtc.py @@ -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("# ", "

").replace("\n# ", "

\n

") + "

" + html = html.replace("## ", "

").replace("\n## ", "

\n

") + "

" + html = html.replace("### ", "

").replace("\n### ", "

\n

") + "

" + html = html.replace("**", "").replace("**", "") + html = html.replace("*", "").replace("*", "") + html = html.replace("`", "").replace("`", "") + html = html.replace("\n", "
") + + # Wrap in proper HTML structure + return f""" + + + + + + {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() \ No newline at end of file diff --git a/readme.md b/readme.md index aefefe7..da02521 100644 --- a/readme.md +++ b/readme.md @@ -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. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8233b47..a00c885 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +pygments>=2.19.2 +pyqt6>=6.9.1 +PyQt6-WebEngine>=6.9.0 \ No newline at end of file diff --git a/src/JavaScript/script.js b/src/JavaScript/script.js index 104fab4..93f3313 100644 --- a/src/JavaScript/script.js +++ b/src/JavaScript/script.js @@ -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. * diff --git a/src/app.py b/src/app.py index 2c9a593..4ecfb42 100644 --- a/src/app.py +++ b/src/app.py @@ -1,20 +1,27 @@ # API endpoint to get problem manifest (description) by folder from markupsafe import Markup from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify +from flask_caching import Cache import markdown as md import ast from src.models import db, Problem, Solution from src.utils import run_code_against_tests from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard - - import os from src.problem_scanner import start_problem_scanner import sqlite3 from pathlib import Path -app = Flask(__name__) +# Config cache +config = { + "DEBUG": True, + "CACHE_TYPE": "SimpleCache", + "CACHE_DEFAULT_TIMEOUT": 300 +} +app = Flask(__name__) +app.config.from_mapping(config) +cache = Cache(app) BASE_DIR = Path(__file__).parent app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{BASE_DIR / 'database' / 'db.sqlite3'}" @@ -47,18 +54,22 @@ def api_problem_manifest(folder): # I introduce you to the fucking JavaScript shit routes, fuck javascripts @app.route('/JavaScript/') +@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) diff --git a/src/cache.py b/src/cache.py new file mode 100644 index 0000000..cf718f8 --- /dev/null +++ b/src/cache.py @@ -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() \ No newline at end of file diff --git a/src/problems/Hashmaps/description.md b/src/problems/Hashmaps/description.md new file mode 100644 index 0000000..2abb555 --- /dev/null +++ b/src/problems/Hashmaps/description.md @@ -0,0 +1,50 @@ +## 🏷️ Problem: Lost & Found Office + +You are designing a system for a **Lost-and-Found office**. + +* People can **report lost items**, where each item is mapped to the owner’s name. +* People can later **claim their item**. +* If the item is not found, return `"No item found!"`. + +--- + +### Function Signature + +```python +class LostAndFound: + def __init__(self): + pass + + def add_item(self, owner: str, item: str) -> None: + """ + Stores the item with the owner's name. + """ + + def claim_item(self, owner: str) -> str: + """ + Returns the owner's item if it exists, otherwise + returns 'No item found!'. + """ +``` + +--- + +### Example + +```python +office = LostAndFound() +office.add_item("Alice", "Umbrella") +office.add_item("Bob", "Backpack") + +print(office.claim_item("Alice")) # Output: "Umbrella" +print(office.claim_item("Alice")) # Output: "No item found!" +print(office.claim_item("Charlie")) # Output: "No item found!" +``` + +--- + +### Constraints + +* `1 <= len(owner), len(item) <= 100` +* You may assume only **strings** are used for owner and item. +* An owner can only have **one item** at a time. \ No newline at end of file diff --git a/src/problems/Hashmaps/manifest.json b/src/problems/Hashmaps/manifest.json new file mode 100644 index 0000000..54941b8 --- /dev/null +++ b/src/problems/Hashmaps/manifest.json @@ -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" +} \ No newline at end of file diff --git a/src/problems/Hashmaps/test.py b/src/problems/Hashmaps/test.py new file mode 100644 index 0000000..1ef28bb --- /dev/null +++ b/src/problems/Hashmaps/test.py @@ -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) diff --git a/src/utils.py b/src/utils.py index e6fbf81..99776a9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -75,9 +75,11 @@ 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', ]