158 lines
7.0 KiB
Python
158 lines
7.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Spec Editor Panel
|
|
|
|
Provides a structured UI to inspect and edit Analysis/EXE/COLLECT portions
|
|
of a PyInstaller .spec file using the `pyinstallerguiwrapper.spec_editor` API.
|
|
|
|
This is intentionally conservative: users edit Python literal values for
|
|
keywords (lists, strings, booleans, dicts) which are converted back to AST
|
|
nodes and written to the spec.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import pathlib
|
|
import ast
|
|
from typing import Callable, Optional
|
|
|
|
from pyinstallerguiwrapper import spec_editor
|
|
|
|
|
|
class SpecEditorPanel(ttk.Frame):
|
|
def __init__(self, parent, get_spec_file_path_func: Callable[[], Optional[str]], logger_func: Callable[[str, str], None]):
|
|
super().__init__(parent)
|
|
self.get_spec_file_path = get_spec_file_path_func
|
|
self.logger = logger_func
|
|
|
|
# UI
|
|
self.call_type_var = tk.StringVar(value='Analysis')
|
|
self.call_select = ttk.Combobox(self, textvariable=self.call_type_var, values=['Analysis', 'EXE', 'COLLECT', 'PYZ'], state='readonly')
|
|
self.refresh_calls_btn = ttk.Button(self, text='Refresh', command=self.refresh_calls)
|
|
|
|
self.keywords_listbox = tk.Listbox(self, height=8)
|
|
self.keywords_scroll = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.keywords_listbox.yview)
|
|
self.keywords_listbox.config(yscrollcommand=self.keywords_scroll.set)
|
|
|
|
self.value_text = tk.Text(self, height=10, width=80)
|
|
self.load_keyword_btn = ttk.Button(self, text='Load Keyword', command=self.load_selected_keyword)
|
|
self.apply_keyword_btn = ttk.Button(self, text='Apply Keyword', command=self.apply_keyword_change)
|
|
self.save_spec_btn = ttk.Button(self, text='Save Spec', command=self.save_spec_file)
|
|
|
|
# Layout
|
|
self.call_select.grid(row=0, column=0, sticky='w', padx=5, pady=5)
|
|
self.refresh_calls_btn.grid(row=0, column=1, sticky='w', padx=5, pady=5)
|
|
self.keywords_listbox.grid(row=1, column=0, rowspan=3, sticky='nsew', padx=(5,0), pady=5)
|
|
self.keywords_scroll.grid(row=1, column=1, rowspan=3, sticky='ns', padx=(0,5), pady=5)
|
|
self.value_text.grid(row=1, column=2, columnspan=2, sticky='nsew', padx=5, pady=5)
|
|
self.load_keyword_btn.grid(row=2, column=2, sticky='w', padx=5, pady=5)
|
|
self.apply_keyword_btn.grid(row=2, column=3, sticky='w', padx=5, pady=5)
|
|
self.save_spec_btn.grid(row=3, column=3, sticky='e', padx=5, pady=5)
|
|
|
|
self.columnconfigure(2, weight=1)
|
|
self.rowconfigure(1, weight=1)
|
|
|
|
# Internal
|
|
self._ast_tree = None
|
|
self._source_text = None
|
|
|
|
# Do an initial silent refresh (do not show messageboxes when no spec)
|
|
self.refresh_calls(silent=True)
|
|
|
|
def _log(self, msg: str, level: str = 'INFO'):
|
|
try:
|
|
self.logger(msg, level)
|
|
except Exception:
|
|
print(f"[{level}] {msg}")
|
|
|
|
def refresh_calls(self, silent: bool = False):
|
|
spec_path = self.get_spec_file_path()
|
|
if not spec_path or not pathlib.Path(spec_path).is_file():
|
|
# Do not interrupt the startup flow with modal dialogs if called silently.
|
|
msg = 'Spec file not found for current project. Load a project with a spec first.'
|
|
if silent:
|
|
self._log(msg, 'WARNING')
|
|
return
|
|
else:
|
|
messagebox.showwarning('Spec Not Found', msg, parent=self)
|
|
return
|
|
try:
|
|
tree, src = spec_editor.parse_spec_to_ast(spec_path)
|
|
self._ast_tree = tree
|
|
self._source_text = src
|
|
call_name = self.call_type_var.get()
|
|
calls = spec_editor.find_calls(tree, call_name)
|
|
self.keywords_listbox.delete(0, tk.END)
|
|
if calls:
|
|
first = calls[0]
|
|
for kw in first.keywords:
|
|
if kw.arg:
|
|
self.keywords_listbox.insert(tk.END, kw.arg)
|
|
else:
|
|
self._log(f'No {call_name} call found in spec.', 'WARNING')
|
|
self._log(f'Refreshed calls for {spec_path}', 'DEBUG')
|
|
except Exception as e:
|
|
self._log(f'Failed to parse/refresh spec: {e}', 'ERROR')
|
|
messagebox.showerror('Parse Error', f'Error parsing spec: {e}', parent=self)
|
|
|
|
def load_selected_keyword(self):
|
|
sel = self.keywords_listbox.curselection()
|
|
if not sel:
|
|
return
|
|
kw = self.keywords_listbox.get(sel[0])
|
|
call_name = self.call_type_var.get()
|
|
calls = spec_editor.find_calls(self._ast_tree, call_name)
|
|
if not calls:
|
|
messagebox.showwarning('Not Found', f'No {call_name} call found.', parent=self)
|
|
return
|
|
node = spec_editor.get_keyword_value(calls[0], kw)
|
|
if node is None:
|
|
self.value_text.delete('1.0', tk.END)
|
|
self.value_text.insert(tk.END, '')
|
|
return
|
|
try:
|
|
s = ast.unparse(node) if hasattr(ast, 'unparse') else '<unparse unavailable>'
|
|
except Exception:
|
|
s = '<unparse unavailable>'
|
|
self.value_text.delete('1.0', tk.END)
|
|
self.value_text.insert(tk.END, s)
|
|
|
|
def apply_keyword_change(self):
|
|
sel = self.keywords_listbox.curselection()
|
|
if not sel:
|
|
messagebox.showinfo('No Selection', 'Select a keyword to apply change to.', parent=self)
|
|
return
|
|
kw = self.keywords_listbox.get(sel[0])
|
|
raw = self.value_text.get('1.0', tk.END).strip()
|
|
# Try to parse the raw text into a Python literal safely
|
|
try:
|
|
py_val = ast.literal_eval(raw)
|
|
except Exception:
|
|
# As a fallback, treat it as a string
|
|
py_val = raw
|
|
modified = spec_editor.set_call_keyword(self._ast_tree, self.call_type_var.get(), kw, py_val)
|
|
if modified:
|
|
messagebox.showinfo('Applied', f'Keyword "{kw}" updated in memory. Click Save Spec to write file.', parent=self)
|
|
self._log(f'Keyword {kw} updated in AST (in-memory).', 'INFO')
|
|
else:
|
|
messagebox.showerror('Failed', 'Failed to apply the change. See log.', parent=self)
|
|
|
|
def save_spec_file(self):
|
|
if self._ast_tree is None or self._source_text is None:
|
|
messagebox.showwarning('Nothing to Save', 'Load a spec file first.', parent=self)
|
|
return
|
|
try:
|
|
new_src = spec_editor.ast_to_source(self._ast_tree)
|
|
spec_path = self.get_spec_file_path()
|
|
if not spec_path:
|
|
messagebox.showerror('No Spec Path', 'Spec file path not available.', parent=self)
|
|
return
|
|
spec_editor.write_source_to_file(new_src, spec_path)
|
|
messagebox.showinfo('Saved', f'Spec saved to: {spec_path}', parent=self)
|
|
self._log(f'Spec file saved: {spec_path}', 'INFO')
|
|
# Refresh to reflect saved keywords
|
|
self.refresh_calls()
|
|
except Exception as e:
|
|
self._log(f'Failed to save spec: {e}', 'ERROR')
|
|
messagebox.showerror('Save Failed', f'Failed to save spec: {e}', parent=self)
|