reqora 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,305 @@
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
48
+
49
+ import ast
50
+ import os
51
+ import sys
52
+ from pathlib import Path
53
+ from typing import Iterable, Optional, Set, Tuple
54
+
55
+
56
+ class CodeAnalyzer:
57
+ """Analyze Python imports, including dependencies hidden behind local modules."""
58
+
59
+ PROJECT_MARKERS = (
60
+ "pyproject.toml",
61
+ "setup.py",
62
+ "setup.cfg",
63
+ "pytest.ini",
64
+ "tox.ini",
65
+ ".git",
66
+ )
67
+
68
+ SKIP_DIR_NAMES = {
69
+ ".git",
70
+ ".hg",
71
+ ".svn",
72
+ "__pycache__",
73
+ ".pytest_cache",
74
+ ".mypy_cache",
75
+ ".ruff_cache",
76
+ "build",
77
+ "dist",
78
+ "node_modules",
79
+ }
80
+
81
+ def __init__(self, project_root: Optional[Path] = None):
82
+ self.stdlib_modules: Set[str] = set(sys.stdlib_module_names)
83
+ self.project_root = Path(project_root).resolve() if project_root else None
84
+
85
+ @classmethod
86
+ def discover_project_root(cls, target: Path) -> Path:
87
+ """Return the nearest ancestor that looks like the project root."""
88
+ target = Path(target).resolve()
89
+ start = target.parent if target.is_file() else target
90
+
91
+ for candidate in (start, *start.parents):
92
+ if any((candidate / marker).exists() for marker in cls.PROJECT_MARKERS):
93
+ return candidate
94
+ return start
95
+
96
+ def _ensure_project_root(self, target: Path) -> Path:
97
+ if self.project_root is None:
98
+ self.project_root = self.discover_project_root(target)
99
+ return self.project_root
100
+
101
+ @staticmethod
102
+ def _looks_like_venv(path: Path) -> bool:
103
+ """Detect a virtual environment by structure, not by directory name."""
104
+ if not path.is_dir():
105
+ return False
106
+ if (path / "pyvenv.cfg").is_file():
107
+ return True
108
+ return (
109
+ (path / "Scripts" / "python.exe").is_file()
110
+ or (path / "bin" / "python").is_file()
111
+ ) and (
112
+ (path / "Lib" / "site-packages").is_dir()
113
+ or (path / "lib").is_dir()
114
+ )
115
+
116
+ def _source_roots(self, current_file: Optional[Path] = None) -> Tuple[Path, ...]:
117
+ root = self.project_root or (current_file.parent if current_file else Path.cwd())
118
+ roots = []
119
+ for candidate in (
120
+ current_file.parent if current_file else None,
121
+ root,
122
+ root / "src",
123
+ ):
124
+ if candidate and candidate.is_dir() and candidate not in roots:
125
+ roots.append(candidate)
126
+ return tuple(roots)
127
+
128
+ @staticmethod
129
+ def _module_candidates(base: Path, module_name: str) -> Iterable[Path]:
130
+ if not module_name:
131
+ return ()
132
+ module_path = base.joinpath(*module_name.split("."))
133
+ return (
134
+ module_path.with_suffix(".py"),
135
+ module_path / "__init__.py",
136
+ )
137
+
138
+ def _resolve_local_module(
139
+ self,
140
+ module_name: str,
141
+ current_file: Path,
142
+ level: int = 0,
143
+ ) -> Optional[Path]:
144
+ """Resolve an import to a local .py file/package if one exists."""
145
+ candidates = []
146
+
147
+ if level > 0:
148
+ # from .x => current package; from ..x => parent package, etc.
149
+ base = current_file.parent
150
+ for _ in range(level - 1):
151
+ base = base.parent
152
+ candidates.extend(self._module_candidates(base, module_name))
153
+ else:
154
+ for root in self._source_roots(current_file):
155
+ candidates.extend(self._module_candidates(root, module_name))
156
+
157
+ for candidate in candidates:
158
+ try:
159
+ resolved = candidate.resolve()
160
+ except OSError:
161
+ continue
162
+ if resolved.is_file() and self._is_inside_project(resolved):
163
+ return resolved
164
+ return None
165
+
166
+ def _is_inside_project(self, path: Path) -> bool:
167
+ root = self.project_root
168
+ if root is None:
169
+ return True
170
+ try:
171
+ path.resolve().relative_to(root.resolve())
172
+ return True
173
+ except ValueError:
174
+ return False
175
+
176
+ @staticmethod
177
+ def _parse_tree(file_path: Path) -> Optional[ast.AST]:
178
+ try:
179
+ text = file_path.read_text(encoding="utf-8")
180
+ return ast.parse(text, filename=str(file_path))
181
+ except (OSError, SyntaxError, UnicodeDecodeError):
182
+ return None
183
+
184
+ def _analyze_file_imports(self, file_path: Path) -> Tuple[Set[str], Set[Path]]:
185
+ """Return (external top-level imports, directly referenced local files)."""
186
+ tree = self._parse_tree(file_path)
187
+ if tree is None:
188
+ return set(), set()
189
+
190
+ external: Set[str] = set()
191
+ local_files: Set[Path] = set()
192
+
193
+ for node in ast.walk(tree):
194
+ if isinstance(node, ast.Import):
195
+ for alias in node.names:
196
+ root_name = alias.name.split(".", 1)[0]
197
+ if root_name in self.stdlib_modules:
198
+ continue
199
+ local = self._resolve_local_module(alias.name, file_path)
200
+ if local:
201
+ local_files.add(local)
202
+ else:
203
+ # Try the root package too, useful for package imports.
204
+ local_root = self._resolve_local_module(root_name, file_path)
205
+ if local_root:
206
+ local_files.add(local_root)
207
+ else:
208
+ external.add(root_name)
209
+
210
+ elif isinstance(node, ast.ImportFrom):
211
+ module_name = node.module or ""
212
+ root_name = module_name.split(".", 1)[0] if module_name else ""
213
+
214
+ if node.level == 0 and root_name in self.stdlib_modules:
215
+ continue
216
+
217
+ base_local = self._resolve_local_module(
218
+ module_name,
219
+ file_path,
220
+ level=node.level,
221
+ ) if module_name else None
222
+
223
+ found_local = False
224
+ if base_local:
225
+ local_files.add(base_local)
226
+ found_local = True
227
+
228
+ # `from package import submodule` can hide another local file.
229
+ for alias in node.names:
230
+ if alias.name == "*":
231
+ continue
232
+ child_name = f"{module_name}.{alias.name}" if module_name else alias.name
233
+ child_local = self._resolve_local_module(
234
+ child_name,
235
+ file_path,
236
+ level=node.level,
237
+ )
238
+ if child_local:
239
+ local_files.add(child_local)
240
+ found_local = True
241
+
242
+ if not found_local and node.level == 0 and root_name:
243
+ external.add(root_name)
244
+
245
+ return external, local_files
246
+
247
+ def analyze_entrypoint(self, file_path: Path) -> Set[str]:
248
+ """
249
+ Recursively follow local imports starting at file_path and return only
250
+ third-party imports required by the reachable dependency graph.
251
+ """
252
+ file_path = Path(file_path).resolve()
253
+ self._ensure_project_root(file_path)
254
+
255
+ required: Set[str] = set()
256
+ visited: Set[Path] = set()
257
+ pending = [file_path]
258
+
259
+ while pending:
260
+ current = pending.pop()
261
+ if current in visited or not current.is_file():
262
+ continue
263
+ visited.add(current)
264
+
265
+ external, local_files = self._analyze_file_imports(current)
266
+ required.update(external)
267
+
268
+ for local_file in local_files:
269
+ if local_file not in visited:
270
+ pending.append(local_file)
271
+
272
+ return required
273
+
274
+ def extract_imports_from_file(self, file_path: Path) -> Set[str]:
275
+ """
276
+ Backward-compatible API. It now follows local dependencies recursively,
277
+ which is the desired behavior for framework-based projects.
278
+ """
279
+ return self.analyze_entrypoint(file_path)
280
+
281
+ def scan_directory(self, target_dir: Path) -> Set[str]:
282
+ """Scan a project while pruning caches, build output, and any venv name."""
283
+ target_dir = Path(target_dir).resolve()
284
+ self._ensure_project_root(target_dir)
285
+ all_imports: Set[str] = set()
286
+
287
+ for current_root, dir_names, file_names in os.walk(target_dir):
288
+ current_path = Path(current_root)
289
+
290
+ kept_dirs = []
291
+ for name in dir_names:
292
+ directory = current_path / name
293
+ if name in self.SKIP_DIR_NAMES:
294
+ continue
295
+ if self._looks_like_venv(directory):
296
+ continue
297
+ kept_dirs.append(name)
298
+ dir_names[:] = kept_dirs
299
+
300
+ for name in file_names:
301
+ if not name.endswith(".py"):
302
+ continue
303
+ all_imports.update(self.analyze_entrypoint(current_path / name))
304
+
305
+ return all_imports
auto_req/cli.py ADDED
@@ -0,0 +1,165 @@
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(
33
+ # project_root=target_path.parent if target_path.is_file() else target_path
34
+ # )
35
+ # missing = installer.resolve_missing(required_imports)
36
+
37
+ # if not missing:
38
+ # print("[TraceReq] All required dependencies are already installed.")
39
+ # else:
40
+ # success = installer.install_packages(missing)
41
+ # if not success:
42
+ # sys.exit(1)
43
+
44
+ # # Automatically execute the file if target is a single Python script
45
+ # if target_path.is_file():
46
+ # python_exec = installer.get_target_python()
47
+ # print(f"[TraceReq] Running {target_path.name} using {python_exec.name}...\n")
48
+
49
+ # # Step 1: Run execution and capture output to intercept runtime errors
50
+ # process = subprocess.run(
51
+ # [str(python_exec), str(target_path)],
52
+ # capture_output=True,
53
+ # text=True,
54
+ # )
55
+
56
+ # # Print standard output cleanly
57
+ # if process.stdout:
58
+ # print(process.stdout, end="")
59
+
60
+ # # Step 2: Intercept execution failures & trigger auto-remediation
61
+ # if process.returncode != 0:
62
+ # error_output = (process.stderr or "") + (process.stdout or "")
63
+ # if process.stderr:
64
+ # print(process.stderr, file=sys.stderr, end="")
65
+
66
+ # # Attempt self-healing repair via post-execution error rules
67
+ # remediated = installer.attempt_error_remediation(error_output)
68
+
69
+ # if remediated:
70
+ # print(f"[TraceReq] Retrying execution for {target_path.name}...\n")
71
+ # retry_process = subprocess.run([str(python_exec), str(target_path)])
72
+ # sys.exit(retry_process.returncode)
73
+ # else:
74
+ # print(f"\n[TraceReq] Execution failed with exit code {process.returncode}")
75
+ # sys.exit(process.returncode)
76
+
77
+
78
+ # if __name__ == "__main__":
79
+ # main()
80
+
81
+ import argparse
82
+ import subprocess
83
+ import sys
84
+ from pathlib import Path
85
+
86
+ from auto_req.analyzer import CodeAnalyzer
87
+ from auto_req.installer import PackageInstaller
88
+
89
+
90
+ def main():
91
+ parser = argparse.ArgumentParser(
92
+ description="TraceReq: discover runtime dependencies recursively and install missing packages."
93
+ )
94
+ parser.add_argument(
95
+ "target",
96
+ nargs="?",
97
+ default=".",
98
+ help="Python entry file or project directory (default: current directory)",
99
+ )
100
+ args = parser.parse_args()
101
+
102
+ target_path = Path(args.target).resolve()
103
+ if not target_path.exists():
104
+ print(f"Error: Target path '{target_path}' does not exist.")
105
+ sys.exit(1)
106
+
107
+ project_root = CodeAnalyzer.discover_project_root(target_path)
108
+ analyzer = CodeAnalyzer(project_root=project_root)
109
+
110
+ if target_path.is_file():
111
+ if target_path.suffix != ".py":
112
+ print(f"Error: '{target_path}' is not a Python file.")
113
+ sys.exit(1)
114
+ print(f"[TraceReq] Analyzing dependency graph from: {target_path.name}")
115
+ required_imports = analyzer.analyze_entrypoint(target_path)
116
+ else:
117
+ print(f"[TraceReq] Scanning project: {target_path}")
118
+ required_imports = analyzer.scan_directory(target_path)
119
+
120
+ if required_imports:
121
+ print(f"[TraceReq] Third-party imports detected: {', '.join(sorted(required_imports))}")
122
+ else:
123
+ print("[TraceReq] No third-party imports detected.")
124
+
125
+ installer = PackageInstaller(project_root=project_root)
126
+ missing = installer.resolve_missing(required_imports)
127
+
128
+ if not missing:
129
+ print("[TraceReq] All required dependencies are already installed.")
130
+ elif not installer.install_packages(missing):
131
+ sys.exit(1)
132
+
133
+ if target_path.is_file():
134
+ python_exec = installer.get_target_python()
135
+ print(f"[TraceReq] Running {target_path.name} using {python_exec}...\n")
136
+
137
+ process = subprocess.run(
138
+ [str(python_exec), str(target_path)],
139
+ cwd=str(target_path.parent),
140
+ capture_output=True,
141
+ text=True,
142
+ )
143
+
144
+ if process.stdout:
145
+ print(process.stdout, end="")
146
+
147
+ if process.returncode != 0:
148
+ error_output = (process.stderr or "") + (process.stdout or "")
149
+ if process.stderr:
150
+ print(process.stderr, file=sys.stderr, end="")
151
+
152
+ if installer.attempt_error_remediation(error_output):
153
+ print(f"[TraceReq] Retrying execution for {target_path.name}...\n")
154
+ retry = subprocess.run(
155
+ [str(python_exec), str(target_path)],
156
+ cwd=str(target_path.parent),
157
+ )
158
+ sys.exit(retry.returncode)
159
+
160
+ print(f"\n[TraceReq] Execution failed with exit code {process.returncode}")
161
+ sys.exit(process.returncode)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()