runtime-deps 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runtime_deps-0.1.0/PKG-INFO +25 -0
- runtime_deps-0.1.0/README.md +17 -0
- runtime_deps-0.1.0/auto_req/__init__.py +0 -0
- runtime_deps-0.1.0/auto_req/analyzer.py +47 -0
- runtime_deps-0.1.0/auto_req/cli.py +134 -0
- runtime_deps-0.1.0/auto_req/installer.py +548 -0
- runtime_deps-0.1.0/auto_req/mapper.py +166 -0
- runtime_deps-0.1.0/auto_req/pytest_plugin.py +33 -0
- runtime_deps-0.1.0/pyproject.toml +18 -0
- runtime_deps-0.1.0/runtime_deps.egg-info/PKG-INFO +25 -0
- runtime_deps-0.1.0/runtime_deps.egg-info/SOURCES.txt +13 -0
- runtime_deps-0.1.0/runtime_deps.egg-info/dependency_links.txt +1 -0
- runtime_deps-0.1.0/runtime_deps.egg-info/entry_points.txt +3 -0
- runtime_deps-0.1.0/runtime_deps.egg-info/top_level.txt +1 -0
- runtime_deps-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runtime-deps
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AST-driven dynamic dependency resolution and runtime provisioning engine.
|
|
5
|
+
Author: Pavan Kumar Korrapati
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
TraceReq source project
|
|
10
|
+
│
|
|
11
|
+
│ python -m build
|
|
12
|
+
▼
|
|
13
|
+
dist/
|
|
14
|
+
├── auto_req-0.1.0-py3-none-any.whl ← reusable package
|
|
15
|
+
└── auto_req-0.1.0.tar.gz
|
|
16
|
+
│
|
|
17
|
+
│ pip install
|
|
18
|
+
▼
|
|
19
|
+
Any Python project
|
|
20
|
+
├── .venv/
|
|
21
|
+
├── tests/
|
|
22
|
+
├── src/
|
|
23
|
+
└── ...
|
|
24
|
+
│
|
|
25
|
+
└── auto-req command
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
TraceReq source project
|
|
2
|
+
│
|
|
3
|
+
│ python -m build
|
|
4
|
+
▼
|
|
5
|
+
dist/
|
|
6
|
+
├── auto_req-0.1.0-py3-none-any.whl ← reusable package
|
|
7
|
+
└── auto_req-0.1.0.tar.gz
|
|
8
|
+
│
|
|
9
|
+
│ pip install
|
|
10
|
+
▼
|
|
11
|
+
Any Python project
|
|
12
|
+
├── .venv/
|
|
13
|
+
├── tests/
|
|
14
|
+
├── src/
|
|
15
|
+
└── ...
|
|
16
|
+
│
|
|
17
|
+
└── auto-req command
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CodeAnalyzer:
|
|
8
|
+
"""Analyzes Python code to extract top-level third-party imported modules."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
# Retrieve all standard library modules for the current Python runtime
|
|
12
|
+
self.stdlib_modules: Set[str] = set(sys.stdlib_module_names)
|
|
13
|
+
|
|
14
|
+
def extract_imports_from_file(self, file_path: Path) -> Set[str]:
|
|
15
|
+
"""Parses a single .py file and returns non-stdlib top-level import names."""
|
|
16
|
+
if not file_path.is_file() or file_path.suffix != ".py":
|
|
17
|
+
return set()
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
21
|
+
tree = ast.parse(f.read(), filename=str(file_path))
|
|
22
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
23
|
+
return set()
|
|
24
|
+
|
|
25
|
+
modules = set()
|
|
26
|
+
for node in ast.walk(tree):
|
|
27
|
+
if isinstance(node, ast.Import):
|
|
28
|
+
for alias in node.names:
|
|
29
|
+
# Capture top-level package (e.g., 'os.path' -> 'os')
|
|
30
|
+
modules.add(alias.name.split(".")[0])
|
|
31
|
+
elif isinstance(node, ast.ImportFrom):
|
|
32
|
+
# Ensure it's an absolute import (level == 0)
|
|
33
|
+
if node.module and node.level == 0:
|
|
34
|
+
modules.add(node.module.split(".")[0])
|
|
35
|
+
|
|
36
|
+
# Exclude standard library modules
|
|
37
|
+
return modules - self.stdlib_modules
|
|
38
|
+
|
|
39
|
+
def scan_directory(self, target_dir: Path) -> Set[str]:
|
|
40
|
+
"""Recursively scans a directory for Python files and aggregates imports."""
|
|
41
|
+
all_imports = set()
|
|
42
|
+
for py_file in target_dir.rglob("*.py"):
|
|
43
|
+
# Ignore virtual environment directories
|
|
44
|
+
if any(part.startswith((".", "venv", "env")) for part in py_file.parts):
|
|
45
|
+
continue
|
|
46
|
+
all_imports.update(self.extract_imports_from_file(py_file))
|
|
47
|
+
return all_imports
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# import argparse
|
|
2
|
+
# import subprocess
|
|
3
|
+
# import sys
|
|
4
|
+
# from pathlib import Path
|
|
5
|
+
# from auto_req.analyzer import CodeAnalyzer
|
|
6
|
+
# from auto_req.installer import PackageInstaller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# def main():
|
|
10
|
+
# parser = argparse.ArgumentParser(
|
|
11
|
+
# description="TraceReq: Auto-detect and install missing Python requirements by analyzing code."
|
|
12
|
+
# )
|
|
13
|
+
# parser.add_argument(
|
|
14
|
+
# "target",
|
|
15
|
+
# nargs="?",
|
|
16
|
+
# default=".",
|
|
17
|
+
# help="Target directory or Python file to analyze (default: current directory)",
|
|
18
|
+
# )
|
|
19
|
+
# args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
# target_path = Path(args.target).resolve()
|
|
22
|
+
# analyzer = CodeAnalyzer()
|
|
23
|
+
|
|
24
|
+
# if target_path.is_file():
|
|
25
|
+
# required_imports = analyzer.extract_imports_from_file(target_path)
|
|
26
|
+
# elif target_path.is_dir():
|
|
27
|
+
# required_imports = analyzer.scan_directory(target_path)
|
|
28
|
+
# else:
|
|
29
|
+
# print(f"Error: Target path '{target_path}' does not exist.")
|
|
30
|
+
# sys.exit(1)
|
|
31
|
+
|
|
32
|
+
# installer = PackageInstaller(project_root=target_path.parent if target_path.is_file() else target_path)
|
|
33
|
+
# missing = installer.resolve_missing(required_imports)
|
|
34
|
+
|
|
35
|
+
# if not missing:
|
|
36
|
+
# print("[TraceReq] All required dependencies are already installed.")
|
|
37
|
+
# else:
|
|
38
|
+
# success = installer.install_packages(missing)
|
|
39
|
+
# if not success:
|
|
40
|
+
# sys.exit(1)
|
|
41
|
+
|
|
42
|
+
# # Automatically execute the file if target is a single Python script
|
|
43
|
+
# if target_path.is_file():
|
|
44
|
+
# python_exec = installer.get_target_python()
|
|
45
|
+
# print(f"[TraceReq] Running {target_path.name} using {python_exec.name}...\n")
|
|
46
|
+
|
|
47
|
+
# try:
|
|
48
|
+
# subprocess.run([str(python_exec), str(target_path)], check=True)
|
|
49
|
+
# except subprocess.CalledProcessError as e:
|
|
50
|
+
# print(f"\n[TraceReq] Execution failed with exit code {e.returncode}")
|
|
51
|
+
# sys.exit(e.returncode)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# if __name__ == "__main__":
|
|
55
|
+
# main()
|
|
56
|
+
import argparse
|
|
57
|
+
import subprocess
|
|
58
|
+
import sys
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from auto_req.analyzer import CodeAnalyzer
|
|
61
|
+
from auto_req.installer import PackageInstaller
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
description="TraceReq: Auto-detect and install missing Python requirements by analyzing code."
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"target",
|
|
70
|
+
nargs="?",
|
|
71
|
+
default=".",
|
|
72
|
+
help="Target directory or Python file to analyze (default: current directory)",
|
|
73
|
+
)
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
target_path = Path(args.target).resolve()
|
|
77
|
+
analyzer = CodeAnalyzer()
|
|
78
|
+
|
|
79
|
+
if target_path.is_file():
|
|
80
|
+
required_imports = analyzer.extract_imports_from_file(target_path)
|
|
81
|
+
elif target_path.is_dir():
|
|
82
|
+
required_imports = analyzer.scan_directory(target_path)
|
|
83
|
+
else:
|
|
84
|
+
print(f"Error: Target path '{target_path}' does not exist.")
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
installer = PackageInstaller(
|
|
88
|
+
project_root=target_path.parent if target_path.is_file() else target_path
|
|
89
|
+
)
|
|
90
|
+
missing = installer.resolve_missing(required_imports)
|
|
91
|
+
|
|
92
|
+
if not missing:
|
|
93
|
+
print("[TraceReq] All required dependencies are already installed.")
|
|
94
|
+
else:
|
|
95
|
+
success = installer.install_packages(missing)
|
|
96
|
+
if not success:
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
# Automatically execute the file if target is a single Python script
|
|
100
|
+
if target_path.is_file():
|
|
101
|
+
python_exec = installer.get_target_python()
|
|
102
|
+
print(f"[TraceReq] Running {target_path.name} using {python_exec.name}...\n")
|
|
103
|
+
|
|
104
|
+
# Step 1: Run execution and capture output to intercept runtime errors
|
|
105
|
+
process = subprocess.run(
|
|
106
|
+
[str(python_exec), str(target_path)],
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Print standard output cleanly
|
|
112
|
+
if process.stdout:
|
|
113
|
+
print(process.stdout, end="")
|
|
114
|
+
|
|
115
|
+
# Step 2: Intercept execution failures & trigger auto-remediation
|
|
116
|
+
if process.returncode != 0:
|
|
117
|
+
error_output = (process.stderr or "") + (process.stdout or "")
|
|
118
|
+
if process.stderr:
|
|
119
|
+
print(process.stderr, file=sys.stderr, end="")
|
|
120
|
+
|
|
121
|
+
# Attempt self-healing repair via post-execution error rules
|
|
122
|
+
remediated = installer.attempt_error_remediation(error_output)
|
|
123
|
+
|
|
124
|
+
if remediated:
|
|
125
|
+
print(f"[TraceReq] Retrying execution for {target_path.name}...\n")
|
|
126
|
+
retry_process = subprocess.run([str(python_exec), str(target_path)])
|
|
127
|
+
sys.exit(retry_process.returncode)
|
|
128
|
+
else:
|
|
129
|
+
print(f"\n[TraceReq] Execution failed with exit code {process.returncode}")
|
|
130
|
+
sys.exit(process.returncode)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# import importlib.metadata
|
|
2
|
+
# import os
|
|
3
|
+
# import subprocess
|
|
4
|
+
# import sys
|
|
5
|
+
# from pathlib import Path
|
|
6
|
+
# from typing import List, Optional, Set
|
|
7
|
+
# from auto_req.mapper import PyPIMapper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# class PackageInstaller:
|
|
11
|
+
# """Detects missing dependencies and installs them into a project's local virtual environment."""
|
|
12
|
+
|
|
13
|
+
# COMMON_VENV_NAMES = [".venv", "venv", "env", ".env", "virtualenv"]
|
|
14
|
+
|
|
15
|
+
# def __init__(self, project_root: Optional[Path] = None):
|
|
16
|
+
# self.mapper = PyPIMapper()
|
|
17
|
+
# self.stdlib_modules = set(sys.stdlib_module_names)
|
|
18
|
+
# self.project_root = Path(project_root or Path.cwd()).resolve()
|
|
19
|
+
# self.venv_path = self._detect_virtual_env()
|
|
20
|
+
|
|
21
|
+
# def _detect_virtual_env(self) -> Optional[Path]:
|
|
22
|
+
# """Dynamically locates any virtual environment directory inside or around the project root."""
|
|
23
|
+
# # 1. Check if VIRTUAL_ENV variable is set in the environment
|
|
24
|
+
# if "VIRTUAL_ENV" in os.environ:
|
|
25
|
+
# active_env = Path(os.environ["VIRTUAL_ENV"])
|
|
26
|
+
# if self._is_valid_venv(active_env):
|
|
27
|
+
# return active_env
|
|
28
|
+
|
|
29
|
+
# # 2. Check if the currently running Python runtime itself is a venv
|
|
30
|
+
# if sys.prefix != sys.base_prefix:
|
|
31
|
+
# active_sys_venv = Path(sys.prefix)
|
|
32
|
+
# if self._is_valid_venv(active_sys_venv):
|
|
33
|
+
# return active_sys_venv
|
|
34
|
+
|
|
35
|
+
# # 3. Check common venv folder names inside project_root
|
|
36
|
+
# for name in self.COMMON_VENV_NAMES:
|
|
37
|
+
# candidate = self.project_root / name
|
|
38
|
+
# if self._is_valid_venv(candidate):
|
|
39
|
+
# return candidate
|
|
40
|
+
|
|
41
|
+
# # 4. Check parent directory if project_root is inside a nested package
|
|
42
|
+
# for name in self.COMMON_VENV_NAMES:
|
|
43
|
+
# candidate = self.project_root.parent / name
|
|
44
|
+
# if self._is_valid_venv(candidate):
|
|
45
|
+
# return candidate
|
|
46
|
+
|
|
47
|
+
# # 5. Fallback: Scan root directory for any subfolder containing a valid python executable
|
|
48
|
+
# try:
|
|
49
|
+
# for item in self.project_root.iterdir():
|
|
50
|
+
# if item.is_dir() and item.name not in self.COMMON_VENV_NAMES:
|
|
51
|
+
# if self._is_valid_venv(item):
|
|
52
|
+
# return item
|
|
53
|
+
# except PermissionError:
|
|
54
|
+
# pass
|
|
55
|
+
|
|
56
|
+
# return None
|
|
57
|
+
|
|
58
|
+
# def _is_valid_venv(self, path: Path) -> bool:
|
|
59
|
+
# """Checks if a directory contains a valid virtual environment Python binary."""
|
|
60
|
+
# if os.name == "nt": # Windows
|
|
61
|
+
# py_exec = path / "Scripts" / "python.exe"
|
|
62
|
+
# else: # macOS / Linux
|
|
63
|
+
# py_exec = path / "bin" / "python"
|
|
64
|
+
# return py_exec.exists()
|
|
65
|
+
|
|
66
|
+
# def get_target_python(self) -> Path:
|
|
67
|
+
# """Returns the Python binary path of the detected venv, or system Python fallback."""
|
|
68
|
+
# if self.venv_path:
|
|
69
|
+
# if os.name == "nt":
|
|
70
|
+
# py_exec = self.venv_path / "Scripts" / "python.exe"
|
|
71
|
+
# else:
|
|
72
|
+
# py_exec = self.venv_path / "bin" / "python"
|
|
73
|
+
|
|
74
|
+
# if py_exec.exists():
|
|
75
|
+
# return py_exec
|
|
76
|
+
|
|
77
|
+
# return Path(sys.executable)
|
|
78
|
+
|
|
79
|
+
# def get_installed_packages(self) -> Set[str]:
|
|
80
|
+
# """Returns normalized names of installed packages in the target venv/environment."""
|
|
81
|
+
# search_paths = []
|
|
82
|
+
|
|
83
|
+
# if self.venv_path:
|
|
84
|
+
# if os.name == "nt":
|
|
85
|
+
# venv_site = self.venv_path / "Lib" / "site-packages"
|
|
86
|
+
# else:
|
|
87
|
+
# venv_site = (
|
|
88
|
+
# self.venv_path
|
|
89
|
+
# / "lib"
|
|
90
|
+
# / f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
91
|
+
# / "site-packages"
|
|
92
|
+
# )
|
|
93
|
+
# if venv_site.exists():
|
|
94
|
+
# search_paths = [str(venv_site)]
|
|
95
|
+
|
|
96
|
+
# installed = set()
|
|
97
|
+
|
|
98
|
+
# # Query site-packages of the target venv directly if present
|
|
99
|
+
# dists = (
|
|
100
|
+
# importlib.metadata.distributions(path=search_paths)
|
|
101
|
+
# if search_paths
|
|
102
|
+
# else importlib.metadata.distributions()
|
|
103
|
+
# )
|
|
104
|
+
|
|
105
|
+
# for dist in dists:
|
|
106
|
+
# name = dist.metadata.get("Name")
|
|
107
|
+
# if name:
|
|
108
|
+
# installed.add(name.lower().replace("_", "-"))
|
|
109
|
+
# return installed
|
|
110
|
+
|
|
111
|
+
# def resolve_missing(self, required_imports: Set[str]) -> List[str]:
|
|
112
|
+
# """Identifies imports that are not installed in the target environment."""
|
|
113
|
+
# installed = self.get_installed_packages()
|
|
114
|
+
# missing_pypi_packages = []
|
|
115
|
+
|
|
116
|
+
# # Strip standard library imports first
|
|
117
|
+
# third_party_imports = required_imports - self.stdlib_modules
|
|
118
|
+
|
|
119
|
+
# for imp in third_party_imports:
|
|
120
|
+
# pypi_name = self.mapper.map_import_to_pypi(imp)
|
|
121
|
+
# normalized_pypi = pypi_name.lower().replace("_", "-")
|
|
122
|
+
|
|
123
|
+
# if normalized_pypi not in installed:
|
|
124
|
+
# missing_pypi_packages.append(pypi_name)
|
|
125
|
+
|
|
126
|
+
# return missing_pypi_packages
|
|
127
|
+
|
|
128
|
+
# def install_packages(self, packages: List[str]) -> bool:
|
|
129
|
+
# """Executes pip install targeting the detected virtual environment."""
|
|
130
|
+
# if not packages:
|
|
131
|
+
# return True
|
|
132
|
+
|
|
133
|
+
# python_exec = self.get_target_python()
|
|
134
|
+
|
|
135
|
+
# if self.venv_path:
|
|
136
|
+
# print(f"\n[TraceReq] Target Virtual Environment Found: {self.venv_path.name}")
|
|
137
|
+
# else:
|
|
138
|
+
# print("\n[TraceReq] No local venv found. Target: Global Environment")
|
|
139
|
+
|
|
140
|
+
# print(f"[TraceReq] Target Python: {python_exec}")
|
|
141
|
+
# print(f"[TraceReq] Installing dependencies: {', '.join(packages)}...")
|
|
142
|
+
|
|
143
|
+
# cmd = [str(python_exec), "-m", "pip", "install", *packages]
|
|
144
|
+
# result = subprocess.run(cmd)
|
|
145
|
+
|
|
146
|
+
# if result.returncode == 0:
|
|
147
|
+
# print("[TraceReq] Installation completed successfully!\n")
|
|
148
|
+
# return True
|
|
149
|
+
|
|
150
|
+
# print("[TraceReq] Error occurred during pip installation.\n")
|
|
151
|
+
# return False
|
|
152
|
+
|
|
153
|
+
# import importlib.metadata
|
|
154
|
+
# import os
|
|
155
|
+
# import subprocess
|
|
156
|
+
# import sys
|
|
157
|
+
# from pathlib import Path
|
|
158
|
+
# from typing import List, Optional, Set
|
|
159
|
+
# from auto_req.mapper import PyPIMapper
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# class PackageInstaller:
|
|
163
|
+
# """Detects missing dependencies and installs them into a project's local virtual environment."""
|
|
164
|
+
|
|
165
|
+
# COMMON_VENV_NAMES = [".venv", "venv", "env", ".env", "virtualenv"]
|
|
166
|
+
|
|
167
|
+
# # Special packages that require additional setup commands after pip install
|
|
168
|
+
# POST_INSTALL_HOOKS = {
|
|
169
|
+
# "playwright": ["-m", "playwright", "install"]
|
|
170
|
+
# }
|
|
171
|
+
|
|
172
|
+
# def __init__(self, project_root: Optional[Path] = None):
|
|
173
|
+
# self.mapper = PyPIMapper()
|
|
174
|
+
# self.stdlib_modules = set(sys.stdlib_module_names)
|
|
175
|
+
# self.project_root = Path(project_root or Path.cwd()).resolve()
|
|
176
|
+
# self.venv_path = self._detect_virtual_env()
|
|
177
|
+
|
|
178
|
+
# def _detect_virtual_env(self) -> Optional[Path]:
|
|
179
|
+
# """Dynamically locates any virtual environment directory inside or around the project root."""
|
|
180
|
+
# if "VIRTUAL_ENV" in os.environ:
|
|
181
|
+
# active_env = Path(os.environ["VIRTUAL_ENV"])
|
|
182
|
+
# if self._is_valid_venv(active_env):
|
|
183
|
+
# return active_env
|
|
184
|
+
|
|
185
|
+
# if sys.prefix != sys.base_prefix:
|
|
186
|
+
# active_sys_venv = Path(sys.prefix)
|
|
187
|
+
# if self._is_valid_venv(active_sys_venv):
|
|
188
|
+
# return active_sys_venv
|
|
189
|
+
|
|
190
|
+
# for name in self.COMMON_VENV_NAMES:
|
|
191
|
+
# candidate = self.project_root / name
|
|
192
|
+
# if self._is_valid_venv(candidate):
|
|
193
|
+
# return candidate
|
|
194
|
+
|
|
195
|
+
# for name in self.COMMON_VENV_NAMES:
|
|
196
|
+
# candidate = self.project_root.parent / name
|
|
197
|
+
# if self._is_valid_venv(candidate):
|
|
198
|
+
# return candidate
|
|
199
|
+
|
|
200
|
+
# try:
|
|
201
|
+
# for item in self.project_root.iterdir():
|
|
202
|
+
# if item.is_dir() and item.name not in self.COMMON_VENV_NAMES:
|
|
203
|
+
# if self._is_valid_venv(item):
|
|
204
|
+
# return item
|
|
205
|
+
# except PermissionError:
|
|
206
|
+
# pass
|
|
207
|
+
|
|
208
|
+
# return None
|
|
209
|
+
|
|
210
|
+
# def _is_valid_venv(self, path: Path) -> bool:
|
|
211
|
+
# """Checks if a directory contains a valid virtual environment Python binary."""
|
|
212
|
+
# if os.name == "nt":
|
|
213
|
+
# py_exec = path / "Scripts" / "python.exe"
|
|
214
|
+
# else:
|
|
215
|
+
# py_exec = path / "bin" / "python"
|
|
216
|
+
# return py_exec.exists()
|
|
217
|
+
|
|
218
|
+
# def get_target_python(self) -> Path:
|
|
219
|
+
# """Returns the Python binary path of the detected venv, or system Python fallback."""
|
|
220
|
+
# if self.venv_path:
|
|
221
|
+
# if os.name == "nt":
|
|
222
|
+
# py_exec = self.venv_path / "Scripts" / "python.exe"
|
|
223
|
+
# else:
|
|
224
|
+
# py_exec = self.venv_path / "bin" / "python"
|
|
225
|
+
|
|
226
|
+
# if py_exec.exists():
|
|
227
|
+
# return py_exec
|
|
228
|
+
|
|
229
|
+
# return Path(sys.executable)
|
|
230
|
+
|
|
231
|
+
# def get_installed_packages(self) -> Set[str]:
|
|
232
|
+
# """Returns normalized names of installed packages in the target venv/environment."""
|
|
233
|
+
# search_paths = []
|
|
234
|
+
|
|
235
|
+
# if self.venv_path:
|
|
236
|
+
# if os.name == "nt":
|
|
237
|
+
# venv_site = self.venv_path / "Lib" / "site-packages"
|
|
238
|
+
# else:
|
|
239
|
+
# venv_site = (
|
|
240
|
+
# self.venv_path
|
|
241
|
+
# / "lib"
|
|
242
|
+
# / f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
243
|
+
# / "site-packages"
|
|
244
|
+
# )
|
|
245
|
+
# if venv_site.exists():
|
|
246
|
+
# search_paths = [str(venv_site)]
|
|
247
|
+
|
|
248
|
+
# installed = set()
|
|
249
|
+
# dists = (
|
|
250
|
+
# importlib.metadata.distributions(path=search_paths)
|
|
251
|
+
# if search_paths
|
|
252
|
+
# else importlib.metadata.distributions()
|
|
253
|
+
# )
|
|
254
|
+
|
|
255
|
+
# for dist in dists:
|
|
256
|
+
# name = dist.metadata.get("Name")
|
|
257
|
+
# if name:
|
|
258
|
+
# installed.add(name.lower().replace("_", "-"))
|
|
259
|
+
# return installed
|
|
260
|
+
|
|
261
|
+
# def resolve_missing(self, required_imports: Set[str]) -> List[str]:
|
|
262
|
+
# """Identifies imports that are not installed in the target environment."""
|
|
263
|
+
# installed = self.get_installed_packages()
|
|
264
|
+
# missing_pypi_packages = []
|
|
265
|
+
|
|
266
|
+
# third_party_imports = required_imports - self.stdlib_modules
|
|
267
|
+
|
|
268
|
+
# for imp in third_party_imports:
|
|
269
|
+
# pypi_name = self.mapper.map_import_to_pypi(imp)
|
|
270
|
+
# normalized_pypi = pypi_name.lower().replace("_", "-")
|
|
271
|
+
|
|
272
|
+
# if normalized_pypi not in installed:
|
|
273
|
+
# missing_pypi_packages.append(pypi_name)
|
|
274
|
+
|
|
275
|
+
# return missing_pypi_packages
|
|
276
|
+
|
|
277
|
+
# def run_post_install_hooks(self, installed_packages: List[str], python_exec: Path):
|
|
278
|
+
# """Runs additional commands required by specific packages after pip install."""
|
|
279
|
+
# for pkg in installed_packages:
|
|
280
|
+
# normalized_pkg = pkg.lower().replace("_", "-")
|
|
281
|
+
# if normalized_pkg in self.POST_INSTALL_HOOKS:
|
|
282
|
+
# hook_args = self.POST_INSTALL_HOOKS[normalized_pkg]
|
|
283
|
+
# cmd = [str(python_exec), *hook_args]
|
|
284
|
+
# print(f"[TraceReq] Running post-installation setup for '{pkg}': {' '.join(cmd)}")
|
|
285
|
+
# subprocess.run(cmd, check=False)
|
|
286
|
+
|
|
287
|
+
# def install_packages(self, packages: List[str]) -> bool:
|
|
288
|
+
# """Executes pip install targeting the detected virtual environment and triggers post-install hooks."""
|
|
289
|
+
# if not packages:
|
|
290
|
+
# return True
|
|
291
|
+
|
|
292
|
+
# python_exec = self.get_target_python()
|
|
293
|
+
|
|
294
|
+
# if self.venv_path:
|
|
295
|
+
# print(f"\n[TraceReq] Target Virtual Environment Found: {self.venv_path.name}")
|
|
296
|
+
# else:
|
|
297
|
+
# print("\n[TraceReq] No local venv found. Target: Global Environment")
|
|
298
|
+
|
|
299
|
+
# print(f"[TraceReq] Target Python: {python_exec}")
|
|
300
|
+
# print(f"[TraceReq] Installing dependencies: {', '.join(packages)}...")
|
|
301
|
+
|
|
302
|
+
# cmd = [str(python_exec), "-m", "pip", "install", *packages]
|
|
303
|
+
# result = subprocess.run(cmd)
|
|
304
|
+
|
|
305
|
+
# if result.returncode == 0:
|
|
306
|
+
# print("[TraceReq] Installation completed successfully!")
|
|
307
|
+
# # Run post-install hooks (e.g., downloading Playwright browser binaries)
|
|
308
|
+
# self.run_post_install_hooks(packages, python_exec)
|
|
309
|
+
# print()
|
|
310
|
+
# return True
|
|
311
|
+
|
|
312
|
+
# print("[TraceReq] Error occurred during pip installation.\n")
|
|
313
|
+
# return False
|
|
314
|
+
|
|
315
|
+
import importlib.metadata
|
|
316
|
+
import os
|
|
317
|
+
import re
|
|
318
|
+
import subprocess
|
|
319
|
+
import sys
|
|
320
|
+
from pathlib import Path
|
|
321
|
+
from typing import List, Optional, Set
|
|
322
|
+
from auto_req.mapper import PyPIMapper
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class PackageInstaller:
|
|
326
|
+
"""Detects missing dependencies and installs them into a project's local virtual environment."""
|
|
327
|
+
|
|
328
|
+
COMMON_VENV_NAMES = [".venv", "venv", "env", ".env", "virtualenv"]
|
|
329
|
+
|
|
330
|
+
# Special packages requiring additional commands after initial pip install
|
|
331
|
+
POST_INSTALL_HOOKS = {
|
|
332
|
+
"playwright": ["-m", "playwright", "install","chromium"]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Regex patterns mapped to repair actions when execution fails
|
|
336
|
+
ERROR_REMEDIATION_RULES = [
|
|
337
|
+
{
|
|
338
|
+
"pattern": r"Executable doesn't exist at .*ms-playwright",
|
|
339
|
+
"description": "Missing Playwright browser binaries",
|
|
340
|
+
"args": ["-m", "playwright", "install"],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"pattern": r"playwright install",
|
|
344
|
+
"description": "Playwright post-install requirement detected",
|
|
345
|
+
"args": ["-m", "playwright", "install"],
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"pattern": r"spacy\.util\.load_model.*Can't find model",
|
|
349
|
+
"description": "Missing spaCy language model",
|
|
350
|
+
"args": ["-m", "spacy", "download", "en_core_web_sm"],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"pattern": r"Resource .* not found\. Please use the NLTK Downloader",
|
|
354
|
+
"description": "Missing NLTK data package",
|
|
355
|
+
"args": ["-c", "import nltk; nltk.download('all')"],
|
|
356
|
+
},
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
360
|
+
self.mapper = PyPIMapper()
|
|
361
|
+
self.stdlib_modules = set(sys.stdlib_module_names)
|
|
362
|
+
self.project_root = Path(project_root or Path.cwd()).resolve()
|
|
363
|
+
self.venv_path = self._detect_virtual_env()
|
|
364
|
+
|
|
365
|
+
def _detect_virtual_env(self) -> Optional[Path]:
|
|
366
|
+
"""Dynamically locates any virtual environment directory inside or around the project root."""
|
|
367
|
+
if "VIRTUAL_ENV" in os.environ:
|
|
368
|
+
active_env = Path(os.environ["VIRTUAL_ENV"])
|
|
369
|
+
if self._is_valid_venv(active_env):
|
|
370
|
+
return active_env
|
|
371
|
+
|
|
372
|
+
if sys.prefix != sys.base_prefix:
|
|
373
|
+
active_sys_venv = Path(sys.prefix)
|
|
374
|
+
if self._is_valid_venv(active_sys_venv):
|
|
375
|
+
return active_sys_venv
|
|
376
|
+
|
|
377
|
+
for name in self.COMMON_VENV_NAMES:
|
|
378
|
+
candidate = self.project_root / name
|
|
379
|
+
if self._is_valid_venv(candidate):
|
|
380
|
+
return candidate
|
|
381
|
+
|
|
382
|
+
for name in self.COMMON_VENV_NAMES:
|
|
383
|
+
candidate = self.project_root.parent / name
|
|
384
|
+
if self._is_valid_venv(candidate):
|
|
385
|
+
return candidate
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
for item in self.project_root.iterdir():
|
|
389
|
+
if item.is_dir() and item.name not in self.COMMON_VENV_NAMES:
|
|
390
|
+
if self._is_valid_venv(item):
|
|
391
|
+
return item
|
|
392
|
+
except PermissionError:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def _is_valid_venv(self, path: Path) -> bool:
|
|
398
|
+
"""Checks if a directory contains a valid virtual environment Python binary."""
|
|
399
|
+
if os.name == "nt": # Windows
|
|
400
|
+
py_exec = path / "Scripts" / "python.exe"
|
|
401
|
+
else: # macOS / Linux
|
|
402
|
+
py_exec = path / "bin" / "python"
|
|
403
|
+
return py_exec.exists()
|
|
404
|
+
|
|
405
|
+
def get_target_python(self) -> Path:
|
|
406
|
+
"""Returns the Python binary path of the detected venv, or system Python fallback."""
|
|
407
|
+
if self.venv_path:
|
|
408
|
+
if os.name == "nt":
|
|
409
|
+
py_exec = self.venv_path / "Scripts" / "python.exe"
|
|
410
|
+
else:
|
|
411
|
+
py_exec = self.venv_path / "bin" / "python"
|
|
412
|
+
|
|
413
|
+
if py_exec.exists():
|
|
414
|
+
return py_exec
|
|
415
|
+
|
|
416
|
+
return Path(sys.executable)
|
|
417
|
+
|
|
418
|
+
def get_installed_packages(self) -> Set[str]:
|
|
419
|
+
"""Returns normalized names of installed packages in the target venv/environment."""
|
|
420
|
+
search_paths = []
|
|
421
|
+
|
|
422
|
+
if self.venv_path:
|
|
423
|
+
if os.name == "nt":
|
|
424
|
+
venv_site = self.venv_path / "Lib" / "site-packages"
|
|
425
|
+
else:
|
|
426
|
+
venv_site = (
|
|
427
|
+
self.venv_path
|
|
428
|
+
/ "lib"
|
|
429
|
+
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
430
|
+
/ "site-packages"
|
|
431
|
+
)
|
|
432
|
+
if venv_site.exists():
|
|
433
|
+
search_paths = [str(venv_site)]
|
|
434
|
+
|
|
435
|
+
installed = set()
|
|
436
|
+
dists = (
|
|
437
|
+
importlib.metadata.distributions(path=search_paths)
|
|
438
|
+
if search_paths
|
|
439
|
+
else importlib.metadata.distributions()
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
for dist in dists:
|
|
443
|
+
name = dist.metadata.get("Name")
|
|
444
|
+
if name:
|
|
445
|
+
installed.add(name.lower().replace("_", "-"))
|
|
446
|
+
return installed
|
|
447
|
+
|
|
448
|
+
# def resolve_missing(self, required_imports: Set[str]) -> List[str]:
|
|
449
|
+
# """Identifies imports that are not installed in the target environment."""
|
|
450
|
+
# installed = self.get_installed_packages()
|
|
451
|
+
# missing_pypi_packages = []
|
|
452
|
+
|
|
453
|
+
# third_party_imports = required_imports - self.stdlib_modules
|
|
454
|
+
|
|
455
|
+
# for imp in third_party_imports:
|
|
456
|
+
# pypi_name = self.mapper.map_import_to_pypi(imp)
|
|
457
|
+
# normalized_pypi = pypi_name.lower().replace("_", "-")
|
|
458
|
+
|
|
459
|
+
# if normalized_pypi not in installed:
|
|
460
|
+
# missing_pypi_packages.append(pypi_name)
|
|
461
|
+
|
|
462
|
+
# return missing_pypi_packages
|
|
463
|
+
def resolve_missing(self, required_imports: Set[str]) -> List[str]:
|
|
464
|
+
"""Identifies imports that are not installed in the target environment."""
|
|
465
|
+
installed = self.get_installed_packages()
|
|
466
|
+
missing_pypi_packages = []
|
|
467
|
+
|
|
468
|
+
# 1. Ignore Python built-in standard library modules (os, sys, math, etc.)
|
|
469
|
+
third_party_imports = required_imports - self.stdlib_modules
|
|
470
|
+
|
|
471
|
+
# 2. Ignore Local Project Modules/Folders (e.g., auto_req, utils, tests)
|
|
472
|
+
external_imports = set()
|
|
473
|
+
for imp in third_party_imports:
|
|
474
|
+
local_folder = self.project_root / imp
|
|
475
|
+
local_file = self.project_root / f"{imp}.py"
|
|
476
|
+
|
|
477
|
+
# If the folder or file exists locally in your project, skip it!
|
|
478
|
+
if local_folder.exists() or local_file.exists():
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
external_imports.add(imp)
|
|
482
|
+
|
|
483
|
+
# 3. Only attempt to download actual third-party packages from PyPI
|
|
484
|
+
for imp in external_imports:
|
|
485
|
+
pypi_name = self.mapper.map_import_to_pypi(imp)
|
|
486
|
+
normalized_pypi = pypi_name.lower().replace("_", "-")
|
|
487
|
+
|
|
488
|
+
if normalized_pypi not in installed:
|
|
489
|
+
missing_pypi_packages.append(pypi_name)
|
|
490
|
+
|
|
491
|
+
return missing_pypi_packages
|
|
492
|
+
|
|
493
|
+
def run_post_install_hooks(self, installed_packages: List[str], python_exec: Path):
|
|
494
|
+
"""Runs additional setup commands required by specific packages after pip install."""
|
|
495
|
+
for pkg in installed_packages:
|
|
496
|
+
normalized_pkg = pkg.lower().replace("_", "-")
|
|
497
|
+
if normalized_pkg in self.POST_INSTALL_HOOKS:
|
|
498
|
+
hook_args = self.POST_INSTALL_HOOKS[normalized_pkg]
|
|
499
|
+
cmd = [str(python_exec), *hook_args]
|
|
500
|
+
print(f"[TraceReq] Running post-installation setup for '{pkg}': {' '.join(cmd)}")
|
|
501
|
+
subprocess.run(cmd, check=False)
|
|
502
|
+
|
|
503
|
+
def install_packages(self, packages: List[str]) -> bool:
|
|
504
|
+
"""Executes pip install targeting the detected virtual environment and triggers post-install hooks."""
|
|
505
|
+
if not packages:
|
|
506
|
+
return True
|
|
507
|
+
|
|
508
|
+
python_exec = self.get_target_python()
|
|
509
|
+
|
|
510
|
+
if self.venv_path:
|
|
511
|
+
print(f"\n[TraceReq] Target Virtual Environment Found: {self.venv_path.name}")
|
|
512
|
+
else:
|
|
513
|
+
print("\n[TraceReq] No local venv found. Target: Global Environment")
|
|
514
|
+
|
|
515
|
+
print(f"[TraceReq] Target Python: {python_exec}")
|
|
516
|
+
print(f"[TraceReq] Installing dependencies: {', '.join(packages)}...")
|
|
517
|
+
|
|
518
|
+
cmd = [str(python_exec), "-m", "pip", "install", *packages]
|
|
519
|
+
result = subprocess.run(cmd)
|
|
520
|
+
|
|
521
|
+
if result.returncode == 0:
|
|
522
|
+
print("[TraceReq] Installation completed successfully!")
|
|
523
|
+
self.run_post_install_hooks(packages, python_exec)
|
|
524
|
+
print()
|
|
525
|
+
return True
|
|
526
|
+
|
|
527
|
+
print("[TraceReq] Error occurred during pip installation.\n")
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
def attempt_error_remediation(self, error_output: str) -> bool:
|
|
531
|
+
"""Parses stdout/stderr tracebacks for known dependency errors and attempts repair."""
|
|
532
|
+
python_exec = self.get_target_python()
|
|
533
|
+
|
|
534
|
+
for rule in self.ERROR_REMEDIATION_RULES:
|
|
535
|
+
if re.search(rule["pattern"], error_output, re.IGNORECASE):
|
|
536
|
+
print(f"\n[TraceReq] Detected execution error: {rule['description']}")
|
|
537
|
+
cmd = [str(python_exec), *rule["args"]]
|
|
538
|
+
print(f"[TraceReq] Auto-remediating: {' '.join(cmd)}")
|
|
539
|
+
|
|
540
|
+
result = subprocess.run(cmd)
|
|
541
|
+
if result.returncode == 0:
|
|
542
|
+
print("[TraceReq] Auto-remediation succeeded!\n")
|
|
543
|
+
return True
|
|
544
|
+
|
|
545
|
+
print("[TraceReq] Auto-remediation failed.\n")
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
return False
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# import json
|
|
2
|
+
# import urllib.request
|
|
3
|
+
# import urllib.error
|
|
4
|
+
# from pathlib import Path
|
|
5
|
+
# from typing import Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# class PyPIMapper:
|
|
9
|
+
# """Maps import names to PyPI package names with static overrides and PyPI API fallback."""
|
|
10
|
+
|
|
11
|
+
# # Well-known import-to-PyPI mismatches
|
|
12
|
+
# KNOWN_MAPPINGS: Dict[str, str] = {
|
|
13
|
+
# "bs4": "beautifulsoup4",
|
|
14
|
+
# "PIL": "Pillow",
|
|
15
|
+
# "cv2": "opencv-python",
|
|
16
|
+
# "yaml": "PyYAML",
|
|
17
|
+
# "sklearn": "scikit-learn",
|
|
18
|
+
# "fitz": "PyMuPDF",
|
|
19
|
+
# "docx": "python-docx",
|
|
20
|
+
# "pptx": "python-pptx",
|
|
21
|
+
# "google/protobuf": "protobuf",
|
|
22
|
+
# "crypto": "pycryptodome",
|
|
23
|
+
# "serial": "pyserial",
|
|
24
|
+
# "jose": "python-jose",
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
# def __init__(self, cache_file: Path = Path(".auto_req_cache.json")):
|
|
28
|
+
# self.cache_file = cache_file
|
|
29
|
+
# self.cache: Dict[str, str] = self._load_cache()
|
|
30
|
+
|
|
31
|
+
# def _load_cache(self) -> Dict[str, str]:
|
|
32
|
+
# if self.cache_file.exists():
|
|
33
|
+
# try:
|
|
34
|
+
# with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
35
|
+
# return json.load(f)
|
|
36
|
+
# except Exception:
|
|
37
|
+
# return {}
|
|
38
|
+
# return {}
|
|
39
|
+
|
|
40
|
+
# def _save_cache(self):
|
|
41
|
+
# try:
|
|
42
|
+
# with open(self.cache_file, "w", encoding="utf-8") as f:
|
|
43
|
+
# json.dump(self.cache, f, indent=2)
|
|
44
|
+
# except Exception:
|
|
45
|
+
# pass
|
|
46
|
+
|
|
47
|
+
# def map_import_to_pypi(self, import_name: str) -> str:
|
|
48
|
+
# """Resolves an import name to a PyPI package name."""
|
|
49
|
+
# # 1. Check known mappings override
|
|
50
|
+
# if import_name in self.KNOWN_MAPPINGS:
|
|
51
|
+
# return self.KNOWN_MAPPINGS[import_name]
|
|
52
|
+
|
|
53
|
+
# # 2. Check local disk cache
|
|
54
|
+
# if import_name in self.cache:
|
|
55
|
+
# return self.cache[import_name]
|
|
56
|
+
|
|
57
|
+
# # 3. Query PyPI JSON API to confirm package existence
|
|
58
|
+
# pypi_name = import_name
|
|
59
|
+
# url = f"https://pypi.org/pypi/{import_name}/json"
|
|
60
|
+
# try:
|
|
61
|
+
# req = urllib.request.Request(url, headers={"User-Agent": "auto-req"})
|
|
62
|
+
# with urllib.request.urlopen(req, timeout=2):
|
|
63
|
+
# pypi_name = import_name
|
|
64
|
+
# except urllib.error.HTTPError as e:
|
|
65
|
+
# if e.code == 404:
|
|
66
|
+
# # Common fallback heuristic: underscore to dash
|
|
67
|
+
# pypi_name = import_name.replace("_", "-")
|
|
68
|
+
|
|
69
|
+
# # Update cache
|
|
70
|
+
# self.cache[import_name] = pypi_name
|
|
71
|
+
# self._save_cache()
|
|
72
|
+
# return pypi_name
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
import json
|
|
76
|
+
import urllib.request
|
|
77
|
+
import urllib.error
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
from typing import Dict, Optional
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PyPIMapper:
|
|
83
|
+
"""Dynamically resolves import names to PyPI package names using heuristics,
|
|
84
|
+
|
|
85
|
+
well-known mappings, and PyPI API checks.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Primary overrides where import name and PyPI package name completely diverge
|
|
89
|
+
KNOWN_MAPPINGS: Dict[str, str] = {
|
|
90
|
+
"allure": "allure-pytest",
|
|
91
|
+
"bs4": "beautifulsoup4",
|
|
92
|
+
"PIL": "Pillow",
|
|
93
|
+
"cv2": "opencv-python",
|
|
94
|
+
"yaml": "PyYAML",
|
|
95
|
+
"sklearn": "scikit-learn",
|
|
96
|
+
"fitz": "PyMuPDF",
|
|
97
|
+
"docx": "python-docx",
|
|
98
|
+
"pptx": "python-pptx",
|
|
99
|
+
"google/protobuf": "protobuf",
|
|
100
|
+
"crypto": "pycryptodome",
|
|
101
|
+
"serial": "pyserial",
|
|
102
|
+
"jose": "python-jose",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def __init__(self, cache_file: Path = Path(".auto_req_cache.json")):
|
|
106
|
+
self.cache_file = cache_file
|
|
107
|
+
self.cache: Dict[str, str] = self._load_cache()
|
|
108
|
+
|
|
109
|
+
def _load_cache(self) -> Dict[str, str]:
|
|
110
|
+
if self.cache_file.exists():
|
|
111
|
+
try:
|
|
112
|
+
with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
113
|
+
return json.load(f)
|
|
114
|
+
except Exception:
|
|
115
|
+
return {}
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
def _save_cache(self):
|
|
119
|
+
try:
|
|
120
|
+
with open(self.cache_file, "w", encoding="utf-8") as f:
|
|
121
|
+
json.dump(self.cache, f, indent=2)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def _verify_pypi_package(self, package_name: str) -> bool:
|
|
126
|
+
"""Queries PyPI JSON API to check if a package name exists."""
|
|
127
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
128
|
+
try:
|
|
129
|
+
req = urllib.request.Request(url, headers={"User-Agent": "TraceReq"})
|
|
130
|
+
with urllib.request.urlopen(req, timeout=3):
|
|
131
|
+
return True
|
|
132
|
+
except (urllib.error.HTTPError, urllib.error.URLError, Exception):
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def map_import_to_pypi(self, import_name: str) -> str:
|
|
136
|
+
"""Resolves an import name to a PyPI package name dynamically."""
|
|
137
|
+
clean_import = import_name.lower().strip()
|
|
138
|
+
|
|
139
|
+
# 1. Check known static overrides
|
|
140
|
+
if clean_import in self.KNOWN_MAPPINGS:
|
|
141
|
+
return self.KNOWN_MAPPINGS[clean_import]
|
|
142
|
+
|
|
143
|
+
# 2. Check local disk cache
|
|
144
|
+
if clean_import in self.cache:
|
|
145
|
+
return self.cache[clean_import]
|
|
146
|
+
|
|
147
|
+
# 3. Dynamic candidate resolution heuristics
|
|
148
|
+
candidates = [
|
|
149
|
+
clean_import, # Exact match (e.g., requests -> requests)
|
|
150
|
+
clean_import.replace("_", "-"), # Underscore to dash (e.g., pytest_cov -> pytest-cov)
|
|
151
|
+
f"pytest-{clean_import}", # Pytest plugin naming convention
|
|
152
|
+
f"python-{clean_import.replace('_', '-')}", # Common prefix (e.g., python-dotenv)
|
|
153
|
+
f"{clean_import.replace('_', '-')}-python", # Common suffix
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# 4. Probe candidates against PyPI API
|
|
157
|
+
resolved_pypi_name = clean_import
|
|
158
|
+
for candidate in candidates:
|
|
159
|
+
if self._verify_pypi_package(candidate):
|
|
160
|
+
resolved_pypi_name = candidate
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# 5. Persist resolved candidate to cache
|
|
164
|
+
self.cache[clean_import] = resolved_pypi_name
|
|
165
|
+
self._save_cache()
|
|
166
|
+
return resolved_pypi_name
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from auto_req.analyzer import CodeAnalyzer
|
|
3
|
+
from auto_req.installer import PackageInstaller
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def pytest_addoption(parser):
|
|
7
|
+
"""Add CLI flags to PyTest."""
|
|
8
|
+
group = parser.getgroup("auto-req")
|
|
9
|
+
group.addoption(
|
|
10
|
+
"--no-auto-req",
|
|
11
|
+
action="store_true",
|
|
12
|
+
default=False,
|
|
13
|
+
help="Disable automatic requirement analysis and installation.",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pytest_configure(config):
|
|
18
|
+
"""PyTest hook that runs early during test session setup."""
|
|
19
|
+
if config.getoption("--no-auto-req", default=False):
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
root_dir = Path(config.rootdir)
|
|
23
|
+
|
|
24
|
+
# 1. Scan codebase and test files
|
|
25
|
+
analyzer = CodeAnalyzer()
|
|
26
|
+
required_imports = analyzer.scan_directory(root_dir)
|
|
27
|
+
|
|
28
|
+
# 2. Resolve missing and auto-install
|
|
29
|
+
installer = PackageInstaller()
|
|
30
|
+
missing = installer.resolve_missing(required_imports)
|
|
31
|
+
|
|
32
|
+
if missing:
|
|
33
|
+
installer.install_packages(missing)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runtime-deps" # PyPI package name (pip install runtime-deps)
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AST-driven dynamic dependency resolution and runtime provisioning engine."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Pavan Kumar Korrapati" }]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
# Add any external libraries your tool requires (e.g., "requests")
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
runtime-deps = "auto_req.cli:main" # Full command name
|
|
18
|
+
rtdeps = "auto_req.cli:main" # Short, fast-to-type command alias
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runtime-deps
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AST-driven dynamic dependency resolution and runtime provisioning engine.
|
|
5
|
+
Author: Pavan Kumar Korrapati
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
TraceReq source project
|
|
10
|
+
│
|
|
11
|
+
│ python -m build
|
|
12
|
+
▼
|
|
13
|
+
dist/
|
|
14
|
+
├── auto_req-0.1.0-py3-none-any.whl ← reusable package
|
|
15
|
+
└── auto_req-0.1.0.tar.gz
|
|
16
|
+
│
|
|
17
|
+
│ pip install
|
|
18
|
+
▼
|
|
19
|
+
Any Python project
|
|
20
|
+
├── .venv/
|
|
21
|
+
├── tests/
|
|
22
|
+
├── src/
|
|
23
|
+
└── ...
|
|
24
|
+
│
|
|
25
|
+
└── auto-req command
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
auto_req/__init__.py
|
|
4
|
+
auto_req/analyzer.py
|
|
5
|
+
auto_req/cli.py
|
|
6
|
+
auto_req/installer.py
|
|
7
|
+
auto_req/mapper.py
|
|
8
|
+
auto_req/pytest_plugin.py
|
|
9
|
+
runtime_deps.egg-info/PKG-INFO
|
|
10
|
+
runtime_deps.egg-info/SOURCES.txt
|
|
11
|
+
runtime_deps.egg-info/dependency_links.txt
|
|
12
|
+
runtime_deps.egg-info/entry_points.txt
|
|
13
|
+
runtime_deps.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
auto_req
|