runtime-deps 0.1.0__py3-none-any.whl

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.
auto_req/__init__.py ADDED
File without changes
auto_req/analyzer.py ADDED
@@ -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
auto_req/cli.py ADDED
@@ -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()
auto_req/installer.py ADDED
@@ -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
auto_req/mapper.py ADDED
@@ -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,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,11 @@
1
+ auto_req/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ auto_req/analyzer.py,sha256=CnBbg2W2N0_7nyiW4C2s871IcHgWi3T2KJ2pcujzYPU,1935
3
+ auto_req/cli.py,sha256=HgOeuBa8mZcnLrWpClIrxx36XhIDKFd-2FvXhrscQ7E,4807
4
+ auto_req/installer.py,sha256=dc_kUp0iLzh72J_qHrjfuNly3okXUHOTc0jaFSAsAhs,21963
5
+ auto_req/mapper.py,sha256=YH2MCQNWPOFS5aw7Cc8R5fr47up83msuT0msBkYaMQM,6012
6
+ auto_req/pytest_plugin.py,sha256=DPsDWx90oZO7aPlVntqUJI20bwRsI_ttabwbAwElHQE,964
7
+ runtime_deps-0.1.0.dist-info/METADATA,sha256=D2siJMr3WG-XJoiJKmpzetDBBqCrs5Tgn_Rw6LNlFA4,621
8
+ runtime_deps-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ runtime_deps-0.1.0.dist-info/entry_points.txt,sha256=RHxwHRvoF8xI3InFkshRJahmexqTFYUoxsdgxK0G7qY,78
10
+ runtime_deps-0.1.0.dist-info/top_level.txt,sha256=iBvflo4ZDdfGy5z7EsrRAwD8wg_pyDIKZ2acAQvsKKI,9
11
+ runtime_deps-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ rtdeps = auto_req.cli:main
3
+ runtime-deps = auto_req.cli:main
@@ -0,0 +1 @@
1
+ auto_req