aggiunto normalizzazione del progetto python
This commit is contained in:
parent
b3b8736e36
commit
e8485703b3
@ -1,25 +1,26 @@
|
||||
# dependencyanalyzer/__main__.py
|
||||
import tkinter as tk
|
||||
import logging # Import standard logging
|
||||
import sys # For checking command line args, etc. (optional)
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
|
||||
# Use absolute import assuming 'dependencyanalyzer' is runnable with `python -m`
|
||||
# Or use relative if structure guarantees it. Let's assume absolute is safer if structure might change.
|
||||
# If running with `python -m dependencyanalyzer` from parent dir, this works.
|
||||
try:
|
||||
from dependencyanalyzer.gui import DependencyAnalyzerApp
|
||||
from dependencyanalyzer import (
|
||||
core,
|
||||
) # To access core.logger if needed, though handled by root
|
||||
# --- MODIFIED IMPORT ---
|
||||
# We now import the GUI module and the new core package.
|
||||
from dependencyanalyzer import gui
|
||||
from dependencyanalyzer import core # This still works to access the package
|
||||
# --- END MODIFIED IMPORT ---
|
||||
except ImportError:
|
||||
# Fallback for running __main__.py directly inside the package dir during dev
|
||||
# Fallback for running __main__.py directly during development
|
||||
print(
|
||||
"Running __main__.py directly, attempting relative imports...", file=sys.stderr
|
||||
)
|
||||
# --- MODIFIED IMPORT (FALLBACK) ---
|
||||
from gui import DependencyAnalyzerApp
|
||||
import core
|
||||
# --- END MODIFIED IMPORT (FALLBACK) ---
|
||||
|
||||
|
||||
def setup_logging():
|
||||
@ -29,15 +30,12 @@ def setup_logging():
|
||||
|
||||
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
# Use force=True on Python 3.8+ to ensure handlers are replaced if already configured
|
||||
# Use force=True on Python 3.8+ to ensure handlers are replaced
|
||||
kwargs = {"force": True} if sys.version_info >= (3, 8) else {}
|
||||
|
||||
# Basic config - consider adding FileHandler later if needed
|
||||
logging.basicConfig(level=log_level, format=log_format, stream=sys.stdout, **kwargs) # type: ignore
|
||||
|
||||
logging.info(f"Root logger configured. Level: {logging.getLevelName(log_level)}")
|
||||
# Ensure core logger level is at least as verbose as root, if needed
|
||||
# logging.getLogger(core.__name__).setLevel(log_level)
|
||||
|
||||
|
||||
def main():
|
||||
@ -45,12 +43,13 @@ def main():
|
||||
setup_logging() # Configure logging first
|
||||
|
||||
root = tk.Tk()
|
||||
root.minsize(width=800, height=700) # Set a reasonable minimum size
|
||||
app = DependencyAnalyzerApp(master=root)
|
||||
root.minsize(width=800, height=700)
|
||||
# --- MODIFIED CLASS NAME ---
|
||||
# The class is now accessed via the imported gui module
|
||||
app = gui.DependencyAnalyzerApp(master=root)
|
||||
# --- END MODIFIED CLASS NAME ---
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# This block runs when script is executed directly (python __main__.py)
|
||||
# or via python -m dependencyanalyzer
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
0
dependencyanalyzer/core/__init__.py
Normal file
0
dependencyanalyzer/core/__init__.py
Normal file
210
dependencyanalyzer/core/analyzer.py
Normal file
210
dependencyanalyzer/core/analyzer.py
Normal file
@ -0,0 +1,210 @@
|
||||
# dependency_analyzer/core/analyzer.py
|
||||
"""
|
||||
Core static analysis functionality for Python projects.
|
||||
|
||||
This module provides the tools to parse Python source files, extract
|
||||
import statements, and classify them as standard library, external,
|
||||
or project-internal modules.
|
||||
"""
|
||||
import ast
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Set, Tuple, Union, Optional
|
||||
|
||||
from .stdlib_detector import is_standard_library
|
||||
|
||||
# --- Logger Configuration ---
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Type Aliases ---
|
||||
# Structure: {'pypi_name': {'locations': {'file1', 'file2'}, 'version': '1.2.3', ...}}
|
||||
DependencyInfo = Dict[str, Dict[str, Union[Set[str], Optional[str], str]]]
|
||||
|
||||
# --- Constants ---
|
||||
# Maps common import names to their actual PyPI package names.
|
||||
MODULE_NAME_TO_PACKAGE_NAME_MAP: Dict[str, str] = {
|
||||
"PIL": "Pillow",
|
||||
"cv2": "opencv-python",
|
||||
# Add other common mappings here if needed
|
||||
}
|
||||
|
||||
# Modules that are often incorrectly identified as external.
|
||||
FALSE_POSITIVE_EXTERNAL_MODULES: Set[str] = {"mpl_toolkits"}
|
||||
|
||||
|
||||
class ImportExtractor(ast.NodeVisitor):
|
||||
"""
|
||||
An AST (Abstract Syntax Tree) visitor that extracts top-level module imports
|
||||
from a Python source file.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path_str: str):
|
||||
"""
|
||||
Initializes the visitor.
|
||||
Args:
|
||||
file_path_str: The relative path to the file being analyzed,
|
||||
used for tracking where modules are imported.
|
||||
"""
|
||||
super().__init__()
|
||||
self.file_path_str = file_path_str
|
||||
self.imported_modules: Set[Tuple[str, str]] = set()
|
||||
|
||||
def visit_Import(self, node: ast.Import):
|
||||
"""Handles 'import module' statements."""
|
||||
for alias in node.names:
|
||||
# We only care about the top-level package (e.g., 'os' from 'os.path')
|
||||
module_name = alias.name.split(".")[0]
|
||||
if module_name:
|
||||
self.imported_modules.add((module_name, self.file_path_str))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom):
|
||||
"""Handles 'from module import something' statements."""
|
||||
# We only consider absolute imports (level=0). Relative imports are local.
|
||||
if node.module and node.level == 0:
|
||||
module_name = node.module.split(".")[0]
|
||||
if module_name:
|
||||
self.imported_modules.add((module_name, self.file_path_str))
|
||||
|
||||
|
||||
def find_project_modules_and_dependencies(
|
||||
repo_path: Path,
|
||||
scan_path: Path,
|
||||
) -> Tuple[DependencyInfo, DependencyInfo]:
|
||||
"""
|
||||
Analyzes Python files, identifies project modules, and classifies dependencies.
|
||||
|
||||
This function walks through the `scan_path`, parses each Python file to find
|
||||
imports, and then categorizes each import as either part of the standard
|
||||
library or an external dependency. It intelligently ignores imports that
|
||||
refer to the project's own modules.
|
||||
|
||||
Args:
|
||||
repo_path: The root path of the repository, used for reporting relative paths.
|
||||
scan_path: The specific directory to start the analysis from (can be a subfolder).
|
||||
|
||||
Returns:
|
||||
A tuple containing two dictionaries:
|
||||
- std_lib_info: Information about used standard library modules.
|
||||
- external_deps_info: Information about detected external dependencies.
|
||||
"""
|
||||
all_imports_locations: Dict[str, Set[str]] = {}
|
||||
|
||||
# If scanning a sub-directory that matches the repo name, assume it's the main package.
|
||||
# Imports of this name will be ignored as project-internal.
|
||||
main_project_package_name: Optional[str] = None
|
||||
if repo_path != scan_path and scan_path.name == repo_path.name.lower():
|
||||
main_project_package_name = scan_path.name
|
||||
logger.info(
|
||||
f"Assuming '{main_project_package_name}' is the main project package."
|
||||
)
|
||||
|
||||
logger.info(f"Analyzing Python files for imports in '{scan_path}'...")
|
||||
excluded_dirs = {
|
||||
"venv", ".venv", "env", ".env", "docs", "tests", "test",
|
||||
"site-packages", "dist-packages", "__pycache__", ".git", ".hg",
|
||||
".svn", ".tox", ".nox", "build", "dist", "*.egg-info",
|
||||
}
|
||||
file_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(scan_path, topdown=True):
|
||||
# Prune the directory list to avoid walking into excluded folders
|
||||
dirs[:] = [d for d in dirs if d not in excluded_dirs and not d.startswith(".")]
|
||||
|
||||
current_root_path = Path(root)
|
||||
for file_name in files:
|
||||
if not file_name.endswith(".py"):
|
||||
continue
|
||||
|
||||
file_path_obj = current_root_path / file_name
|
||||
file_count += 1
|
||||
|
||||
try:
|
||||
# Use repo_path as the base for user-friendly relative paths
|
||||
report_rel_path_str = str(file_path_obj.relative_to(repo_path))
|
||||
except ValueError:
|
||||
report_rel_path_str = str(file_path_obj)
|
||||
logger.warning(
|
||||
f"File path '{file_path_obj}' is not relative to repo root '{repo_path}'."
|
||||
)
|
||||
|
||||
logger.debug(f"Parsing: {report_rel_path_str}")
|
||||
try:
|
||||
with open(file_path_obj, "r", encoding="utf-8", errors="ignore") as f:
|
||||
source_code = f.read()
|
||||
|
||||
tree = ast.parse(source_code, filename=str(file_path_obj))
|
||||
extractor = ImportExtractor(file_path_str=report_rel_path_str)
|
||||
extractor.visit(tree)
|
||||
|
||||
for module, path in extractor.imported_modules:
|
||||
all_imports_locations.setdefault(module, set()).add(path)
|
||||
|
||||
except SyntaxError as e:
|
||||
logger.warning(f"Syntax error in '{report_rel_path_str}': {e}. Skipping.")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error processing file '{report_rel_path_str}': {e}")
|
||||
|
||||
logger.info(
|
||||
f"Analyzed {file_count} Python files. Found {len(all_imports_locations)} unique top-level imports."
|
||||
)
|
||||
logger.info("Classifying imports and fetching package versions...")
|
||||
|
||||
std_libs: DependencyInfo = {}
|
||||
external_deps: DependencyInfo = {}
|
||||
|
||||
for imp_module, locations in all_imports_locations.items():
|
||||
# Ignore if it matches the main project package name (project-internal import)
|
||||
if main_project_package_name and imp_module == main_project_package_name:
|
||||
logger.info(
|
||||
f"Skipping '{imp_module}' as it matches the main project package."
|
||||
)
|
||||
continue
|
||||
|
||||
# Ignore known false positives
|
||||
if imp_module in FALSE_POSITIVE_EXTERNAL_MODULES:
|
||||
logger.info(f"Skipping known false positive: '{imp_module}'")
|
||||
continue
|
||||
|
||||
# Classify as standard library or external
|
||||
if is_standard_library(imp_module):
|
||||
logger.debug(f"'{imp_module}' classified as standard library.")
|
||||
std_libs[imp_module] = {"locations": locations, "version": None}
|
||||
else:
|
||||
# It's an external dependency, process it
|
||||
pypi_name = MODULE_NAME_TO_PACKAGE_NAME_MAP.get(imp_module, imp_module)
|
||||
orig_imp = imp_module if pypi_name != imp_module else None
|
||||
logger.debug(
|
||||
f"'{imp_module}' (PyPI: '{pypi_name}') is external. Fetching version..."
|
||||
)
|
||||
|
||||
version: Optional[str] = None
|
||||
try:
|
||||
version = importlib.metadata.version(pypi_name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
logger.warning(
|
||||
f"Version for '{pypi_name}' not found in the current environment."
|
||||
)
|
||||
|
||||
# Aggregate information for the dependency
|
||||
dep_data = external_deps.setdefault(
|
||||
pypi_name,
|
||||
{"locations": set(), "version": version, "original_import_name": None},
|
||||
)
|
||||
dep_data["locations"].update(locations)
|
||||
|
||||
# Record the original import name if it was mapped
|
||||
if orig_imp and dep_data.get("original_import_name") is None:
|
||||
dep_data["original_import_name"] = orig_imp # type: ignore
|
||||
|
||||
# Update version if it was found now but not before (unlikely but safe)
|
||||
if dep_data.get("version") is None and version is not None:
|
||||
dep_data["version"] = version
|
||||
|
||||
logger.info(
|
||||
f"Classification complete: {len(std_libs)} stdlib modules, "
|
||||
f"{len(external_deps)} external dependencies."
|
||||
)
|
||||
return std_libs, external_deps
|
||||
599
dependencyanalyzer/core/package_manager.py
Normal file
599
dependencyanalyzer/core/package_manager.py
Normal file
@ -0,0 +1,599 @@
|
||||
# dependency_analyzer/core/package_manager.py
|
||||
"""
|
||||
Handles Python package management tasks.
|
||||
|
||||
This module includes functions for:
|
||||
- Generating and reading requirements.txt files.
|
||||
- Downloading packages for offline installation using pip.
|
||||
- Creating installation scripts (.bat, .sh).
|
||||
- Comparing project requirements with the currently installed packages.
|
||||
- Updating packages in the local environment.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from .analyzer import DependencyInfo # Import type alias for clarity
|
||||
|
||||
# --- Logger Configuration ---
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Optional 'packaging' library import for robust version/specifier parsing ---
|
||||
try:
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import InvalidVersion, parse as parse_version
|
||||
|
||||
PACKAGING_AVAILABLE = True
|
||||
logger.debug("Successfully imported the 'packaging' library for robust parsing.")
|
||||
except ImportError:
|
||||
# Define dummy classes for type hinting and checks if 'packaging' is not available
|
||||
class Requirement: # type: ignore
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
class InvalidVersion(Exception): # type: ignore
|
||||
pass
|
||||
parse_version = None
|
||||
PACKAGING_AVAILABLE = False
|
||||
logger.warning(
|
||||
"'packaging' library not found. Version comparison will be less robust."
|
||||
)
|
||||
|
||||
|
||||
def find_main_script(repo_path: Path) -> Optional[Path]:
|
||||
"""
|
||||
Finds the main executable script (__main__.py) in standard project locations.
|
||||
|
||||
Args:
|
||||
repo_path: The root directory of the repository.
|
||||
|
||||
Returns:
|
||||
The Path object to the main script if found, otherwise None.
|
||||
"""
|
||||
repo_name = repo_path.name
|
||||
script_subfolder_name = repo_name.lower()
|
||||
|
||||
# Standard location: repo_root/repo_name/__main__.py
|
||||
path_in_subdir = repo_path / script_subfolder_name / "__main__.py"
|
||||
# Fallback location: repo_root/__main__.py
|
||||
path_in_root = repo_path / "__main__.py"
|
||||
|
||||
logger.info(
|
||||
f"Searching for main script at '{path_in_subdir}' or '{path_in_root}'"
|
||||
)
|
||||
if path_in_subdir.is_file():
|
||||
logger.info(f"Main script found: {path_in_subdir}")
|
||||
return path_in_subdir
|
||||
if path_in_root.is_file():
|
||||
logger.info(f"Main script found: {path_in_root}")
|
||||
return path_in_root
|
||||
|
||||
logger.warning("Main script (__main__.py) not found in standard locations.")
|
||||
return None
|
||||
|
||||
|
||||
def generate_requirements_file(
|
||||
repo_path: Path,
|
||||
external_deps_info: DependencyInfo,
|
||||
std_lib_deps_info: DependencyInfo,
|
||||
) -> Path:
|
||||
"""
|
||||
Generates a detailed requirements.txt file in the repository's root.
|
||||
|
||||
Args:
|
||||
repo_path: The root directory of the repository.
|
||||
external_deps_info: Dictionary of detected external dependencies.
|
||||
std_lib_deps_info: Dictionary of detected standard library modules.
|
||||
|
||||
Returns:
|
||||
The path to the generated requirements.txt file.
|
||||
"""
|
||||
req_file_path = repo_path / "requirements.txt"
|
||||
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
logger.info(f"Generating '{req_file_path}'...")
|
||||
|
||||
try:
|
||||
with open(req_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Requirements generated by DependencyAnalyzer for {repo_path.name}\n")
|
||||
f.write(f"# Python Version (analysis env): {py_ver}\n\n")
|
||||
|
||||
f.write(f"# --- Standard Library Modules Used (part of Python {py_ver}) ---\n")
|
||||
if not std_lib_deps_info:
|
||||
f.write("# (No stdlib imports detected)\n")
|
||||
else:
|
||||
for name in sorted(std_lib_deps_info.keys()):
|
||||
locs = std_lib_deps_info[name].get("locations", set())
|
||||
# Ensure locs is a list for consistent sorting
|
||||
loc_list = sorted(list(locs)) if isinstance(locs, set) else []
|
||||
loc_str = ""
|
||||
if loc_list:
|
||||
# Show first 3 locations for brevity
|
||||
preview = ", ".join(loc_list[:3])
|
||||
ellipsis = ", ..." if len(loc_list) > 3 else ""
|
||||
loc_str = f"Used in: {preview}{ellipsis}"
|
||||
f.write(f"# {name} ({loc_str})\n".replace(" ()", ""))
|
||||
|
||||
f.write("\n# --- External Dependencies (for pip install) ---\n")
|
||||
if not external_deps_info:
|
||||
f.write("# (No external dependencies detected)\n")
|
||||
else:
|
||||
for pypi_name in sorted(external_deps_info.keys()):
|
||||
info = external_deps_info[pypi_name]
|
||||
ver = info.get("version")
|
||||
orig_imp = info.get("original_import_name")
|
||||
|
||||
# Construct a requirement string, e.g., "package==1.2.3" or "package"
|
||||
req_line = f"{pypi_name}=={ver}" if ver else pypi_name
|
||||
|
||||
locs = info.get("locations", set())
|
||||
loc_list = sorted(list(locs)) if isinstance(locs, set) else []
|
||||
|
||||
comment_lines = []
|
||||
if loc_list:
|
||||
imp_as = f"(imported as '{orig_imp}') " if orig_imp else ""
|
||||
preview = ", ".join(loc_list[:3])
|
||||
ellipsis = ", ..." if len(loc_list) > 3 else ""
|
||||
comment_lines.append(f"# Found {imp_as}in: {preview}{ellipsis}")
|
||||
|
||||
if not ver:
|
||||
comment_lines.append("# Version not detected in analysis environment.")
|
||||
|
||||
if comment_lines:
|
||||
f.write("\n".join(comment_lines) + "\n")
|
||||
f.write(f"{req_line}\n\n")
|
||||
|
||||
logger.info(f"Successfully generated '{req_file_path}'.")
|
||||
except IOError as e:
|
||||
logger.exception(f"Error writing to '{req_file_path}': {e}")
|
||||
raise
|
||||
return req_file_path
|
||||
|
||||
|
||||
def copy_requirements_to_packages_dir(repo_path: Path, packages_dir: Path) -> bool:
|
||||
"""
|
||||
Copies the main requirements.txt file to the offline packages directory.
|
||||
"""
|
||||
source_req = repo_path / "requirements.txt"
|
||||
dest_req = packages_dir / "requirements.txt"
|
||||
|
||||
if not source_req.is_file():
|
||||
logger.warning(f"Source '{source_req}' not found, cannot copy.")
|
||||
return False
|
||||
try:
|
||||
shutil.copy2(source_req, dest_req)
|
||||
logger.info(f"Copied '{source_req.name}' to '{packages_dir}'.")
|
||||
return True
|
||||
except (IOError, shutil.Error) as e:
|
||||
logger.exception(f"Failed to copy '{source_req.name}': {e}")
|
||||
return False
|
||||
|
||||
|
||||
def download_packages(
|
||||
repo_path: Path, requirements_file_path: Path
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Downloads all required packages into a local '_req_packages' directory.
|
||||
|
||||
Args:
|
||||
repo_path: The root directory of the repository.
|
||||
requirements_file_path: Path to the requirements.txt file to use.
|
||||
|
||||
Returns:
|
||||
A tuple (success_flag, summary_message).
|
||||
"""
|
||||
packages_dir = repo_path / "_req_packages"
|
||||
packages_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Starting package download to '{packages_dir}'...")
|
||||
|
||||
if not requirements_file_path.is_file():
|
||||
msg = f"Source requirements file '{requirements_file_path}' not found."
|
||||
logger.error(msg)
|
||||
return False, msg
|
||||
|
||||
copy_requirements_to_packages_dir(repo_path, packages_dir)
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-m", "pip", "download",
|
||||
"-r", str(requirements_file_path.resolve()),
|
||||
"-d", str(packages_dir.resolve()),
|
||||
"--no-cache-dir",
|
||||
"--disable-pip-version-check",
|
||||
]
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
try:
|
||||
# Using subprocess.run for a simpler blocking call
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
timeout=600, # 10-minute timeout
|
||||
check=True, # Raises CalledProcessError on non-zero exit codes
|
||||
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
|
||||
)
|
||||
if proc.stderr and proc.stderr.strip():
|
||||
logger.warning(f"Pip download reported warnings:\n{proc.stderr.strip()}")
|
||||
|
||||
summary = f"All packages from '{requirements_file_path.name}' downloaded successfully."
|
||||
logger.info(summary)
|
||||
return True, summary
|
||||
|
||||
except FileNotFoundError:
|
||||
msg = f"Command not found: '{sys.executable}'. Is Python/pip in PATH?"
|
||||
logger.error(msg)
|
||||
return False, msg
|
||||
except subprocess.TimeoutExpired:
|
||||
msg = "Package download timed out after 10 minutes."
|
||||
logger.error(msg)
|
||||
return False, msg
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_details = f"Pip error (code {e.returncode}):\n{e.stderr.strip()}"
|
||||
logger.error(error_details)
|
||||
return False, f"Failed to download packages. {error_details}"
|
||||
except Exception as e:
|
||||
logger.exception("An unexpected error occurred during package download.")
|
||||
return False, f"An unexpected error occurred: {e}"
|
||||
|
||||
|
||||
def create_install_scripts(
|
||||
repo_path: Path, requirements_file_path: Path
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Creates install_packages.bat and install_packages.sh scripts.
|
||||
"""
|
||||
packages_dir = repo_path / "_req_packages"
|
||||
packages_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not requirements_file_path.is_file():
|
||||
msg = f"Source requirements file '{requirements_file_path}' not found."
|
||||
return False, msg
|
||||
if not copy_requirements_to_packages_dir(repo_path, packages_dir):
|
||||
return False, "Failed to copy requirements.txt to packages directory."
|
||||
|
||||
req_file_rel_name = "requirements.txt"
|
||||
|
||||
# Windows Batch Script
|
||||
bat_script_content = f"""@echo off
|
||||
cls
|
||||
echo ------------------------------------
|
||||
echo Package Installer for {repo_path.name}
|
||||
echo ------------------------------------
|
||||
echo.
|
||||
echo Checking for a valid Python pip module...
|
||||
python -m pip --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo WARNING: 'python -m pip' failed. Trying 'py -m pip'...
|
||||
py -m pip --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
goto :pip_error
|
||||
) else (
|
||||
set PYTHON_CMD=py
|
||||
)
|
||||
) else (
|
||||
set PYTHON_CMD=python
|
||||
)
|
||||
echo Python pip found.
|
||||
goto :install
|
||||
|
||||
:pip_error
|
||||
echo ERROR: Python pip module not found.
|
||||
echo Please ensure Python is installed and 'pip' is available in your PATH.
|
||||
echo See: https://pip.pypa.io/en/stable/installation/
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:install
|
||||
echo.
|
||||
echo Installing packages from local folder...
|
||||
echo Source Directory: %~dp0
|
||||
echo.
|
||||
%PYTHON_CMD% -m pip install --no-index --find-links "%~dp0" -r "%~dp0{req_file_rel_name}" --disable-pip-version-check
|
||||
if %errorlevel% neq 0 (
|
||||
goto :install_error
|
||||
)
|
||||
|
||||
echo.
|
||||
echo --------------------------
|
||||
echo Installation Successful!
|
||||
echo --------------------------
|
||||
echo.
|
||||
pause
|
||||
exit /b 0
|
||||
|
||||
:install_error
|
||||
echo.
|
||||
echo --------------------------
|
||||
echo ERROR: Installation Failed.
|
||||
echo --------------------------
|
||||
echo Please check the messages above for details.
|
||||
pause
|
||||
exit /b 1
|
||||
"""
|
||||
|
||||
# Linux/macOS Shell Script
|
||||
sh_script_content = f"""#!/bin/sh
|
||||
# Shell script for installing packages on Linux/macOS
|
||||
|
||||
echo "------------------------------------"
|
||||
echo "Package Installer for {repo_path.name}"
|
||||
echo "------------------------------------"
|
||||
echo ""
|
||||
echo "Checking for a valid Python pip module..."
|
||||
|
||||
# Find a valid python command
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
|
||||
PYTHON_CMD="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python -m pip --version >/dev/null 2>&1; then
|
||||
PYTHON_CMD="python"
|
||||
else
|
||||
echo "ERROR: Could not find a working Python pip installation for 'python' or 'python3'."
|
||||
echo "Please ensure Python and pip are correctly installed."
|
||||
echo "See: https://pip.pypa.io/en/stable/installation/"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found pip using: '$PYTHON_CMD'"
|
||||
|
||||
# Get the directory where the script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REQ_FILE="$SCRIPT_DIR/{req_file_rel_name}"
|
||||
|
||||
echo ""
|
||||
echo "Installing packages from: '$SCRIPT_DIR'"
|
||||
echo "Using requirements file: '$REQ_FILE'"
|
||||
echo ""
|
||||
|
||||
"$PYTHON_CMD" -m pip install --no-index --find-links "$SCRIPT_DIR" -r "$REQ_FILE" --disable-pip-version-check
|
||||
INSTALL_STATUS=$?
|
||||
|
||||
echo ""
|
||||
if [ $INSTALL_STATUS -ne 0 ]; then
|
||||
echo "--------------------------"
|
||||
echo "ERROR: Installation Failed."
|
||||
echo "--------------------------"
|
||||
echo "Please check the error messages above."
|
||||
exit 1
|
||||
else
|
||||
echo "--------------------------"
|
||||
echo "Installation Successful!"
|
||||
echo "--------------------------"
|
||||
exit 0
|
||||
fi
|
||||
"""
|
||||
try:
|
||||
bat_path = packages_dir / "install_packages.bat"
|
||||
sh_path = packages_dir / "install_packages.sh"
|
||||
|
||||
with open(bat_path, "w", encoding="utf-8", newline="\r\n") as f:
|
||||
f.write(bat_script_content)
|
||||
logger.info(f"Created Windows install script: {bat_path}")
|
||||
|
||||
with open(sh_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(sh_script_content)
|
||||
os.chmod(sh_path, 0o755) # Make the script executable on Unix-like systems
|
||||
logger.info(f"Created Linux/macOS install script: {sh_path} (executable)")
|
||||
|
||||
return True, f"Installation scripts created in '{packages_dir}'."
|
||||
except IOError as e:
|
||||
logger.exception("Error creating installation scripts.")
|
||||
return False, f"Error creating scripts: {e}"
|
||||
|
||||
|
||||
def get_installed_packages() -> Dict[str, str]:
|
||||
"""
|
||||
Retrieves all packages installed in the current Python environment.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping normalized package names to their versions.
|
||||
"""
|
||||
installed: Dict[str, str] = {}
|
||||
cmd = [
|
||||
sys.executable, "-m", "pip", "list",
|
||||
"--format=json", "--disable-pip-version-check",
|
||||
]
|
||||
logger.debug(f"Getting installed packages using command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
|
||||
)
|
||||
packages = json.loads(proc.stdout)
|
||||
for pkg in packages:
|
||||
# Normalize names to lowercase and hyphens for reliable matching
|
||||
normalized_name = pkg["name"].lower().replace("_", "-")
|
||||
installed[normalized_name] = pkg["version"]
|
||||
logger.info(f"Found {len(installed)} installed packages in the environment.")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Command not found: '{cmd[0]}'. Is Python/pip in PATH?")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"'pip list' command failed (code {e.returncode}): {e.stderr}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse 'pip list' JSON output: {e}")
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error getting installed packages.")
|
||||
|
||||
return installed
|
||||
|
||||
|
||||
def _extract_pkg_name_from_spec(spec: str) -> Optional[str]:
|
||||
"""
|
||||
Helper to extract a package name from a requirement spec string.
|
||||
"""
|
||||
if PACKAGING_AVAILABLE:
|
||||
try:
|
||||
return Requirement(spec).name
|
||||
except Exception:
|
||||
# Fallback to regex if 'packaging' fails on a malformed line
|
||||
pass
|
||||
|
||||
# Basic regex fallback for when 'packaging' is not available or fails
|
||||
match = re.match(r"([a-zA-Z0-9._-]+)", spec)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def compare_requirements_with_installed(
|
||||
requirements_file: Path, installed_packages: Dict[str, str]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Compares a requirements file against the dictionary of installed packages.
|
||||
"""
|
||||
results: List[Dict[str, str]] = []
|
||||
if not requirements_file.is_file():
|
||||
logger.warning(f"Comparison failed: '{requirements_file}' not found.")
|
||||
return results
|
||||
|
||||
logger.info(f"Comparing '{requirements_file.name}' with installed packages...")
|
||||
with open(requirements_file, "r", encoding="utf-8") as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
spec = line.strip()
|
||||
if not spec or spec.startswith("#") or spec.startswith("-"):
|
||||
continue
|
||||
|
||||
pkg_name = _extract_pkg_name_from_spec(spec)
|
||||
if not pkg_name:
|
||||
logger.warning(f"L{line_num}: Could not extract package name from '{spec}'. Skipping.")
|
||||
continue
|
||||
|
||||
required_spec = "any"
|
||||
req_obj = None
|
||||
if PACKAGING_AVAILABLE:
|
||||
try:
|
||||
req_obj = Requirement(spec)
|
||||
# Use str() to handle complex specifiers like '>=1.0,<2.0'
|
||||
required_spec = str(req_obj.specifier) if req_obj.specifier else "any"
|
||||
except Exception as e:
|
||||
logger.warning(f"L{line_num}: Could not parse '{spec}' with 'packaging': {e}")
|
||||
elif "==" in spec:
|
||||
required_spec = spec.split("==", 1)[1].strip()
|
||||
|
||||
norm_name = pkg_name.lower().replace("_", "-")
|
||||
installed_version = installed_packages.get(norm_name)
|
||||
|
||||
status = ""
|
||||
if installed_version:
|
||||
if PACKAGING_AVAILABLE and req_obj and req_obj.specifier:
|
||||
try:
|
||||
if parse_version(installed_version) in req_obj.specifier:
|
||||
status = "OK"
|
||||
else:
|
||||
status = "Version Mismatch"
|
||||
except InvalidVersion:
|
||||
status = "Invalid Installed Version"
|
||||
elif "==" in required_spec: # Basic check if packaging not available
|
||||
status = "OK" if installed_version == required_spec else "Version Mismatch"
|
||||
else:
|
||||
status = "Installed (Version not specified)"
|
||||
else:
|
||||
status = "Not Installed"
|
||||
|
||||
results.append({
|
||||
"package": pkg_name,
|
||||
"required": required_spec,
|
||||
"installed": installed_version or "Not Installed",
|
||||
"status": status,
|
||||
})
|
||||
|
||||
logger.info(f"Comparison finished with {len(results)} entries.")
|
||||
return results
|
||||
|
||||
|
||||
def update_system_packages(
|
||||
packages_to_update: List[str], repo_path: Path
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Updates or installs a list of packages using pip install --upgrade.
|
||||
It reads the full specifier from the project's requirements file.
|
||||
"""
|
||||
if not packages_to_update:
|
||||
return True, "No packages were specified for update."
|
||||
|
||||
req_file_path = repo_path / "requirements.txt"
|
||||
if not req_file_path.is_file():
|
||||
return False, f"Cannot update: '{req_file_path}' not found."
|
||||
|
||||
# Create a map of {normalized_name: original_spec_line} from requirements.txt
|
||||
req_map: Dict[str, str] = {}
|
||||
try:
|
||||
with open(req_file_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
spec = line.strip()
|
||||
if not spec or spec.startswith("#") or spec.startswith("-"):
|
||||
continue
|
||||
pkg_name = _extract_pkg_name_from_spec(spec)
|
||||
if pkg_name:
|
||||
req_map[pkg_name.lower().replace("_", "-")] = spec
|
||||
except IOError as e:
|
||||
return False, f"Error reading '{req_file_path}': {e}"
|
||||
|
||||
# Build a list of specs to pass to pip, based on the user's selection
|
||||
final_specs_to_update: List[str] = []
|
||||
for pkg_name in packages_to_update:
|
||||
norm_name = pkg_name.lower().replace("_", "-")
|
||||
if norm_name in req_map:
|
||||
final_specs_to_update.append(req_map[norm_name])
|
||||
else:
|
||||
# Fallback: if not in requirements, just use the name
|
||||
logger.warning(
|
||||
f"'{pkg_name}' not found in requirements.txt. Upgrading by name only."
|
||||
)
|
||||
final_specs_to_update.append(pkg_name)
|
||||
|
||||
if not final_specs_to_update:
|
||||
return True, "No matching packages found in requirements.txt to update."
|
||||
|
||||
# Use a temporary file to handle complex specifiers safely
|
||||
temp_req_path = repo_path / "_temp_update_reqs.txt"
|
||||
cmd = [
|
||||
sys.executable, "-m", "pip", "install", "--upgrade",
|
||||
"-r", str(temp_req_path.resolve()),
|
||||
"--disable-pip-version-check",
|
||||
]
|
||||
|
||||
try:
|
||||
with open(temp_req_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(final_specs_to_update))
|
||||
|
||||
logger.info(f"Attempting to update {len(final_specs_to_update)} package(s).")
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True, text=True, encoding="utf-8", errors="ignore",
|
||||
timeout=600, check=True,
|
||||
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
|
||||
)
|
||||
|
||||
summary = f"Update completed for: {', '.join(packages_to_update)}."
|
||||
logger.info(summary)
|
||||
if proc.stderr and proc.stderr.strip():
|
||||
logger.warning(f"Update process reported warnings:\n{proc.stderr.strip()}")
|
||||
return True, summary
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = f"Update failed. Pip error (code {e.returncode}):\n{e.stderr.strip()}"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
except Exception as e:
|
||||
logger.exception("An unexpected error occurred during package update.")
|
||||
return False, f"An unexpected error occurred: {e}"
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
if temp_req_path.exists():
|
||||
try:
|
||||
os.remove(temp_req_path)
|
||||
except OSError as e:
|
||||
logger.warning(f"Could not remove temporary file '{temp_req_path}': {e}")
|
||||
209
dependencyanalyzer/core/project_normalizer.py
Normal file
209
dependencyanalyzer/core/project_normalizer.py
Normal file
@ -0,0 +1,209 @@
|
||||
# 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.
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, 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(),
|
||||
}
|
||||
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).
|
||||
"""
|
||||
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
|
||||
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
|
||||
except FileNotFoundError:
|
||||
msg = f"Error: Command '{command[0]}' not found. Is Python correctly installed and in PATH?"
|
||||
logger.error(msg)
|
||||
return False, msg
|
||||
except subprocess.TimeoutExpired:
|
||||
msg = f"Error: Task '{description}' timed out after 10 minutes."
|
||||
logger.error(msg)
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
def normalize_project(project_path: Path) -> Dict[str, Tuple[bool, str]]:
|
||||
"""
|
||||
Executes the full project normalization workflow.
|
||||
|
||||
Args:
|
||||
project_path: The root path of the project to normalize.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the success status and a message for each step.
|
||||
"""
|
||||
results: Dict[str, Tuple[bool, str]] = {}
|
||||
|
||||
# --- 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.")
|
||||
else:
|
||||
success, message = _run_command(
|
||||
[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
|
||||
|
||||
# --- 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
|
||||
|
||||
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.")
|
||||
except Exception as e:
|
||||
msg = f"Failed to analyze project dependencies: {e}"
|
||||
logger.exception(msg)
|
||||
results["analysis"] = (False, msg)
|
||||
return results
|
||||
|
||||
# --- 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}'.")
|
||||
except Exception as e:
|
||||
msg = f"Failed to generate requirements.txt: {e}"
|
||||
logger.exception(msg)
|
||||
results["requirements_generation"] = (False, msg)
|
||||
return results
|
||||
|
||||
# --- 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.")
|
||||
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
|
||||
|
||||
# --- 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.")
|
||||
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"
|
||||
"""
|
||||
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)
|
||||
except IOError as e:
|
||||
msg = f"Failed to write 'pyproject.toml': {e}"
|
||||
logger.error(msg)
|
||||
results["pyproject_creation"] = (False, msg)
|
||||
|
||||
return results
|
||||
257
dependencyanalyzer/core/stdlib_detector.py
Normal file
257
dependencyanalyzer/core/stdlib_detector.py
Normal file
@ -0,0 +1,257 @@
|
||||
# dependency_analyzer/core/stdlib_detector.py
|
||||
"""
|
||||
Handles the detection of Python's standard library modules.
|
||||
|
||||
This module provides functions to determine if a module name or file path
|
||||
belongs to the Python standard library, accommodating different Python versions.
|
||||
"""
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set
|
||||
|
||||
# --- Logger Configuration ---
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Standard Library Detection Logic ---
|
||||
# Attempt to get the frozenset of stdlib modules, introduced in Python 3.10
|
||||
if sys.version_info >= (3, 10):
|
||||
try:
|
||||
# The most reliable method for modern Python versions
|
||||
STANDARD_LIBRARY_MODULES: frozenset[str] = sys.stdlib_module_names
|
||||
logger.debug(
|
||||
"Using sys.stdlib_module_names for standard library module list (Python 3.10+)."
|
||||
)
|
||||
except AttributeError:
|
||||
# Fallback in case the attribute is missing for some reason
|
||||
logger.warning(
|
||||
"sys.stdlib_module_names not found despite Python 3.10+. Using a predefined list."
|
||||
)
|
||||
STANDARD_LIBRARY_MODULES = frozenset() # Will be populated by the predefined list
|
||||
else:
|
||||
# For Python versions older than 3.10
|
||||
logger.debug(
|
||||
"Using a predefined list for standard library modules (Python < 3.10)."
|
||||
)
|
||||
STANDARD_LIBRARY_MODULES = frozenset() # Will be populated below
|
||||
|
||||
# If the modern method is unavailable or failed, use a hardcoded list.
|
||||
# This list is extensive but might not be perfectly up-to-date.
|
||||
if not STANDARD_LIBRARY_MODULES:
|
||||
_PREDEFINED_STDLIBS = {
|
||||
"abc", "aifc", "argparse", "array", "ast", "asynchat", "asyncio",
|
||||
"asyncore", "atexit", "audioop", "base64", "bdb", "binascii",
|
||||
"binhex", "bisect", "builtins", "bz2", "calendar", "cgi", "cgitb",
|
||||
"chunk", "cmath", "cmd", "code", "codecs", "codeop", "collections",
|
||||
"colorsys", "compileall", "concurrent", "configparser", "contextlib",
|
||||
"contextvars", "copy", "copyreg", "cProfile", "crypt", "csv",
|
||||
"ctypes", "curses", "dataclasses", "datetime", "dbm", "decimal",
|
||||
"difflib", "dis", "distutils", "doctest", "email", "encodings",
|
||||
"ensurepip", "enum", "errno", "faulthandler", "fcntl", "filecmp",
|
||||
"fileinput", "fnmatch", "formatter", "fractions", "ftplib",
|
||||
"functools", "gc", "getopt", "getpass", "gettext", "glob",
|
||||
"graphlib", "grp", "gzip", "hashlib", "heapq", "hmac", "html",
|
||||
"http", "idlelib", "imaplib", "imghdr", "imp", "importlib",
|
||||
"inspect", "io", "ipaddress", "itertools", "json", "keyword",
|
||||
"lib2to3", "linecache", "locale", "logging", "lzma", "mailbox",
|
||||
"mailcap", "marshal", "math", "mimetypes", "mmap", "modulefinder",
|
||||
"multiprocessing", "netrc", "nis", "nntplib", "numbers", "operator",
|
||||
"optparse", "os", "ossaudiodev", "parser", "pathlib", "pdb",
|
||||
"pickle", "pickletools", "pipes", "pkgutil", "platform", "plistlib",
|
||||
"poplib", "posix", "pprint", "profile", "pstats", "pty", "pwd",
|
||||
"py_compile", "pyclbr", "pydoc", "pydoc_data", "pyexpat", "queue",
|
||||
"quopri", "random", "re", "readline", "reprlib", "resource",
|
||||
"rlcompleter", "runpy", "sched", "secrets", "select", "selectors",
|
||||
"shelve", "shlex", "shutil", "signal", "site", "smtpd", "smtplib",
|
||||
"sndhdr", "socket", "socketserver", "spwd", "sqlite3", "ssl",
|
||||
"stat", "statistics", "string", "stringprep", "struct",
|
||||
"subprocess", "sunau", "symtable", "sys", "sysconfig", "syslog",
|
||||
"tabnanny", "tarfile", "telnetlib", "tempfile", "termios",
|
||||
"textwrap", "threading", "time", "timeit", "tkinter", "token",
|
||||
"tokenize", "trace", "traceback", "tracemalloc", "tty", "turtle",
|
||||
"turtledemo", "types", "typing", "unicodedata", "unittest",
|
||||
"urllib", "uu", "uuid", "venv", "warnings", "wave", "weakref",
|
||||
"webbrowser", "wsgiref", "xdrlib", "xml", "xmlrpc", "zipapp",
|
||||
"zipfile", "zipimport", "zlib", "_thread", "_collections_abc",
|
||||
"_json", "_datetime", "_weakrefset", "_strptime", "_socket", "_ssl",
|
||||
"_struct", "_queue", "_pickle", "_lsprof", "_heapq", "_hashlib",
|
||||
"_csv", "_bz2", "_codecs", "_bisect", "_blake2", "_asyncio", "_ast",
|
||||
"_abc",
|
||||
}
|
||||
STANDARD_LIBRARY_MODULES = frozenset(_PREDEFINED_STDLIBS)
|
||||
logger.debug("Populated standard library list from predefined set.")
|
||||
|
||||
|
||||
_CACHED_STD_LIB_PATHS: Optional[Set[str]] = None
|
||||
|
||||
|
||||
def get_standard_library_paths() -> Set[str]:
|
||||
"""
|
||||
Retrieves and caches normalized paths for the Python standard library.
|
||||
This is a fallback/supplement to the module name list.
|
||||
"""
|
||||
global _CACHED_STD_LIB_PATHS
|
||||
if _CACHED_STD_LIB_PATHS is not None:
|
||||
return _CACHED_STD_LIB_PATHS
|
||||
|
||||
paths: Set[str] = set()
|
||||
logger.debug("Determining standard library paths for the first time...")
|
||||
try:
|
||||
# Common paths from sysconfig
|
||||
for path_name in ("stdlib", "platstdlib"):
|
||||
try:
|
||||
path_val = sysconfig.get_path(path_name)
|
||||
if path_val and os.path.isdir(path_val):
|
||||
paths.add(os.path.normpath(path_val))
|
||||
logger.debug(f"Found stdlib path ({path_name}): {path_val}")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not get sysconfig path '{path_name}': {e}"
|
||||
)
|
||||
|
||||
# Path relative to the Python executable
|
||||
prefix_lib_path = os.path.normpath(
|
||||
os.path.join(
|
||||
sys.prefix,
|
||||
"lib",
|
||||
f"python{sys.version_info.major}.{sys.version_info.minor}",
|
||||
)
|
||||
)
|
||||
if os.path.isdir(prefix_lib_path):
|
||||
paths.add(prefix_lib_path)
|
||||
logger.debug(f"Found stdlib path (prefix): {prefix_lib_path}")
|
||||
|
||||
# Platform-specific paths
|
||||
if sys.platform == "win32":
|
||||
dlls_path = os.path.join(sys.prefix, "DLLs")
|
||||
if os.path.isdir(dlls_path):
|
||||
paths.add(os.path.normpath(dlls_path))
|
||||
logger.debug(f"Found stdlib path (DLLs): {dlls_path}")
|
||||
else:
|
||||
dynload_path = os.path.join(
|
||||
sys.exec_prefix,
|
||||
"lib",
|
||||
f"python{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"lib-dynload",
|
||||
)
|
||||
if os.path.isdir(dynload_path):
|
||||
paths.add(os.path.normpath(dynload_path))
|
||||
logger.debug(f"Found stdlib path (dynload): {dynload_path}")
|
||||
|
||||
# Framework paths (macOS)
|
||||
fw_prefix = sysconfig.get_config_var("PYTHONFRAMEWORKPREFIX")
|
||||
if fw_prefix and isinstance(fw_prefix, str) and os.path.isdir(fw_prefix):
|
||||
fw_path = os.path.normpath(
|
||||
os.path.join(
|
||||
fw_prefix,
|
||||
"lib",
|
||||
f"python{sys.version_info.major}.{sys.version_info.minor}",
|
||||
)
|
||||
)
|
||||
if os.path.isdir(fw_path):
|
||||
paths.add(fw_path)
|
||||
logger.debug(f"Found stdlib path (Framework): {fw_path}")
|
||||
|
||||
# Fallback if sysconfig fails, using well-known module locations
|
||||
if not paths:
|
||||
logger.warning("Sysconfig paths failed, attempting fallback using module locations.")
|
||||
try:
|
||||
paths.add(os.path.normpath(os.path.dirname(os.__file__)))
|
||||
except (AttributeError, TypeError):
|
||||
logger.error("Could not determine path for 'os' module.")
|
||||
try:
|
||||
paths.add(os.path.normpath(os.path.dirname(sysconfig.__file__)))
|
||||
except (AttributeError, TypeError):
|
||||
logger.error("Could not determine path for 'sysconfig' module.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error while determining standard library paths: {e}")
|
||||
|
||||
_CACHED_STD_LIB_PATHS = {p for p in paths if p} # Filter out any empty paths
|
||||
|
||||
if not _CACHED_STD_LIB_PATHS:
|
||||
logger.error("Failed to determine ANY standard library paths. Path-based checks will fail.")
|
||||
else:
|
||||
logger.debug(f"Final cached standard library paths: {_CACHED_STD_LIB_PATHS}")
|
||||
|
||||
return _CACHED_STD_LIB_PATHS
|
||||
|
||||
|
||||
def is_path_in_standard_library(file_path_str: Optional[str]) -> bool:
|
||||
"""
|
||||
Checks if a file path string is within any known standard library directory.
|
||||
Excludes 'site-packages' and 'dist-packages' explicitly.
|
||||
"""
|
||||
if not file_path_str:
|
||||
return False
|
||||
|
||||
std_lib_paths = get_standard_library_paths()
|
||||
if not std_lib_paths:
|
||||
return False
|
||||
|
||||
try:
|
||||
norm_file_path = os.path.normpath(os.path.abspath(file_path_str))
|
||||
|
||||
# Quick check to exclude third-party packages
|
||||
path_parts = Path(norm_file_path).parts
|
||||
if "site-packages" in path_parts or "dist-packages" in path_parts:
|
||||
return False
|
||||
|
||||
# Check if the file path starts with any of the identified stdlib paths
|
||||
for std_path in std_lib_paths:
|
||||
# Ensure the comparison is robust by checking for the directory separator
|
||||
if norm_file_path.startswith(std_path + os.sep):
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during path comparison for '{file_path_str}': {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_standard_library(module_name: str) -> bool:
|
||||
"""
|
||||
Checks if a module name belongs to the standard library using multiple strategies.
|
||||
|
||||
1. Checks against a predefined/system-provided list of names.
|
||||
2. Finds the module's specification (`find_spec`) to check its origin.
|
||||
3. If the origin is a file path, checks if it's in a standard library directory.
|
||||
"""
|
||||
# Strategy 1: Check against the reliable name list
|
||||
if module_name in STANDARD_LIBRARY_MODULES:
|
||||
logger.debug(f"'{module_name}' is in the standard library name set.")
|
||||
return True
|
||||
|
||||
# Strategy 2: Use importlib to find the module's specification
|
||||
try:
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
except (ValueError, ModuleNotFoundError, Exception) as e:
|
||||
# Catching specific errors and a general Exception is safer than a bare except.
|
||||
# ValueError can occur for invalid names, ModuleNotFoundError for non-existent parents.
|
||||
logger.debug(
|
||||
f"Could not find spec for '{module_name}' (error: {e}). Assuming it's non-standard."
|
||||
)
|
||||
return False
|
||||
|
||||
if spec is None:
|
||||
logger.debug(f"No spec found for '{module_name}'. Assuming non-standard.")
|
||||
return False
|
||||
|
||||
origin = spec.origin
|
||||
logger.debug(f"Module '{module_name}' has origin: '{origin}'")
|
||||
|
||||
# 'built-in' or 'frozen' modules are part of the standard library
|
||||
if origin in ("built-in", "frozen"):
|
||||
logger.debug(f"'{module_name}' is a '{origin}' module.")
|
||||
return True
|
||||
|
||||
# Strategy 3: Check if the module's file path is in a stdlib directory
|
||||
if is_path_in_standard_library(origin):
|
||||
logger.debug(f"Path for '{module_name}' ('{origin}') is a standard library path.")
|
||||
return True
|
||||
|
||||
logger.debug(f"'{module_name}' is not classified as standard library.")
|
||||
return False
|
||||
File diff suppressed because it is too large
Load Diff
31
requirements.txt
Normal file
31
requirements.txt
Normal file
@ -0,0 +1,31 @@
|
||||
# Requirements generated by DependencyAnalyzer for DependencyAnalyzer
|
||||
# Python Version (analysis env): 3.13.9
|
||||
|
||||
# --- Standard Library Modules Used (part of Python 3.13.9) ---
|
||||
# ast (Used in: dependencyanalyzer\core\analyzer.py)
|
||||
# importlib (Used in: dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\stdlib_detector.py)
|
||||
# json (Used in: dependencyanalyzer\core\package_manager.py)
|
||||
# logging (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, ...)
|
||||
# os (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, ...)
|
||||
# pathlib (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, ...)
|
||||
# re (Used in: dependencyanalyzer\_version.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\gui.py)
|
||||
# shutil (Used in: dependencyanalyzer\core\package_manager.py)
|
||||
# subprocess (Used in: dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py)
|
||||
# sys (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...)
|
||||
# sysconfig (Used in: dependencyanalyzer\core\stdlib_detector.py)
|
||||
# threading (Used in: dependencyanalyzer\gui.py)
|
||||
# tkinter (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\gui.py)
|
||||
# typing (Used in: dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...)
|
||||
|
||||
# --- External Dependencies (for pip install) ---
|
||||
# Found in: dependencyanalyzer\__main__.py
|
||||
# Version not detected in analysis environment.
|
||||
core
|
||||
|
||||
# Found in: dependencyanalyzer\__main__.py
|
||||
# Version not detected in analysis environment.
|
||||
gui
|
||||
|
||||
# Found in: dependencyanalyzer\core\package_manager.py
|
||||
packaging==25.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user