CD/CD consolidation

This commit is contained in:
StellaOps Bot
2025-12-26 17:32:23 +02:00
parent a866eb6277
commit c786faae84
638 changed files with 3821 additions and 181 deletions

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
stella-callgraph-python
Call graph extraction tool for Python projects using AST analysis.
"""
import argparse
import ast
import json
import os
import sys
from pathlib import Path
from typing import Any
from ast_analyzer import PythonASTAnalyzer
from framework_detect import detect_frameworks
def main() -> int:
parser = argparse.ArgumentParser(
description="Extract call graphs from Python projects"
)
parser.add_argument(
"path",
help="Path to Python project or file"
)
parser.add_argument(
"--json",
action="store_true",
help="Output formatted JSON"
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Verbose output"
)
args = parser.parse_args()
try:
result = analyze_project(Path(args.path), verbose=args.verbose)
if args.json:
print(json.dumps(result, indent=2))
else:
print(json.dumps(result))
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
def analyze_project(project_path: Path, verbose: bool = False) -> dict[str, Any]:
"""Analyze a Python project and extract its call graph."""
if not project_path.exists():
raise FileNotFoundError(f"Path not found: {project_path}")
# Find project root (look for pyproject.toml, setup.py, etc.)
root = find_project_root(project_path)
package_name = extract_package_name(root)
# Detect frameworks
frameworks = detect_frameworks(root)
# Find Python source files
source_files = find_python_files(root)
if verbose:
print(f"Found {len(source_files)} Python files", file=sys.stderr)
# Analyze all files
analyzer = PythonASTAnalyzer(package_name, root, frameworks)
for source_file in source_files:
try:
with open(source_file, 'r', encoding='utf-8') as f:
content = f.read()
tree = ast.parse(content, filename=str(source_file))
relative_path = source_file.relative_to(root)
analyzer.analyze_file(tree, str(relative_path))
except SyntaxError as e:
if verbose:
print(f"Warning: Syntax error in {source_file}: {e}", file=sys.stderr)
except Exception as e:
if verbose:
print(f"Warning: Failed to parse {source_file}: {e}", file=sys.stderr)
return analyzer.get_result()
def find_project_root(path: Path) -> Path:
"""Find the project root by looking for marker files."""
markers = ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', '.git']
current = path.resolve()
if current.is_file():
current = current.parent
while current != current.parent:
for marker in markers:
if (current / marker).exists():
return current
current = current.parent
return path.resolve() if path.is_dir() else path.parent.resolve()
def extract_package_name(root: Path) -> str:
"""Extract package name from project metadata."""
# Try pyproject.toml
pyproject = root / 'pyproject.toml'
if pyproject.exists():
try:
import tomllib
with open(pyproject, 'rb') as f:
data = tomllib.load(f)
return data.get('project', {}).get('name', root.name)
except Exception:
pass
# Try setup.py
setup_py = root / 'setup.py'
if setup_py.exists():
try:
with open(setup_py, 'r') as f:
content = f.read()
# Simple regex-based extraction
import re
match = re.search(r"name\s*=\s*['\"]([^'\"]+)['\"]", content)
if match:
return match.group(1)
except Exception:
pass
return root.name
def find_python_files(root: Path) -> list[Path]:
"""Find all Python source files in the project."""
exclude_dirs = {
'__pycache__', '.git', '.tox', '.nox', '.mypy_cache',
'.pytest_cache', 'venv', '.venv', 'env', '.env',
'node_modules', 'dist', 'build', 'eggs', '*.egg-info'
}
files = []
for path in root.rglob('*.py'):
# Skip excluded directories
skip = False
for part in path.parts:
if part in exclude_dirs or part.endswith('.egg-info'):
skip = True
break
if not skip and not path.name.startswith('.'):
files.append(path)
return sorted(files)
if __name__ == '__main__':
sys.exit(main())