gateway/scripts/script_analyze_function_imports.py
2026-01-23 01:10:00 +01:00

223 lines
7.7 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Analyze function-level imports to determine which could be moved to header.
Categories:
1. CIRCULAR - Import would cause circular dependency (must stay in function)
2. REDUNDANT - Same import already exists in header (can be removed)
3. MOVABLE - Could potentially be moved to header
"""
import csv
from pathlib import Path
from typing import Dict, List, Set, Tuple
from collections import defaultdict
# Paths
SCRIPT_DIR = Path(__file__).parent
INPUT_FILE = SCRIPT_DIR / "import_analysis.csv"
OUTPUT_FILE = SCRIPT_DIR / "function_imports_analysis.txt"
def _getContainer(moduleName: str) -> str:
"""Extract container name from module path."""
if moduleName == "gateway.app":
return "app"
parts = moduleName.replace("gateway.", "").split(".")
if len(parts) < 2:
return "app"
container = parts[1]
# Skip tests and scripts
if container in ("tests", "scripts") or container.startswith("script_"):
return None
if parts[0] in ("tests", "scripts"):
return None
# Handle features sub-containers
if container == "features" and len(parts) > 2:
return f"features.{parts[2]}"
return container
def _analyzeImports() -> Tuple[Dict, Dict, Dict, Set]:
"""
Analyze imports and return:
- headerImports: Dict[module] -> Set[imported_modules]
- functionImports: Dict[module] -> List[(imported_module, function_name)]
- allModuleImports: Dict[module] -> Set[all_imports] (for circular detection)
- allModules: Set of all modules
"""
headerImports = defaultdict(set)
functionImports = defaultdict(list)
allModuleImports = defaultdict(set)
allModules = set()
with open(INPUT_FILE, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
sourceFull = row["module_name"]
targetFull = row["imported_module_name"]
position = row["position"]
# Skip external imports and relative imports
if not targetFull.startswith("modules."):
continue
if targetFull.startswith("(relative)"):
continue
# Skip tests/scripts
sourceContainer = _getContainer(sourceFull)
if sourceContainer is None:
continue
# Add gateway prefix for consistency
targetFull = f"gateway.{targetFull}"
allModules.add(sourceFull)
allModules.add(targetFull)
allModuleImports[sourceFull].add(targetFull)
if position == "header":
headerImports[sourceFull].add(targetFull)
else:
# Extract function name
funcName = position.replace("function ", "")
functionImports[sourceFull].append((targetFull, funcName))
return dict(headerImports), dict(functionImports), dict(allModuleImports), allModules
def _detectCircularDependency(source: str, target: str, allModuleImports: Dict) -> bool:
"""
Check if moving target import to header would create circular dependency.
Returns True if target already imports source (directly or indirectly).
"""
visited = set()
def _canReach(current: str, goal: str) -> bool:
if current == goal:
return True
if current in visited:
return False
visited.add(current)
for imported in allModuleImports.get(current, []):
if _canReach(imported, goal):
return True
return False
# Check if target can reach source through its imports
return _canReach(target, source)
def main():
"""Main analysis function."""
print("Analyzing function imports...")
headerImports, functionImports, allModuleImports, allModules = _analyzeImports()
# Categorize function imports
circular = [] # Must stay in function (would cause circular import)
redundant = [] # Already imported in header (can be removed)
movable = [] # Could be moved to header
totalFunctionImports = 0
for source, imports in sorted(functionImports.items()):
headerSet = headerImports.get(source, set())
for target, funcName in imports:
totalFunctionImports += 1
# Check if already in header
if target in headerSet:
redundant.append((source, target, funcName))
continue
# Check for circular dependency
if _detectCircularDependency(source, target, allModuleImports):
circular.append((source, target, funcName))
continue
# Otherwise, could be moved to header
movable.append((source, target, funcName))
# Generate report
lines = []
lines.append("=" * 80)
lines.append("FUNCTION IMPORTS ANALYSIS")
lines.append("=" * 80)
lines.append(f"\nTotal function imports (internal modules): {totalFunctionImports}")
lines.append(f" - CIRCULAR (must stay): {len(circular):4}")
lines.append(f" - REDUNDANT (can remove): {len(redundant):4}")
lines.append(f" - MOVABLE (can move): {len(movable):4}")
# Group movable by source module
movableBySource = defaultdict(list)
for source, target, funcName in movable:
movableBySource[source].append((target, funcName))
lines.append(f"\n\n{'=' * 80}")
lines.append("MOVABLE TO HEADER (grouped by source module)")
lines.append("These imports could potentially be moved to the module header.")
lines.append("=" * 80)
for source in sorted(movableBySource.keys()):
imports = movableBySource[source]
lines.append(f"\n{source}")
lines.append("-" * len(source))
for target, funcName in sorted(set(imports)):
shortTarget = target.replace("gateway.", "")
lines.append(f" [{funcName}] {shortTarget}")
# Redundant imports
if redundant:
lines.append(f"\n\n{'=' * 80}")
lines.append("REDUNDANT IMPORTS (already in header - can be removed)")
lines.append("=" * 80)
redundantBySource = defaultdict(list)
for source, target, funcName in redundant:
redundantBySource[source].append((target, funcName))
for source in sorted(redundantBySource.keys()):
imports = redundantBySource[source]
lines.append(f"\n{source}")
lines.append("-" * len(source))
for target, funcName in sorted(set(imports)):
shortTarget = target.replace("gateway.", "")
lines.append(f" [{funcName}] {shortTarget}")
# Circular imports (for reference)
if circular:
lines.append(f"\n\n{'=' * 80}")
lines.append("CIRCULAR DEPENDENCY (must stay in function)")
lines.append("=" * 80)
circularBySource = defaultdict(list)
for source, target, funcName in circular:
circularBySource[source].append((target, funcName))
for source in sorted(circularBySource.keys()):
imports = circularBySource[source]
lines.append(f"\n{source}")
lines.append("-" * len(source))
for target, funcName in sorted(set(imports)):
shortTarget = target.replace("gateway.", "")
lines.append(f" [{funcName}] {shortTarget}")
# Write report
report = "\n".join(lines)
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
f.write(report)
print(report)
print(f"\n\nReport saved to: {OUTPUT_FILE}")
if __name__ == "__main__":
main()