This commit is contained in:
2025-08-20 19:48:45 +02:00
parent f28a6b36ef
commit 1ef6bdfd80
2 changed files with 572 additions and 1 deletions

569
pcreater.py Normal file
View File

@@ -0,0 +1,569 @@
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()

View File

@@ -5,3 +5,5 @@ MarkupSafe>=2.1
watchdog>=4.0
gunicorn>=23.0.0
waitress>=3.0.2
tkinterweb_html>=1.1.4
pillow>=11.3.0