""" Framework detection for Python projects. """ from pathlib import Path from typing import Any import re # Framework patterns FRAMEWORK_PATTERNS = { "flask": { "packages": ["flask"], "imports": [r"from flask import", r"import flask"], "patterns": [r"@\w+\.route\(", r"Flask\(__name__\)"], "entrypoint_type": "http_handler" }, "fastapi": { "packages": ["fastapi"], "imports": [r"from fastapi import", r"import fastapi"], "patterns": [r"@\w+\.(get|post|put|delete|patch)\(", r"FastAPI\("], "entrypoint_type": "http_handler" }, "django": { "packages": ["django"], "imports": [r"from django", r"import django"], "patterns": [r"urlpatterns\s*=", r"class \w+View\(", r"@api_view\("], "entrypoint_type": "http_handler" }, "click": { "packages": ["click"], "imports": [r"from click import", r"import click"], "patterns": [r"@click\.command\(", r"@click\.group\(", r"@\w+\.command\("], "entrypoint_type": "cli_command" }, "typer": { "packages": ["typer"], "imports": [r"from typer import", r"import typer"], "patterns": [r"typer\.Typer\(", r"@\w+\.command\("], "entrypoint_type": "cli_command" }, "celery": { "packages": ["celery"], "imports": [r"from celery import", r"import celery"], "patterns": [r"@\w+\.task\(", r"@shared_task\(", r"Celery\("], "entrypoint_type": "background_job" }, "dramatiq": { "packages": ["dramatiq"], "imports": [r"from dramatiq import", r"import dramatiq"], "patterns": [r"@dramatiq\.actor\("], "entrypoint_type": "background_job" }, "rq": { "packages": ["rq"], "imports": [r"from rq import", r"import rq"], "patterns": [r"@job\(", r"queue\.enqueue\("], "entrypoint_type": "background_job" }, "sanic": { "packages": ["sanic"], "imports": [r"from sanic import", r"import sanic"], "patterns": [r"@\w+\.route\(", r"Sanic\("], "entrypoint_type": "http_handler" }, "aiohttp": { "packages": ["aiohttp"], "imports": [r"from aiohttp import", r"import aiohttp"], "patterns": [r"web\.Application\(", r"@routes\.(get|post|put|delete)\("], "entrypoint_type": "http_handler" }, "tornado": { "packages": ["tornado"], "imports": [r"from tornado import", r"import tornado"], "patterns": [r"class \w+Handler\(", r"tornado\.web\.Application\("], "entrypoint_type": "http_handler" }, "aws_lambda": { "packages": ["aws_lambda_powertools", "boto3"], "imports": [r"def handler\(event", r"def lambda_handler\("], "patterns": [r"def handler\(event,\s*context\)", r"@logger\.inject_lambda_context"], "entrypoint_type": "lambda" }, "azure_functions": { "packages": ["azure.functions"], "imports": [r"import azure\.functions"], "patterns": [r"@func\.route\(", r"func\.HttpRequest"], "entrypoint_type": "cloud_function" }, "grpc": { "packages": ["grpcio", "grpc"], "imports": [r"import grpc", r"from grpc import"], "patterns": [r"_pb2_grpc\.add_\w+Servicer_to_server\("], "entrypoint_type": "grpc_method" }, "graphql": { "packages": ["graphene", "strawberry", "ariadne"], "imports": [r"import graphene", r"import strawberry", r"import ariadne"], "patterns": [r"@strawberry\.(type|mutation|query)\(", r"class \w+\(graphene\.ObjectType\)"], "entrypoint_type": "graphql_resolver" } } def detect_frameworks(project_root: Path) -> list[str]: """Detect frameworks used in a Python project.""" detected: set[str] = set() # Check pyproject.toml pyproject = project_root / "pyproject.toml" if pyproject.exists(): detected.update(_detect_from_pyproject(pyproject)) # Check requirements.txt requirements = project_root / "requirements.txt" if requirements.exists(): detected.update(_detect_from_requirements(requirements)) # Check setup.py setup_py = project_root / "setup.py" if setup_py.exists(): detected.update(_detect_from_setup_py(setup_py)) # Scan source files for import patterns detected.update(_detect_from_source(project_root)) return sorted(detected) def _detect_from_pyproject(path: Path) -> set[str]: """Detect frameworks from pyproject.toml.""" detected: set[str] = set() try: import tomllib with open(path, 'rb') as f: data = tomllib.load(f) # Check dependencies deps = set() deps.update(data.get("project", {}).get("dependencies", [])) deps.update(data.get("project", {}).get("optional-dependencies", {}).get("dev", [])) # Poetry format poetry = data.get("tool", {}).get("poetry", {}) deps.update(poetry.get("dependencies", {}).keys()) deps.update(poetry.get("dev-dependencies", {}).keys()) for dep in deps: # Extract package name (remove version specifier) pkg = re.split(r'[<>=!~\[]', dep)[0].strip().lower() for framework, config in FRAMEWORK_PATTERNS.items(): if pkg in config["packages"]: detected.add(framework) except Exception: pass return detected def _detect_from_requirements(path: Path) -> set[str]: """Detect frameworks from requirements.txt.""" detected: set[str] = set() try: with open(path, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue # Extract package name pkg = re.split(r'[<>=!~\[]', line)[0].strip().lower() for framework, config in FRAMEWORK_PATTERNS.items(): if pkg in config["packages"]: detected.add(framework) except Exception: pass return detected def _detect_from_setup_py(path: Path) -> set[str]: """Detect frameworks from setup.py.""" detected: set[str] = set() try: with open(path, 'r') as f: content = f.read() # Look for install_requires for framework, config in FRAMEWORK_PATTERNS.items(): for pkg in config["packages"]: if f'"{pkg}"' in content or f"'{pkg}'" in content: detected.add(framework) except Exception: pass return detected def _detect_from_source(project_root: Path) -> set[str]: """Detect frameworks by scanning Python source files.""" detected: set[str] = set() exclude_dirs = { '__pycache__', '.git', '.tox', '.nox', 'venv', '.venv', 'env', '.env', 'node_modules', 'dist', 'build' } # Only scan first few files to avoid slow startup max_files = 50 scanned = 0 for py_file in project_root.rglob('*.py'): if scanned >= max_files: break # Skip excluded directories skip = False for part in py_file.parts: if part in exclude_dirs: skip = True break if skip: continue try: with open(py_file, 'r', encoding='utf-8') as f: content = f.read(4096) # Only read first 4KB for framework, config in FRAMEWORK_PATTERNS.items(): if framework in detected: continue for pattern in config["imports"] + config["patterns"]: if re.search(pattern, content): detected.add(framework) break scanned += 1 except Exception: continue return detected def get_entrypoint_type(framework: str) -> str: """Get the entrypoint type for a framework.""" return FRAMEWORK_PATTERNS.get(framework, {}).get("entrypoint_type", "unknown")