# 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()