ReqScan 1.0.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.
- reqscan-1.0.0/PKG-INFO +10 -0
- reqscan-1.0.0/ReqScan.egg-info/PKG-INFO +10 -0
- reqscan-1.0.0/ReqScan.egg-info/SOURCES.txt +8 -0
- reqscan-1.0.0/ReqScan.egg-info/dependency_links.txt +1 -0
- reqscan-1.0.0/ReqScan.egg-info/entry_points.txt +3 -0
- reqscan-1.0.0/ReqScan.egg-info/top_level.txt +1 -0
- reqscan-1.0.0/pyproject.toml +22 -0
- reqscan-1.0.0/reqscan/__init__.py +3 -0
- reqscan-1.0.0/reqscan/main.py +158 -0
- reqscan-1.0.0/setup.cfg +4 -0
reqscan-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ReqScan
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A dynamic requirements.txt builder that sifts through AST imports.
|
|
5
|
+
Author: turnt ducky
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ReqScan
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A dynamic requirements.txt builder that sifts through AST imports.
|
|
5
|
+
Author: turnt ducky
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reqscan
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ReqScan"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="turnt ducky" },
|
|
10
|
+
]
|
|
11
|
+
description = "A dynamic requirements.txt builder that sifts through AST imports."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
reqscan = "reqscan.main:main"
|
|
22
|
+
ReqScan = "reqscan.main:main"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import sysconfig
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import importlib.util
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
10
|
+
|
|
11
|
+
# Enable ANSI colors
|
|
12
|
+
if os.name == 'nt':
|
|
13
|
+
os.system("")
|
|
14
|
+
|
|
15
|
+
class C:
|
|
16
|
+
CYAN = '\033[96m'
|
|
17
|
+
GREEN = '\033[92m'
|
|
18
|
+
YELLOW = '\033[93m'
|
|
19
|
+
RED = '\033[91m'
|
|
20
|
+
RESET = '\033[0m'
|
|
21
|
+
BOLD = '\033[1m'
|
|
22
|
+
|
|
23
|
+
SKIP_DIRS = {
|
|
24
|
+
".git", "venv", ".venv", "env", ".env", "envs", "node_modules",
|
|
25
|
+
"__pycache__", "build", "dist", ".idea", ".vscode", "site-packages"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def print_banner():
|
|
29
|
+
banner = f"""{C.CYAN}{C.BOLD}
|
|
30
|
+
_____ _____
|
|
31
|
+
| __ \ / ____|
|
|
32
|
+
| |__) |___ __ _| (___ ___ __ _ _ __
|
|
33
|
+
| _ // _ \/ _` |\___ \ / __/ _` | '_ \
|
|
34
|
+
| | \ \ __/ (_| |____) | (_| (_| | | | |
|
|
35
|
+
|_| \_\___|\__, |_____/ \___\__,_|_| |_|
|
|
36
|
+
| |
|
|
37
|
+
|_|
|
|
38
|
+
|
|
39
|
+
| Developed by : turnt ducky 🦆 |{C.RESET}"""
|
|
40
|
+
print(banner)
|
|
41
|
+
|
|
42
|
+
def is_venv(dir_path):
|
|
43
|
+
path = Path(dir_path)
|
|
44
|
+
return (path / "pyvenv.cfg").exists() or (path / "bin" / "python").exists() or (path / "Scripts" / "python.exe").exists()
|
|
45
|
+
|
|
46
|
+
def get_python_files(root):
|
|
47
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
48
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not is_venv(os.path.join(dirpath, d))]
|
|
49
|
+
for file in filenames:
|
|
50
|
+
if file.endswith(".py"):
|
|
51
|
+
yield os.path.join(dirpath, file)
|
|
52
|
+
|
|
53
|
+
def extract_imports(file_path):
|
|
54
|
+
imports = set()
|
|
55
|
+
try:
|
|
56
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
57
|
+
tree = ast.parse(f.read())
|
|
58
|
+
except Exception:
|
|
59
|
+
return imports
|
|
60
|
+
for node in ast.walk(tree):
|
|
61
|
+
if isinstance(node, ast.Import):
|
|
62
|
+
for name in node.names:
|
|
63
|
+
imports.add(name.name.split(".")[0])
|
|
64
|
+
elif isinstance(node, ast.ImportFrom):
|
|
65
|
+
if node.module and node.level == 0:
|
|
66
|
+
imports.add(node.module.split(".")[0])
|
|
67
|
+
return imports
|
|
68
|
+
|
|
69
|
+
def get_stdlib_modules():
|
|
70
|
+
return set(sys.stdlib_module_names)
|
|
71
|
+
|
|
72
|
+
def get_local_modules(project_root):
|
|
73
|
+
local = {Path(project_root).name}
|
|
74
|
+
for dirpath, dirnames, filenames in os.walk(project_root):
|
|
75
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
76
|
+
for d in dirnames: local.add(d)
|
|
77
|
+
for f in filenames:
|
|
78
|
+
if f.endswith(".py"): local.add(os.path.splitext(f)[0])
|
|
79
|
+
return local
|
|
80
|
+
|
|
81
|
+
def get_package_map():
|
|
82
|
+
mapping = {}
|
|
83
|
+
for dist in importlib.metadata.distributions():
|
|
84
|
+
package_name = dist.metadata.get("Name")
|
|
85
|
+
if not package_name: continue
|
|
86
|
+
try:
|
|
87
|
+
top_level = dist.read_text("top_level.txt")
|
|
88
|
+
if top_level:
|
|
89
|
+
for line in top_level.splitlines():
|
|
90
|
+
name = line.strip()
|
|
91
|
+
if name: mapping.setdefault(name, package_name)
|
|
92
|
+
else:
|
|
93
|
+
mapping.setdefault(package_name.lower().replace("-", "_"), package_name)
|
|
94
|
+
except Exception: continue
|
|
95
|
+
return mapping
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
parser = argparse.ArgumentParser(description="Scan a directory for Python dependencies.")
|
|
99
|
+
parser.add_argument("path", nargs="?", default=".", help="Target directory to scan (default: current)")
|
|
100
|
+
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing requirements.txt without asking")
|
|
101
|
+
args = parser.parse_args()
|
|
102
|
+
|
|
103
|
+
print_banner()
|
|
104
|
+
project_root = Path(args.path).resolve()
|
|
105
|
+
output_file = project_root / "requirements.txt"
|
|
106
|
+
|
|
107
|
+
if not project_root.is_dir():
|
|
108
|
+
print(f" {C.RED}[!]{C.RESET} Error: {project_root} is not a directory.")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
print(f" {C.CYAN}[*]{C.RESET} Scanning: {C.YELLOW}{project_root}{C.RESET}")
|
|
112
|
+
|
|
113
|
+
py_files = list(get_python_files(project_root))
|
|
114
|
+
if not py_files:
|
|
115
|
+
print(f" {C.RED}[!]{C.RESET} No Python files found.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
all_imports = set()
|
|
119
|
+
with ProcessPoolExecutor() as executor:
|
|
120
|
+
results = executor.map(extract_imports, py_files)
|
|
121
|
+
for file_imports in results:
|
|
122
|
+
all_imports.update(file_imports)
|
|
123
|
+
|
|
124
|
+
stdlib = get_stdlib_modules()
|
|
125
|
+
local_modules = get_local_modules(project_root)
|
|
126
|
+
pkg_map = get_package_map()
|
|
127
|
+
|
|
128
|
+
external_imports = {imp for imp in all_imports if imp not in stdlib and imp not in local_modules}
|
|
129
|
+
|
|
130
|
+
final_requirements = {}
|
|
131
|
+
for imp in external_imports:
|
|
132
|
+
actual_package = pkg_map.get(imp, imp)
|
|
133
|
+
try:
|
|
134
|
+
version = importlib.metadata.version(actual_package).split("+")[0]
|
|
135
|
+
final_requirements[actual_package] = f"=={version}"
|
|
136
|
+
except importlib.metadata.PackageNotFoundError:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Merge Logic
|
|
140
|
+
merged = {}
|
|
141
|
+
if output_file.exists() and not args.overwrite:
|
|
142
|
+
with open(output_file, "r") as f:
|
|
143
|
+
for line in f:
|
|
144
|
+
if line.strip() and not line.startswith("#"):
|
|
145
|
+
p = line.split("==")[0].split(">=")[0].strip()
|
|
146
|
+
merged[p] = line.strip()
|
|
147
|
+
|
|
148
|
+
for pkg, ver in final_requirements.items():
|
|
149
|
+
merged[pkg] = f"{pkg}{ver}"
|
|
150
|
+
|
|
151
|
+
with open(output_file, "w") as f:
|
|
152
|
+
for line in sorted(merged.values(), key=lambda x: x.lower()):
|
|
153
|
+
f.write(line + "\n")
|
|
154
|
+
|
|
155
|
+
print(f" {C.GREEN}[✓]{C.RESET} Success! Generated {C.YELLOW}{output_file}{C.RESET}")
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
reqscan-1.0.0/setup.cfg
ADDED