creater
This commit is contained in:
569
pcreater.py
Normal file
569
pcreater.py
Normal 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()
|
||||
@@ -4,4 +4,6 @@ Markdown>=3.6
|
||||
MarkupSafe>=2.1
|
||||
watchdog>=4.0
|
||||
gunicorn>=23.0.0
|
||||
waitress>=3.0.2
|
||||
waitress>=3.0.2
|
||||
tkinterweb_html>=1.1.4
|
||||
pillow>=11.3.0
|
||||
Reference in New Issue
Block a user