365 lines
14 KiB
Python
365 lines
14 KiB
Python
# projectutility/__main__.py
|
|
|
|
import sys
|
|
import os
|
|
import logging
|
|
import argparse
|
|
import tkinter as tk
|
|
from tkinter import messagebox
|
|
import traceback # Import traceback for detailed error reporting
|
|
|
|
# --- Calculate Paths Relative to __main__.py ---
|
|
# In the new structure, __file__ points to .../ProjectUtility/projectutility/__main__.py
|
|
try:
|
|
# Path to the directory containing this __main__.py file
|
|
# e.g., /path/to/your/repo/ProjectUtility/projectutility
|
|
APP_SOURCE_ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Path to the repository root directory (one level up from APP_SOURCE_ROOT)
|
|
# e.g., /path/to/your/repo/ProjectUtility
|
|
REPO_ROOT = os.path.dirname(APP_SOURCE_ROOT)
|
|
|
|
# Add REPO_ROOT to sys.path?
|
|
# When running with 'python -m projectutility' from the REPO_ROOT,
|
|
# REPO_ROOT is usually added automatically to sys.path by Python.
|
|
# Explicitly adding it might be redundant but can ensure imports work
|
|
# in some edge cases or different execution methods. Let's keep it simple for now.
|
|
# if REPO_ROOT not in sys.path:
|
|
# sys.path.insert(0, REPO_ROOT) # Insert at beginning
|
|
# logging.getLogger(__name__).debug(f"Manually added REPO_ROOT to sys.path: {REPO_ROOT}")
|
|
|
|
# Configure logging as early as possible
|
|
# Define basic logging config here before other imports if needed,
|
|
# or call setup_logging right after path calculation.
|
|
_log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
logging.basicConfig(
|
|
level=logging.DEBUG, format=_log_format
|
|
) # Basic config initially
|
|
|
|
# Log calculated paths for verification
|
|
logging.getLogger(__name__).debug(
|
|
f"APP_SOURCE_ROOT (inside __main__): {APP_SOURCE_ROOT}"
|
|
)
|
|
logging.getLogger(__name__).debug(f"REPO_ROOT (inside __main__): {REPO_ROOT}")
|
|
logging.getLogger(__name__).debug(f"Current sys.path: {sys.path}")
|
|
|
|
except Exception as e:
|
|
# Use basic logging or print if logging setup failed
|
|
logging.getLogger(__name__).critical(
|
|
f"CRITICAL ERROR: Failed to determine application paths in __main__.py: {e}",
|
|
exc_info=True,
|
|
)
|
|
# Attempt to show error in GUI before exiting
|
|
try:
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
messagebox.showerror(
|
|
"Fatal Initialization Error",
|
|
f"Could not determine critical application paths.\n\nError: {e}\n\nCheck logs or run from the correct directory.",
|
|
)
|
|
root.destroy()
|
|
except Exception:
|
|
print(
|
|
f"CRITICAL ERROR: Could not determine paths: {e}", file=sys.stderr
|
|
) # Fallback print
|
|
sys.exit(1) # Exit immediately if paths are wrong
|
|
|
|
# --- Import Internal Modules using Absolute Package Paths ---
|
|
# Now that paths are set (or assumed correct via -m), import application components.
|
|
try:
|
|
# Use absolute import from the package 'projectutility'
|
|
from projectutility.gui.main_window import MainWindow
|
|
|
|
# Import any other top-level dependencies needed here
|
|
|
|
logging.getLogger(__name__).info("Successfully imported MainWindow.")
|
|
|
|
except ImportError as e:
|
|
error_message = f"ERROR: Failed to import ProjectUtility components in __main__.py.\n\nImportError: {e}\n\n"
|
|
error_message += f"Python Executable: {sys.executable}\n"
|
|
error_message += f"sys.path:\n" + "\n".join(sys.path) + "\n\n"
|
|
error_message += "Ensure you are running 'python -m projectutility' from the repository root directory ('ProjectUtility').\n"
|
|
error_message += (
|
|
"Also check if all '__init__.py' files exist in package directories."
|
|
)
|
|
|
|
print(error_message, file=sys.stderr)
|
|
logging.getLogger(__name__).critical(
|
|
error_message, exc_info=True
|
|
) # Log the detailed error
|
|
traceback.print_exc() # Print traceback to console
|
|
|
|
# Attempt to show error in GUI
|
|
try:
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
messagebox.showerror("Fatal Import Error", error_message)
|
|
root.destroy()
|
|
except Exception as tk_error:
|
|
logging.getLogger(__name__).error(
|
|
f"Could not display Tkinter error message: {tk_error}"
|
|
)
|
|
|
|
sys.exit(1) # Exit due to import failure
|
|
|
|
except Exception as e:
|
|
# Catch any other unexpected errors during import
|
|
error_message = f"Unexpected error during imports in __main__.py: {e}"
|
|
print(error_message, file=sys.stderr)
|
|
logging.getLogger(__name__).critical(error_message, exc_info=True)
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
# --- Constants Based on Calculated Paths ---
|
|
# Place generated/application-specific files inside the source package directory
|
|
LOGS_DIR = os.path.join(APP_SOURCE_ROOT, "logs")
|
|
CONFIG_DIR_MAIN = os.path.join(
|
|
APP_SOURCE_ROOT, "config"
|
|
) # Config files like registry, state
|
|
|
|
# Default log file location inside projectutility/logs/
|
|
DEFAULT_LOG_FILE = os.path.join(LOGS_DIR, "project_utility.log")
|
|
DEFAULT_LOG_LEVEL = logging.INFO # Default logging level
|
|
|
|
|
|
# --- Logging Setup Function ---
|
|
def setup_logging(
|
|
log_level: int = DEFAULT_LOG_LEVEL, log_file: str = DEFAULT_LOG_FILE
|
|
) -> None:
|
|
"""
|
|
Configures the application's logging.
|
|
|
|
Sets up logging to both a file and the console (stdout). Creates the
|
|
log directory if it doesn't exist.
|
|
|
|
Args:
|
|
log_level: The minimum logging level to capture (e.g., logging.INFO).
|
|
log_file: The full path to the log file.
|
|
"""
|
|
try:
|
|
# Ensure the directory for the log file exists
|
|
log_dir = os.path.dirname(log_file)
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
logging.getLogger(__name__).debug(f"Ensured log directory exists: {log_dir}")
|
|
except OSError as e:
|
|
logging.getLogger(__name__).error(
|
|
f"Could not create log directory '{os.path.dirname(log_file)}': {e}. Logging to file might fail."
|
|
)
|
|
# Continue without file logging if directory creation fails? Or exit?
|
|
# For now, let basicConfig handle the FileHandler error if it occurs.
|
|
|
|
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
date_format = "%Y-%m-%d %H:%M:%S"
|
|
|
|
# Get the root logger and remove existing handlers to avoid duplication
|
|
# if basicConfig was called earlier or setup_logging is called multiple times.
|
|
root_logger = logging.getLogger()
|
|
# Clear existing handlers
|
|
if root_logger.hasHandlers():
|
|
logging.getLogger(__name__).debug("Removing existing logging handlers.")
|
|
for handler in root_logger.handlers[:]:
|
|
root_logger.removeHandler(handler)
|
|
handler.close() # Ensure handlers release resources
|
|
|
|
# Configure logging using basicConfig (simpler setup)
|
|
# This will add new handlers.
|
|
try:
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format=log_format,
|
|
datefmt=date_format,
|
|
handlers=[
|
|
logging.FileHandler(
|
|
log_file, mode="a", encoding="utf-8"
|
|
), # Append mode, UTF-8
|
|
logging.StreamHandler(sys.stdout), # Log to console
|
|
],
|
|
force=True, # force=True in Python 3.8+ overrides existing root logger config
|
|
)
|
|
logging.getLogger(__name__).info("Logging configured successfully.")
|
|
logging.getLogger(__name__).info(
|
|
f"Logging Level: {logging.getLevelName(log_level)}"
|
|
)
|
|
logging.getLogger(__name__).info(f"Log File: {log_file}")
|
|
except Exception as e:
|
|
# Fallback if basicConfig fails (e.g., file permission error)
|
|
logging.getLogger(__name__).error(
|
|
f"Failed to configure logging: {e}", exc_info=True
|
|
)
|
|
# Attempt basic console logging
|
|
logging.basicConfig(level=log_level, format=log_format, datefmt=date_format)
|
|
logging.warning("Logging to file failed. Using console logging only.")
|
|
|
|
|
|
# --- Argument Parsing Function ---
|
|
def parse_arguments() -> argparse.Namespace:
|
|
"""
|
|
Parses command-line arguments passed to the application.
|
|
|
|
Handles arguments like log level and log file path provided after
|
|
'python -m projectutility'.
|
|
|
|
Returns:
|
|
An argparse.Namespace object containing the parsed arguments.
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Project Utility Dashboard (Run via -m)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--log-level",
|
|
type=str.upper, # Convert choice to uppercase for matching logging levels
|
|
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
default=logging.getLevelName(
|
|
DEFAULT_LOG_LEVEL
|
|
), # Use the default level constant
|
|
help=f"Set the logging level (default: {logging.getLevelName(DEFAULT_LOG_LEVEL)})",
|
|
metavar="LEVEL",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--log-file",
|
|
type=str,
|
|
default=DEFAULT_LOG_FILE, # Use the default file constant
|
|
help=f"Specify the path for the log file (default: {DEFAULT_LOG_FILE})",
|
|
metavar="FILEPATH",
|
|
)
|
|
|
|
# Add other command-line arguments here if needed in the future
|
|
|
|
# sys.argv[0] will be the path to __main__.py when run with -m
|
|
# We only want to parse arguments *after* the module name.
|
|
# Example: python -m projectutility --log-level DEBUG
|
|
# sys.argv will be ['.../__main__.py', '--log-level', 'DEBUG']
|
|
args = parser.parse_args(
|
|
sys.argv[1:]
|
|
) # Parse arguments after the script/module name
|
|
logging.getLogger(__name__).debug(f"Command line arguments parsed: {args}")
|
|
return args
|
|
|
|
|
|
# --- Main Application Function ---
|
|
def main() -> None:
|
|
"""
|
|
Main function to initialize and run the ProjectUtility application.
|
|
|
|
Parses arguments, sets up logging, creates the main window, and
|
|
starts the Tkinter event loop.
|
|
"""
|
|
# 1. Parse command line arguments
|
|
args = parse_arguments()
|
|
|
|
# 2. Set up logging based on arguments
|
|
# Convert log level string to integer value (e.g., "INFO" -> 20)
|
|
log_level_int = logging.getLevelName(args.log_level) # Returns the integer level
|
|
# Use the log file path specified or the default
|
|
log_file_path = args.log_file
|
|
setup_logging(log_level=log_level_int, log_file=log_file_path)
|
|
|
|
# 3. Log application start
|
|
logging.info(f"Starting ProjectUtility application (via -m execution)...")
|
|
logging.info(f"Repository Root: {REPO_ROOT}")
|
|
logging.info(f"Application Source Root: {APP_SOURCE_ROOT}")
|
|
|
|
# 4. Initialize the main GUI window
|
|
root_tk: tk.Tk | None = None # Initialize with None
|
|
main_app: MainWindow | None = None # Initialize with None
|
|
try:
|
|
# Create the main Tkinter root window
|
|
root_tk = tk.Tk()
|
|
# Hide the window initially until setup is complete
|
|
root_tk.withdraw()
|
|
|
|
# Create the main application instance (passing the root window)
|
|
# MainWindow class was imported earlier
|
|
main_app = MainWindow(root_tk)
|
|
logging.info("Main application window initialized successfully.")
|
|
|
|
# Now show the window (MainWindow __init__ should handle deiconify if needed)
|
|
# Or call root_tk.deiconify() here if MainWindow doesn't do it.
|
|
# Assuming MainWindow's __init__ calls self.root.deiconify() at the end.
|
|
|
|
except Exception as e:
|
|
logging.critical(
|
|
"CRITICAL ERROR: Failed to create the main application window.",
|
|
exc_info=True,
|
|
)
|
|
# Attempt to show a GUI error message
|
|
try:
|
|
temp_root = tk.Tk()
|
|
temp_root.withdraw()
|
|
messagebox.showerror(
|
|
"Fatal Application Error",
|
|
f"Failed to initialize the main window.\n\nError: {e}\n\nCheck logs for more details.",
|
|
)
|
|
temp_root.destroy()
|
|
except Exception as tk_error:
|
|
logging.error(
|
|
f"Could not display Tkinter error message for window init failure: {tk_error}"
|
|
)
|
|
# Ensure the partially created root window is destroyed if it exists
|
|
if root_tk and root_tk.winfo_exists():
|
|
try:
|
|
root_tk.destroy()
|
|
except Exception:
|
|
logging.error("Error trying to destroy main window after init failure.")
|
|
sys.exit(1) # Exit the application
|
|
|
|
# 5. Start the Tkinter event loop
|
|
logging.info("Starting Tkinter main event loop.")
|
|
try:
|
|
# This blocks until the main window is closed
|
|
if root_tk: # Ensure root_tk was created
|
|
root_tk.mainloop()
|
|
else:
|
|
logging.error("Cannot start mainloop: Root window is not available.")
|
|
sys.exit(1)
|
|
|
|
except KeyboardInterrupt:
|
|
# Handle Ctrl+C gracefully if possible
|
|
logging.info("Application interrupted by user (KeyboardInterrupt).")
|
|
# Attempt cleanup, similar to _on_close logic in MainWindow
|
|
if root_tk and main_app and hasattr(main_app, "_on_close"):
|
|
try:
|
|
logging.debug("Calling main_app._on_close() due to KeyboardInterrupt.")
|
|
main_app._on_close() # Trigger the application's close handler
|
|
except Exception as close_err:
|
|
logging.error(
|
|
f"Error during _on_close after KeyboardInterrupt: {close_err}"
|
|
)
|
|
# Force destroy if _on_close fails or doesn't exist
|
|
if root_tk.winfo_exists():
|
|
root_tk.destroy()
|
|
elif root_tk and root_tk.winfo_exists():
|
|
logging.debug("Destroying root window directly after KeyboardInterrupt.")
|
|
root_tk.destroy()
|
|
|
|
except Exception as loop_error:
|
|
logging.exception(
|
|
f"An unexpected error occurred in the Tkinter main loop: {loop_error}"
|
|
)
|
|
# Attempt to cleanup and exit
|
|
if root_tk and root_tk.winfo_exists():
|
|
try:
|
|
root_tk.destroy()
|
|
except Exception:
|
|
pass
|
|
sys.exit(1)
|
|
|
|
finally:
|
|
# This block executes whether mainloop finishes normally or via exception/interrupt
|
|
logging.debug("Tkinter main loop has ended.")
|
|
|
|
# 6. Application finished
|
|
logging.info("ProjectUtility application finished.")
|
|
sys.exit(0) # Explicitly exit with success code
|
|
|
|
|
|
# --- Main Execution Block ---
|
|
# This block is executed ONLY when the script is run directly
|
|
# or via 'python -m projectutility'. It is not executed if
|
|
# the module is imported elsewhere.
|
|
if __name__ == "__main__":
|
|
# Call the main function to start the application
|
|
main()
|