From a91820848db63e1e8ba1d49836273b7185426039 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Mon, 10 Nov 2025 14:43:04 +0100 Subject: [PATCH] sistemata la funzione di normalizzazione con anche le librerie custom importate in maniera assoluta, rivisitazione della gui --- dependencyanalyzer/core/analyzer.py | 11 ++ dependencyanalyzer/core/project_normalizer.py | 184 ++++++------------ dependencyanalyzer/gui.py | 74 ++++++- 3 files changed, 141 insertions(+), 128 deletions(-) diff --git a/dependencyanalyzer/core/analyzer.py b/dependencyanalyzer/core/analyzer.py index e066b1c..31d9365 100644 --- a/dependencyanalyzer/core/analyzer.py +++ b/dependencyanalyzer/core/analyzer.py @@ -33,6 +33,10 @@ MODULE_NAME_TO_PACKAGE_NAME_MAP: Dict[str, str] = { # Modules that are often incorrectly identified as external. FALSE_POSITIVE_EXTERNAL_MODULES: Set[str] = {"mpl_toolkits"} +# Modules that are known to be local to the project's ecosystem but are +# not in the standard scan path (e.g., sibling directories). +PROJECT_SPECIFIC_LOCAL_MODULES: Set[str] = {"geoelevation"} + class ImportExtractor(ast.NodeVisitor): """ @@ -163,6 +167,13 @@ def find_project_modules_and_dependencies( ) continue + # Ignore known project-specific local modules that are not on PyPI + if imp_module in PROJECT_SPECIFIC_LOCAL_MODULES: + logger.info( + f"Skipping known project-specific local module: '{imp_module}'" + ) + continue + # Ignore known false positives if imp_module in FALSE_POSITIVE_EXTERNAL_MODULES: logger.info(f"Skipping known false positive: '{imp_module}'") diff --git a/dependencyanalyzer/core/project_normalizer.py b/dependencyanalyzer/core/project_normalizer.py index fb502fa..71a4f25 100644 --- a/dependencyanalyzer/core/project_normalizer.py +++ b/dependencyanalyzer/core/project_normalizer.py @@ -1,7 +1,6 @@ # dependency_analyzer/core/project_normalizer.py """ Handles the logic for normalizing a Python project structure. - This includes creating virtual environments, generating configuration files, and installing dependencies in an isolated manner. """ @@ -9,201 +8,140 @@ import logging import subprocess import sys from pathlib import Path -from typing import Dict, List, Tuple +from typing import Callable, Dict, List, Optional, Tuple -# Import functions from other core modules that we will reuse from . import analyzer, package_manager -# --- Logger Configuration --- logger = logging.getLogger(__name__) def check_project_status(project_path: Path) -> Dict[str, bool]: - """ - Analyzes a project directory to check for key normalization indicators. - """ if not project_path or not project_path.is_dir(): return {'has_venv': False, 'has_requirements': False, 'has_pyproject': False} - - status = { - 'has_venv': (project_path / '.venv').is_dir(), - 'has_requirements': (project_path / 'requirements.txt').is_file(), - 'has_pyproject': (project_path / 'pyproject.toml').is_file(), - } + status = {'has_venv': (project_path / '.venv').is_dir(), + 'has_requirements': (project_path / 'requirements.txt').is_file(), + 'has_pyproject': (project_path / 'pyproject.toml').is_file()} logger.info(f"Project status check for '{project_path.name}': {status}") return status -def _run_command( - command: List[str], cwd: Path, description: str -) -> Tuple[bool, str]: - """ - A helper to run a shell command and return its status and output. - - Args: - command: The command to run as a list of strings. - cwd: The working directory for the command. - description: A brief description of the action for logging. - - Returns: - A tuple (success_flag, message). - """ +def _run_command(command: List[str], cwd: Path, description: str) -> Tuple[bool, str]: logger.info(f"Running task: {description}...") logger.debug(f"Executing command: {' '.join(command)}") try: proc = subprocess.run( - command, - cwd=cwd, - capture_output=True, - text=True, - encoding="utf-8", - errors="ignore", - check=True, # Raise an exception for non-zero exit codes - timeout=600, # 10-minute timeout + command, cwd=cwd, capture_output=True, text=True, encoding="utf-8", + errors="ignore", check=True, timeout=600, creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0), ) - message = f"Task '{description}' completed successfully." - if proc.stdout and proc.stdout.strip(): - logger.debug(f"Output from '{description}':\n{proc.stdout.strip()}") - return True, message + output = proc.stdout.strip() or f"Task '{description}' completed successfully." + return True, output except FileNotFoundError: - msg = f"Error: Command '{command[0]}' not found. Is Python correctly installed and in PATH?" - logger.error(msg) + msg = f"Error: Command '{command[0]}' not found. Is Python in PATH?" return False, msg except subprocess.TimeoutExpired: - msg = f"Error: Task '{description}' timed out after 10 minutes." - logger.error(msg) + msg = f"Error: Task '{description}' timed out." return False, msg except subprocess.CalledProcessError as e: - error_details = e.stderr.strip() or e.stdout.strip() - msg = f"Error during '{description}':\n{error_details}" - logger.error(msg) + msg = f"Error during '{description}':\n{e.stderr.strip() or e.stdout.strip()}" return False, msg except Exception as e: msg = f"An unexpected error occurred during '{description}': {e}" - logger.exception(msg) return False, msg def _get_venv_python_path(project_path: Path) -> Path: - """ - Determines the path to the Python executable inside the .venv directory. - """ - if sys.platform == "win32": - return project_path / ".venv" / "Scripts" / "python.exe" - else: - return project_path / ".venv" / "bin" / "python" + return project_path / ".venv" / ("Scripts" if sys.platform == "win32" else "bin") / "python.exe" -def normalize_project(project_path: Path) -> Dict[str, Tuple[bool, str]]: +def normalize_project( + project_path: Path, + progress_callback: Optional[Callable[[str, bool, str], None]] = None +) -> None: """ - Executes the full project normalization workflow. + Executes the full project normalization workflow, reporting progress via a callback. Args: project_path: The root path of the project to normalize. - - Returns: - A dictionary containing the success status and a message for each step. + progress_callback: A function to call with updates. It should accept + (step_key, success_flag, message). """ - results: Dict[str, Tuple[bool, str]] = {} + def report_progress(step_key: str, success: bool, message: str): + if progress_callback: + progress_callback(step_key, success, message) + # Log to file/console as well + log_level = logging.INFO if success else logging.ERROR + logging.log(log_level, f"Step '{step_key}': {message}") # --- Step 1: Create Virtual Environment --- venv_path = project_path / ".venv" if venv_path.exists(): - logger.info("Virtual environment '.venv' already exists. Skipping creation.") - results["venv_creation"] = (True, "Virtual environment '.venv' already exists.") + report_progress("venv_creation", True, "Virtual environment '.venv' already exists.") else: success, message = _run_command( - [sys.executable, "-m", "venv", ".venv"], - cwd=project_path, - description="Create virtual environment", + [sys.executable, "-m", "venv", ".venv"], cwd=project_path, description="Create virtual environment" ) - results["venv_creation"] = (success, message) - if not success: - return results # Stop if venv creation fails + report_progress("venv_creation", success, message) + if not success: return # --- Step 2: Analyze Dependencies --- - logger.info("Analyzing project dependencies using AST...") try: - # Determine scan path (same logic as in the GUI) scan_path = project_path potential_sub_pkg = project_path / project_path.name.lower() - if potential_sub_pkg.is_dir(): - scan_path = potential_sub_pkg + if potential_sub_pkg.is_dir(): scan_path = potential_sub_pkg std_lib_info, external_info = analyzer.find_project_modules_and_dependencies( repo_path=project_path, scan_path=scan_path ) - results["analysis"] = (True, f"Analysis found {len(external_info)} external dependencies.") + report_progress("analysis", True, f"Analysis found {len(external_info)} external dependencies.") except Exception as e: - msg = f"Failed to analyze project dependencies: {e}" - logger.exception(msg) - results["analysis"] = (False, msg) - return results + report_progress("analysis", False, f"Failed to analyze project dependencies: {e}") + return # --- Step 3: Generate requirements.txt --- - logger.info("Generating requirements.txt file...") try: req_file = package_manager.generate_requirements_file( project_path, external_info, std_lib_info ) - results["requirements_generation"] = (True, f"Successfully created '{req_file.name}'.") + report_progress("requirements_generation", True, f"Successfully created '{req_file.name}'.") except Exception as e: - msg = f"Failed to generate requirements.txt: {e}" - logger.exception(msg) - results["requirements_generation"] = (False, msg) - return results + report_progress("requirements_generation", False, f"Failed to generate requirements.txt: {e}") + return # --- Step 4: Install Dependencies into .venv --- if not external_info: - logger.info("No external dependencies to install.") - results["dependency_installation"] = (True, "No external dependencies found to install.") + report_progress("dependency_installation", True, "No external dependencies found to install.") else: venv_python = _get_venv_python_path(project_path) - success, message = _run_command( - [str(venv_python), "-m", "pip", "install", "-r", "requirements.txt"], - cwd=project_path, - description="Install dependencies into .venv", - ) - results["dependency_installation"] = (success, message) - if not success: - return results # Stop if installation fails + # We now install one by one for better feedback + all_ok = True + error_messages = [] + for package_name in sorted(external_info.keys()): + report_progress("dependency_installation", True, f"Installing '{package_name}'...") + success, message = _run_command( + [str(venv_python), "-m", "pip", "install", package_name], + cwd=project_path, description=f"Install {package_name}" + ) + if not success: + all_ok = False + error_messages.append(message) + + if all_ok: + report_progress("dependency_installation", True, "All dependencies installed successfully.") + else: + final_error = "Failed to install one or more dependencies:\n" + "\n\n".join(error_messages) + report_progress("dependency_installation", False, final_error) + # We don't return here, to allow pyproject.toml creation anyway # --- Step 5: Create pyproject.toml --- pyproject_path = project_path / "pyproject.toml" if pyproject_path.exists(): - logger.info("'pyproject.toml' already exists. Skipping creation.") - results["pyproject_creation"] = (True, "'pyproject.toml' already exists.") + report_progress("pyproject_creation", True, "'pyproject.toml' already exists.") else: - logger.info("Creating a minimal pyproject.toml file...") project_name = project_path.name.lower().replace("_", "-") - pyproject_content = f"""[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "{project_name}" -version = "0.1.0" -description = "A short description of the project." -readme = "README.md" -requires-python = ">=3.8" -# Add other classifiers or dependencies here if needed -# dependencies = [ -# "dependency1", -# "dependency2", -# ] - -[project.urls] -Homepage = "https://example.com" -""" + pyproject_content = f"""[build-system]\nrequires = ["setuptools>=61.0"]\nbuild-backend = "setuptools.build_meta"\n\n[project]\nname = "{project_name}"\nversion = "0.1.0"\n""" try: pyproject_path.write_text(pyproject_content, encoding="utf-8") - msg = "Successfully created a minimal 'pyproject.toml'." - logger.info(msg) - results["pyproject_creation"] = (True, msg) + report_progress("pyproject_creation", True, "Successfully created a minimal 'pyproject.toml'.") except IOError as e: - msg = f"Failed to write 'pyproject.toml': {e}" - logger.error(msg) - results["pyproject_creation"] = (False, msg) - - return results \ No newline at end of file + report_progress("pyproject_creation", False, f"Failed to write 'pyproject.toml': {e}") \ No newline at end of file diff --git a/dependencyanalyzer/gui.py b/dependencyanalyzer/gui.py index c988408..ae62152 100644 --- a/dependencyanalyzer/gui.py +++ b/dependencyanalyzer/gui.py @@ -344,16 +344,81 @@ class DependencyAnalyzerApp(tk.Frame): logging.log(logging.INFO if success else logging.ERROR, f"{task_name}: {message}") if not success: messagebox.showerror(f"{task_name} Failed", message) + # --- Methods for "Project Normalizer" Tab --- # --- Methods for "Project Normalizer" Tab --- def _normalize_project_threaded(self) -> None: """Starts the project normalization process in a background thread.""" if not self.selected_repository_path: messagebox.showwarning("Warning", "Please select a project repository first.") return + + # Clear previous results before starting + self.normalizer_steps_list.delete(0, tk.END) + self.normalizer_details_text.config(state=tk.NORMAL) + self.normalizer_details_text.delete('1.0', tk.END) + self.normalizer_details_text.config(state=tk.DISABLED) + self.normalizer_step_messages.clear() + + # The backend function no longer returns a value. + # It reports progress via the callback. self._run_long_task_threaded( - project_normalizer.normalize_project, self.selected_repository_path, - callback_success=self._normalize_project_callback, + project_normalizer.normalize_project, + self.selected_repository_path, + self._update_normalizer_progress # Pass the progress callback function ) + + def _update_normalizer_progress(self, step_key: str, success: bool, message: str) -> None: + """ + Thread-safe method to update the normalizer UI with progress. + This function is called by the background thread. + """ + # Ensure GUI updates happen in the main thread + self.master.after(0, self._populate_normalizer_step, step_key, success, message) + + def _populate_normalizer_step(self, step_key: str, success: bool, message: str): + """The actual GUI update logic.""" + step_map = { + "venv_creation": "1. Create Virtual Environment", "analysis": "2. Analyze Dependencies", + "requirements_generation": "3. Generate requirements.txt", "dependency_installation": "4. Install Dependencies", + "pyproject_creation": "5. Create pyproject.toml", + } + + # This logic handles both new steps and updates to existing steps (like installation) + step_name = step_map.get(step_key, step_key.replace("_", " ").title()) + icon = "✓" if success else "✗" + + # Find if the step is already in the list + list_items = self.normalizer_steps_list.get(0, tk.END) + try: + # Find the index of the main step (e.g., "4. Install Dependencies") + main_step_name_prefix = step_name.split(" ")[0] + index = next(i for i, item in enumerate(list_items) if item.strip().startswith(main_step_name_prefix)) + + # Update the main step's final status + self.normalizer_steps_list.delete(index) + self.normalizer_steps_list.insert(index, f" {icon} {step_name}") + self.normalizer_steps_list.itemconfig(index, {'fg': 'green' if success else 'red'}) + self.normalizer_step_messages[index] = message + + except StopIteration: + # If step is not found, it's a new main step + index = self.normalizer_steps_list.size() + self.normalizer_steps_list.insert(tk.END, f" {icon} {step_name}") + self.normalizer_steps_list.itemconfig(index, {'fg': 'green' if success else 'red'}) + self.normalizer_step_messages.append(message) + + # Handle sub-step messages (like individual package installations) + if "Installing '" in message and success: + self.normalizer_steps_list.insert(tk.END, f" - {message.split(' ')[1]}") + self.normalizer_steps_list.itemconfig(tk.END, {'fg': 'grey'}) + self.normalizer_step_messages.append(message) # Add message for sub-step too + + # Auto-select the last updated/added item + last_index = self.normalizer_steps_list.size() - 1 + self.normalizer_steps_list.selection_clear(0, tk.END) + self.normalizer_steps_list.selection_set(last_index) + self.normalizer_steps_list.see(last_index) # Scroll to the new item + self._on_normalizer_step_select(None) # Show details def _normalize_project_callback(self, results: Dict[str, Tuple[bool, str]]) -> None: """Handles the results of the normalization, populating the new UI.""" @@ -383,9 +448,9 @@ class DependencyAnalyzerApp(tk.Frame): def _on_normalizer_step_select(self, event: Optional[tk.Event]) -> None: """Displays the details for the currently selected normalization step.""" + # This function remains largely the same selected_indices = self.normalizer_steps_list.curselection() - if not selected_indices: - return + if not selected_indices: return selected_index = selected_indices[0] try: @@ -395,7 +460,6 @@ class DependencyAnalyzerApp(tk.Frame): self.normalizer_details_text.insert('1.0', message) self.normalizer_details_text.config(state=tk.DISABLED) except IndexError: - # This should not happen, but it's a safe fallback pass if __name__ == "__main__":