627 lines
24 KiB
Python
627 lines
24 KiB
Python
# dependencyanalyzer/gui.py
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox, ttk
|
|
import threading
|
|
from pathlib import Path
|
|
import re # For parsing requirements.txt in fallback
|
|
from typing import (
|
|
Optional,
|
|
List,
|
|
Dict,
|
|
Set,
|
|
Tuple,
|
|
Any,
|
|
Callable,
|
|
) # Added Any, Callable
|
|
|
|
# Assuming core.py is in the same directory or package
|
|
from . import core
|
|
|
|
|
|
class DependencyAnalyzerApp(tk.Frame):
|
|
"""
|
|
Tkinter GUI application for Python dependency analysis.
|
|
"""
|
|
|
|
def __init__(self, master: Optional[tk.Tk] = None):
|
|
super().__init__(master)
|
|
self.master = master
|
|
if self.master:
|
|
self.master.title("Python Dependency Analyzer")
|
|
self.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
self.selected_repository_path: Optional[Path] = None
|
|
|
|
# Store detailed dependency information
|
|
self.std_lib_deps_info: core.DependencyInfo = {}
|
|
self.external_deps_info: core.DependencyInfo = {}
|
|
self.extracted_dependencies_names: Set[str] = (
|
|
set()
|
|
) # Just names for the Treeview
|
|
|
|
self.requirements_file_path: Optional[Path] = None
|
|
|
|
self._create_widgets()
|
|
self._update_button_states()
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""Creates and lays out the GUI widgets."""
|
|
# --- Frame for Repository Selection ---
|
|
repo_frame = ttk.LabelFrame(
|
|
self, text="1. Repository Operations", padding=(10, 5)
|
|
)
|
|
repo_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.select_repo_button = ttk.Button(
|
|
repo_frame, text="Select Repository Folder", command=self._select_repository
|
|
)
|
|
self.select_repo_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
self.repo_path_label = ttk.Label(repo_frame, text="No repository selected.")
|
|
self.repo_path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
|
|
|
|
# --- Frame for Analysis and Requirements ---
|
|
analysis_frame = ttk.LabelFrame(
|
|
self, text="2. Analysis & Requirements", padding=(10, 5)
|
|
)
|
|
analysis_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.analyze_button = ttk.Button(
|
|
analysis_frame,
|
|
text="Analyze Project & Generate requirements.txt",
|
|
command=self._analyze_and_generate_reqs_threaded,
|
|
)
|
|
self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
# --- Frame for Displaying Found Modules (in requirements.txt) ---
|
|
modules_frame = ttk.LabelFrame(
|
|
self, text="3. External Dependencies Found", padding=(10, 5)
|
|
)
|
|
modules_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
self.modules_tree = ttk.Treeview(
|
|
modules_frame, columns=("module_name",), show="headings"
|
|
)
|
|
self.modules_tree.heading("module_name", text="External Module Name")
|
|
self.modules_tree.column("module_name", stretch=tk.YES)
|
|
|
|
modules_scrollbar = ttk.Scrollbar(
|
|
modules_frame, orient="vertical", command=self.modules_tree.yview
|
|
)
|
|
self.modules_tree.configure(yscrollcommand=modules_scrollbar.set)
|
|
|
|
modules_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.modules_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
# --- Frame for Downloading Packages and Creating Install Scripts ---
|
|
download_frame = ttk.LabelFrame(
|
|
self, text="4. Download Packages & Create Installers", padding=(10, 5)
|
|
)
|
|
download_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.download_button = ttk.Button(
|
|
download_frame,
|
|
text="Download Packages to _req_packages",
|
|
command=self._download_packages_threaded,
|
|
)
|
|
self.download_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
self.create_scripts_button = ttk.Button(
|
|
download_frame,
|
|
text="Create install_packages.bat/sh",
|
|
command=self._create_install_scripts_threaded,
|
|
)
|
|
self.create_scripts_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
# --- Frame for System Package Comparison ---
|
|
compare_frame = ttk.LabelFrame(
|
|
self, text="5. System Package Comparison", padding=(10, 5)
|
|
)
|
|
compare_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
self.compare_button = ttk.Button(
|
|
compare_frame,
|
|
text="Compare requirements.txt with Installed Packages",
|
|
command=self._compare_packages_threaded,
|
|
)
|
|
self.compare_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=5)
|
|
|
|
self.comparison_tree = ttk.Treeview(
|
|
compare_frame,
|
|
columns=("package", "required", "installed", "status"),
|
|
show="headings",
|
|
)
|
|
self.comparison_tree.heading("package", text="Package")
|
|
self.comparison_tree.heading("required", text="Required Version (from reqs)")
|
|
self.comparison_tree.heading(
|
|
"installed", text="Installed Version (current env)"
|
|
)
|
|
self.comparison_tree.heading("status", text="Status")
|
|
self.comparison_tree.column("package", width=150, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.column("required", width=180, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.column("installed", width=180, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.column("status", width=200, stretch=tk.YES, anchor=tk.W)
|
|
|
|
compare_scrollbar_y = ttk.Scrollbar(
|
|
compare_frame, orient="vertical", command=self.comparison_tree.yview
|
|
)
|
|
self.comparison_tree.configure(yscrollcommand=compare_scrollbar_y.set)
|
|
compare_scrollbar_x = ttk.Scrollbar(
|
|
compare_frame, orient="horizontal", command=self.comparison_tree.xview
|
|
)
|
|
self.comparison_tree.configure(xscrollcommand=compare_scrollbar_x.set)
|
|
|
|
compare_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
|
|
compare_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
|
|
self.comparison_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
self.update_system_button = ttk.Button(
|
|
compare_frame,
|
|
text="Update Selected Mismatched/Not Installed Packages",
|
|
command=self._update_system_packages_threaded,
|
|
)
|
|
self.update_system_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=5)
|
|
|
|
# --- Log Area ---
|
|
log_frame = ttk.LabelFrame(self, text="Log Messages", padding=(10, 5))
|
|
log_frame.pack(fill=tk.X, pady=5)
|
|
self.log_text = tk.Text(
|
|
log_frame,
|
|
height=8,
|
|
state=tk.DISABLED,
|
|
wrap=tk.WORD,
|
|
relief=tk.SUNKEN,
|
|
borderwidth=1,
|
|
)
|
|
log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
|
|
self.log_text.configure(yscrollcommand=log_scrollbar.set)
|
|
|
|
self.log_text.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
|
|
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
def _log_message(self, message: str, level: str = "INFO") -> None:
|
|
if level == "ERROR":
|
|
core.logger.error(message)
|
|
elif level == "WARNING":
|
|
core.logger.warning(message)
|
|
else:
|
|
core.logger.info(message)
|
|
|
|
if self.winfo_exists():
|
|
formatted_message = f"[{level}] {message}\n"
|
|
self.log_text.config(state=tk.NORMAL)
|
|
self.log_text.insert(tk.END, formatted_message)
|
|
self.log_text.see(tk.END)
|
|
self.log_text.config(state=tk.DISABLED)
|
|
|
|
if level == "ERROR":
|
|
messagebox.showerror("Error", message)
|
|
elif level == "WARNING":
|
|
messagebox.showwarning("Warning", message)
|
|
|
|
def _update_button_states(self) -> None:
|
|
repo_selected = bool(self.selected_repository_path)
|
|
self.analyze_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED)
|
|
|
|
req_file_exists_and_valid = (
|
|
self.requirements_file_path
|
|
and self.requirements_file_path.exists()
|
|
and bool(self.external_deps_info)
|
|
)
|
|
|
|
self.download_button.config(
|
|
state=tk.NORMAL if req_file_exists_and_valid else tk.DISABLED
|
|
)
|
|
self.create_scripts_button.config(
|
|
state=(
|
|
tk.NORMAL
|
|
if req_file_exists_and_valid and self.selected_repository_path
|
|
else tk.DISABLED
|
|
)
|
|
)
|
|
self.compare_button.config(
|
|
state=tk.NORMAL if req_file_exists_and_valid else tk.DISABLED
|
|
)
|
|
|
|
can_update = (
|
|
bool(self.comparison_tree.get_children()) and req_file_exists_and_valid
|
|
)
|
|
self.update_system_button.config(state=tk.NORMAL if can_update else tk.DISABLED)
|
|
|
|
def _select_repository(self) -> None:
|
|
path = filedialog.askdirectory(title="Select Repository Folder")
|
|
if path:
|
|
self.selected_repository_path = Path(path)
|
|
self.repo_path_label.config(text=str(self.selected_repository_path))
|
|
self._log_message(f"Repository selected: {self.selected_repository_path}")
|
|
self.std_lib_deps_info = {}
|
|
self.external_deps_info = {}
|
|
self.extracted_dependencies_names = set()
|
|
self.requirements_file_path = None
|
|
self.modules_tree.delete(*self.modules_tree.get_children())
|
|
self.comparison_tree.delete(*self.comparison_tree.get_children())
|
|
else:
|
|
self._log_message("Repository selection cancelled.", "INFO")
|
|
self._update_button_states()
|
|
|
|
# CORREZIONE APPLICATA ALLA FIRMA QUI:
|
|
def _run_long_task_threaded(
|
|
self,
|
|
task_function: Callable[..., Any],
|
|
*args_for_task: Any, # Argomenti posizionali per task_function
|
|
# I seguenti sono opzionali e dovrebbero essere passati come keyword arguments
|
|
callback_success: Optional[Callable[[Any], None]] = None,
|
|
callback_failure: Optional[Callable[[str], None]] = None,
|
|
) -> None:
|
|
"""
|
|
Helper to run a function in a separate thread.
|
|
`callback_success` and `callback_failure` should be specified as keyword arguments
|
|
if used, to avoid ambiguity with `*args_for_task`.
|
|
"""
|
|
|
|
def task_wrapper():
|
|
try:
|
|
result = task_function(*args_for_task)
|
|
if callback_success and self.winfo_exists():
|
|
self.master.after(0, lambda: callback_success(result))
|
|
except Exception as e:
|
|
error_msg = (
|
|
f"Error in threaded task '{task_function.__name__}': {str(e)}"
|
|
)
|
|
self._log_message(error_msg, "ERROR")
|
|
core.logger.exception(
|
|
f"Full traceback for task {task_function.__name__}:"
|
|
)
|
|
if callback_failure and self.winfo_exists():
|
|
self.master.after(0, lambda: callback_failure(error_msg))
|
|
finally:
|
|
if self.winfo_exists():
|
|
self.master.after(0, self._temporarily_disable_buttons, False)
|
|
self.master.after(10, self._update_button_states)
|
|
|
|
self._temporarily_disable_buttons(True)
|
|
thread = threading.Thread(target=task_wrapper, daemon=True)
|
|
thread.start()
|
|
self._log_message(f"Starting task: {task_function.__name__}...", "INFO")
|
|
|
|
def _temporarily_disable_buttons(self, disable: bool = True) -> None:
|
|
state_to_set = tk.DISABLED if disable else tk.NORMAL
|
|
buttons_to_toggle = [
|
|
self.select_repo_button,
|
|
self.analyze_button,
|
|
self.download_button,
|
|
self.create_scripts_button,
|
|
self.compare_button,
|
|
self.update_system_button,
|
|
]
|
|
for button in buttons_to_toggle:
|
|
if button.winfo_exists():
|
|
button.config(state=state_to_set)
|
|
|
|
def _analyze_and_generate_reqs_threaded(self) -> None:
|
|
if not self.selected_repository_path:
|
|
self._log_message("Please select a repository first.", "WARNING")
|
|
return
|
|
self._run_long_task_threaded(
|
|
self._perform_analysis_and_generation,
|
|
# No *args_for_task needed for _perform_analysis_and_generation
|
|
callback_success=self._analysis_and_generation_callback, # Passato come keyword
|
|
)
|
|
|
|
def _perform_analysis_and_generation(
|
|
self,
|
|
) -> Tuple[Path, core.DependencyInfo, core.DependencyInfo]:
|
|
if not self.selected_repository_path:
|
|
raise ValueError(
|
|
"Repository path not selected when trying to perform analysis."
|
|
)
|
|
|
|
main_script_path = core.find_main_script(self.selected_repository_path)
|
|
if main_script_path:
|
|
core.logger.info(f"Main script identified (for info): {main_script_path}")
|
|
else:
|
|
core.logger.info(
|
|
"Main script (__main__.py) not found in standard locations."
|
|
)
|
|
|
|
std_lib_info, external_info = core.find_project_modules_and_dependencies(
|
|
self.selected_repository_path
|
|
)
|
|
|
|
req_file_path = core.generate_requirements_file(
|
|
self.selected_repository_path, external_info, std_lib_info
|
|
)
|
|
return req_file_path, std_lib_info, external_info
|
|
|
|
def _analysis_and_generation_callback(
|
|
self, result: Tuple[Path, core.DependencyInfo, core.DependencyInfo]
|
|
) -> None:
|
|
req_file_path, std_lib_info, external_info = result
|
|
|
|
self.requirements_file_path = req_file_path
|
|
self.std_lib_deps_info = std_lib_info
|
|
self.external_deps_info = external_info
|
|
self.extracted_dependencies_names = set(self.external_deps_info.keys())
|
|
|
|
self._log_message(
|
|
f"Analysis and requirements.txt generation complete. File: {self.requirements_file_path}",
|
|
"INFO",
|
|
)
|
|
self._populate_modules_tree()
|
|
|
|
def _populate_modules_tree(self) -> None:
|
|
self.modules_tree.delete(*self.modules_tree.get_children())
|
|
if self.extracted_dependencies_names:
|
|
for dep_name in sorted(list(self.extracted_dependencies_names)):
|
|
self.modules_tree.insert("", tk.END, values=(dep_name,))
|
|
elif self.requirements_file_path and self.requirements_file_path.exists():
|
|
self._log_message(
|
|
"Populating tree from requirements.txt as a fallback.", "DEBUG"
|
|
)
|
|
try:
|
|
with open(self.requirements_file_path, "r", encoding="utf-8") as f:
|
|
in_external_section = False
|
|
for line in f:
|
|
line_content = line.strip()
|
|
if "# --- External Dependencies" in line_content:
|
|
in_external_section = True
|
|
continue
|
|
if "# --- Standard Library Modules Used" in line_content:
|
|
in_external_section = False
|
|
continue
|
|
if (
|
|
in_external_section
|
|
and line_content
|
|
and not line_content.startswith("#")
|
|
and not line_content.startswith("-")
|
|
):
|
|
match = re.match(r"([a-zA-Z0-9._-]+)", line_content)
|
|
if match:
|
|
self.modules_tree.insert(
|
|
"", tk.END, values=(match.group(1),)
|
|
)
|
|
except Exception as e:
|
|
self._log_message(
|
|
f"Error reading requirements file for tree view: {e}", "WARNING"
|
|
)
|
|
|
|
def _download_packages_threaded(self) -> None:
|
|
if not self.requirements_file_path or not self.selected_repository_path:
|
|
self._log_message(
|
|
"Requirements file or repository path not set.", "WARNING"
|
|
)
|
|
return
|
|
if not self.external_deps_info:
|
|
self._log_message(
|
|
"No external dependencies found in the current analysis to download.",
|
|
"INFO",
|
|
)
|
|
return
|
|
|
|
self._run_long_task_threaded(
|
|
core.download_packages, # task_function
|
|
# *args_for_task:
|
|
self.selected_repository_path,
|
|
self.requirements_file_path,
|
|
# Keyword-only:
|
|
callback_success=self._download_packages_callback,
|
|
)
|
|
|
|
def _download_packages_callback(self, result: Tuple[bool, str]) -> None:
|
|
success, result_message = result
|
|
if success:
|
|
self._log_message(result_message, "INFO")
|
|
else:
|
|
self._log_message(f"Package download failed: {result_message}", "ERROR")
|
|
|
|
def _create_install_scripts_threaded(self) -> None:
|
|
if (
|
|
not self.selected_repository_path
|
|
or not self.requirements_file_path
|
|
or not self.requirements_file_path.exists()
|
|
):
|
|
self._log_message(
|
|
"Repository not selected or requirements.txt not generated/found.",
|
|
"WARNING",
|
|
)
|
|
return
|
|
|
|
self._run_long_task_threaded(
|
|
core.create_install_scripts, # task_function
|
|
# *args_for_task:
|
|
self.selected_repository_path,
|
|
self.requirements_file_path,
|
|
# Keyword-only:
|
|
callback_success=self._create_install_scripts_callback,
|
|
)
|
|
|
|
def _create_install_scripts_callback(self, result: Tuple[bool, str]) -> None:
|
|
success, result_message = result
|
|
if success:
|
|
self._log_message(result_message, "INFO")
|
|
else:
|
|
self._log_message(f"Script creation failed: {result_message}", "ERROR")
|
|
|
|
def _compare_packages_threaded(self) -> None:
|
|
if not self.requirements_file_path or not self.requirements_file_path.exists():
|
|
self._log_message(
|
|
"requirements.txt not found or not generated yet for comparison.",
|
|
"WARNING",
|
|
)
|
|
return
|
|
if (
|
|
not self.external_deps_info
|
|
and self.requirements_file_path.stat().st_size == 0
|
|
):
|
|
self._log_message(
|
|
"No external dependencies listed or requirements file is empty. Nothing to compare.",
|
|
"INFO",
|
|
)
|
|
self.comparison_tree.delete(*self.comparison_tree.get_children())
|
|
return
|
|
|
|
self._run_long_task_threaded(
|
|
self._get_comparison_data, # task_function
|
|
# No *args_for_task for _get_comparison_data
|
|
# Keyword-only:
|
|
callback_success=self._compare_packages_callback,
|
|
)
|
|
|
|
def _get_comparison_data(self) -> List[Dict[str, str]]:
|
|
installed_packages = core.get_installed_packages()
|
|
if not self.requirements_file_path:
|
|
raise ValueError("Requirements file path is not set for comparison.")
|
|
return core.compare_requirements_with_installed(
|
|
self.requirements_file_path, installed_packages
|
|
)
|
|
|
|
def _compare_packages_callback(self, results: List[Dict[str, str]]) -> None:
|
|
self.comparison_tree.delete(*self.comparison_tree.get_children())
|
|
if results:
|
|
for item in results:
|
|
self.comparison_tree.insert(
|
|
"",
|
|
tk.END,
|
|
values=(
|
|
item["package"],
|
|
item["required"],
|
|
item["installed"],
|
|
item["status"],
|
|
),
|
|
tags=(item["status"],),
|
|
)
|
|
self._log_message("Package comparison complete.", "INFO")
|
|
else:
|
|
self._log_message(
|
|
"No packages to compare or requirements file is effectively empty for comparison.",
|
|
"INFO",
|
|
)
|
|
|
|
def _update_system_packages_threaded(self) -> None:
|
|
selected_items_ids = self.comparison_tree.selection()
|
|
if not selected_items_ids:
|
|
self._log_message(
|
|
"No packages selected in the comparison table for update.", "WARNING"
|
|
)
|
|
if self.winfo_exists():
|
|
messagebox.showinfo(
|
|
"No Selection",
|
|
"Please select packages from the comparison table to update.",
|
|
)
|
|
return
|
|
|
|
packages_to_update: List[str] = []
|
|
for item_id in selected_items_ids:
|
|
item_values = self.comparison_tree.item(item_id, "values")
|
|
if not item_values:
|
|
continue
|
|
package_name, _, _, status = item_values # Unpack all four
|
|
if (
|
|
"Mismatch" in status
|
|
or "Not installed" in status
|
|
or "Outdated" in status
|
|
):
|
|
packages_to_update.append(str(package_name)) # Ensure it's string
|
|
else:
|
|
self._log_message(
|
|
f"Skipping '{package_name}': status is '{status}'. No update needed.",
|
|
"INFO",
|
|
)
|
|
|
|
if not packages_to_update:
|
|
self._log_message(
|
|
"Selected packages do not require an update based on their status.",
|
|
"INFO",
|
|
)
|
|
if self.winfo_exists():
|
|
messagebox.showinfo(
|
|
"No Action Needed",
|
|
"The selected packages do not appear to need an update.",
|
|
)
|
|
return
|
|
|
|
if not self.selected_repository_path or not self.requirements_file_path:
|
|
self._log_message(
|
|
"Repository path or requirements file context not set. Cannot perform update.",
|
|
"ERROR",
|
|
)
|
|
return
|
|
|
|
if self.winfo_exists():
|
|
confirmation = messagebox.askyesno(
|
|
"Confirm Update",
|
|
f"Update/install in current Python environment:\n\n{', '.join(packages_to_update)}\n\n"
|
|
"Using versions from project's requirements.txt (if specified there).",
|
|
)
|
|
if not confirmation:
|
|
self._log_message("System package update cancelled by user.", "INFO")
|
|
return
|
|
|
|
self._run_long_task_threaded(
|
|
core.update_system_packages, # task_function
|
|
# *args_for_task:
|
|
packages_to_update,
|
|
self.selected_repository_path,
|
|
# Keyword-only:
|
|
callback_success=self._update_system_packages_callback,
|
|
)
|
|
|
|
def _update_system_packages_callback(self, result: Tuple[bool, str]) -> None:
|
|
success, result_message = result
|
|
if success:
|
|
self._log_message(
|
|
f"System package update process finished. Result: {result_message}",
|
|
"INFO",
|
|
)
|
|
self._log_message("Re-comparing packages after update attempt...", "INFO")
|
|
self._compare_packages_threaded()
|
|
else:
|
|
self._log_message(
|
|
f"System package update failed: {result_message}", "ERROR"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
root = tk.Tk()
|
|
app = DependencyAnalyzerApp(master=root)
|
|
root.minsize(width=800, height=700)
|
|
|
|
if not logging.getLogger().hasHandlers():
|
|
logging.basicConfig(
|
|
stream=sys.stdout,
|
|
level=logging.DEBUG,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
)
|
|
core.logger.info("Root logger configured for direct GUI test run.")
|
|
else:
|
|
core.logger.info("Root logger already configured. Using existing setup.")
|
|
|
|
# Ensure core.logger specifically has a handler if it's not propagating to a configured root.
|
|
# This can happen if core.logger.propagate is False or if root logger has no handlers.
|
|
if not core.logger.handlers and (
|
|
not core.logger.parent or not core.logger.parent.handlers
|
|
):
|
|
# Add a default handler to core.logger if it doesn't have one and isn't covered by root.
|
|
ch = logging.StreamHandler(sys.stdout)
|
|
ch.setLevel(
|
|
logging.DEBUG
|
|
) # Or core.logger.level if you want to respect a level set on the logger itself
|
|
formatter = logging.Formatter(
|
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
)
|
|
ch.setFormatter(formatter)
|
|
core.logger.addHandler(ch)
|
|
if (
|
|
core.logger.getEffectiveLevel() > logging.DEBUG
|
|
): # if level is higher than DEBUG (e.g. INFO, WARNING)
|
|
core.logger.setLevel(
|
|
logging.DEBUG
|
|
) # Ensure it processes DEBUG messages for testing
|
|
core.logger.info(
|
|
"Configured core.logger with StreamHandler for direct GUI test run because it lacked effective handlers."
|
|
)
|
|
|
|
app.mainloop()
|