justpip 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.
- justpip-1.0.0/PKG-INFO +5 -0
- justpip-1.0.0/justpip/__init__.py +3 -0
- justpip-1.0.0/justpip/installer.py +208 -0
- justpip-1.0.0/justpip.egg-info/PKG-INFO +5 -0
- justpip-1.0.0/justpip.egg-info/SOURCES.txt +7 -0
- justpip-1.0.0/justpip.egg-info/dependency_links.txt +1 -0
- justpip-1.0.0/justpip.egg-info/top_level.txt +2 -0
- justpip-1.0.0/pyproject.toml +12 -0
- justpip-1.0.0/setup.cfg +4 -0
justpip-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import subprocess
|
|
3
|
+
import difflib
|
|
4
|
+
import re
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
PYPI_SIMPLE_INDEX = "https://pypi.org/simple/"
|
|
11
|
+
CACHE_FILE = os.path.join(os.path.expanduser("~"), ".justpip_pypi_cache.json")
|
|
12
|
+
CACHE_MAX_AGE_DAYS = 7
|
|
13
|
+
|
|
14
|
+
# Cases where the import name is genuinely different from the pip install name.
|
|
15
|
+
IMPORT_TO_PIP_NAME = {
|
|
16
|
+
"cv2": "opencv-python",
|
|
17
|
+
"PIL": "Pillow",
|
|
18
|
+
"sklearn": "scikit-learn",
|
|
19
|
+
"bs4": "beautifulsoup4",
|
|
20
|
+
"yaml": "pyyaml",
|
|
21
|
+
"Crypto": "pycryptodome",
|
|
22
|
+
"dotenv": "python-dotenv",
|
|
23
|
+
"serial": "pyserial",
|
|
24
|
+
"docx": "python-docx",
|
|
25
|
+
"pptx": "python-pptx",
|
|
26
|
+
"fitz": "pymupdf",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------- Fetching / caching the full PyPI package list ----------
|
|
31
|
+
|
|
32
|
+
def _download_all_package_names():
|
|
33
|
+
with urllib.request.urlopen(PYPI_SIMPLE_INDEX, timeout=30) as resp:
|
|
34
|
+
html = resp.read().decode("utf-8")
|
|
35
|
+
names = re.findall(r'<a[^>]*>([^<]+)</a>', html)
|
|
36
|
+
return sorted(set(names))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_cache():
|
|
40
|
+
if os.path.exists(CACHE_FILE):
|
|
41
|
+
try:
|
|
42
|
+
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
|
43
|
+
data = json.load(f)
|
|
44
|
+
age_days = (time.time() - data.get("timestamp", 0)) / 86400
|
|
45
|
+
if age_days < CACHE_MAX_AGE_DAYS:
|
|
46
|
+
return data["names"]
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_cache(names):
|
|
53
|
+
try:
|
|
54
|
+
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump({"timestamp": time.time(), "names": names}, f)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_all_package_names(force_refresh=False):
|
|
61
|
+
"""Returns every package name on PyPI, using a 7-day local cache."""
|
|
62
|
+
if not force_refresh:
|
|
63
|
+
cached = _load_cache()
|
|
64
|
+
if cached:
|
|
65
|
+
return cached
|
|
66
|
+
|
|
67
|
+
print("Fetching the full list of PyPI packages (first time only, ~10-20 seconds)...")
|
|
68
|
+
try:
|
|
69
|
+
names = _download_all_package_names()
|
|
70
|
+
_save_cache(names)
|
|
71
|
+
return names
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"Could not fetch the package list ({e}). Check your internet connection.")
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------- Search feature ----------
|
|
78
|
+
|
|
79
|
+
def search(prefix=None):
|
|
80
|
+
"""Search PyPI package names by prefix, e.g. search('s') or search('numpy')."""
|
|
81
|
+
if prefix is None:
|
|
82
|
+
prefix = input("Enter a letter or prefix to search (e.g. 's', 'numpy'): ").strip()
|
|
83
|
+
|
|
84
|
+
names = get_all_package_names()
|
|
85
|
+
if not names:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
prefix_lower = prefix.lower()
|
|
89
|
+
matches = [n for n in names if n.lower().startswith(prefix_lower)]
|
|
90
|
+
|
|
91
|
+
print(f"\nFound {len(matches)} package(s) starting with '{prefix}'.")
|
|
92
|
+
|
|
93
|
+
shown = 0
|
|
94
|
+
while shown < len(matches):
|
|
95
|
+
n_input = input(
|
|
96
|
+
f"Enter number of results to see (remaining: {len(matches) - shown}, or 'q' to stop): "
|
|
97
|
+
).strip()
|
|
98
|
+
|
|
99
|
+
if n_input.lower() == "q":
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
if not n_input.isdigit() or int(n_input) <= 0:
|
|
103
|
+
print("Please enter a positive number.")
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
count = int(n_input)
|
|
107
|
+
end = min(shown + count, len(matches))
|
|
108
|
+
for name in matches[shown:end]:
|
|
109
|
+
print(" -", name)
|
|
110
|
+
shown = end
|
|
111
|
+
print(f"(Shown {shown} of {len(matches)})")
|
|
112
|
+
|
|
113
|
+
return matches
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def package_exists(name):
|
|
117
|
+
"""Checks if an exact package name exists on PyPI."""
|
|
118
|
+
names = get_all_package_names()
|
|
119
|
+
return name.lower() in [n.lower() for n in names]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------- Suggestion logic ----------
|
|
123
|
+
|
|
124
|
+
def suggest_pip_name(missing_module):
|
|
125
|
+
"""Given a missing import name, guess the correct pip package name."""
|
|
126
|
+
if missing_module in IMPORT_TO_PIP_NAME:
|
|
127
|
+
return IMPORT_TO_PIP_NAME[missing_module], "known name mismatch"
|
|
128
|
+
|
|
129
|
+
all_names = get_all_package_names()
|
|
130
|
+
if not all_names:
|
|
131
|
+
return None, None
|
|
132
|
+
|
|
133
|
+
lower_names = [n.lower() for n in all_names]
|
|
134
|
+
|
|
135
|
+
if missing_module.lower() in lower_names:
|
|
136
|
+
idx = lower_names.index(missing_module.lower())
|
|
137
|
+
return all_names[idx], "exact match"
|
|
138
|
+
|
|
139
|
+
same_letter = [n for n in all_names if n and n[0].lower() == missing_module[0].lower()]
|
|
140
|
+
candidates = same_letter if same_letter else all_names
|
|
141
|
+
|
|
142
|
+
matches = difflib.get_close_matches(
|
|
143
|
+
missing_module.lower(),
|
|
144
|
+
[c.lower() for c in candidates],
|
|
145
|
+
n=1,
|
|
146
|
+
cutoff=0.6,
|
|
147
|
+
)
|
|
148
|
+
if matches:
|
|
149
|
+
for c in candidates:
|
|
150
|
+
if c.lower() == matches[0]:
|
|
151
|
+
return c, "possible typo"
|
|
152
|
+
|
|
153
|
+
return None, None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------- Install + crash handling ----------
|
|
157
|
+
|
|
158
|
+
def install_package(pip_name):
|
|
159
|
+
print(f"\nInstalling '{pip_name}' ...\n")
|
|
160
|
+
result = subprocess.run(
|
|
161
|
+
[sys.executable, "-m", "pip", "install", pip_name],
|
|
162
|
+
capture_output=False,
|
|
163
|
+
)
|
|
164
|
+
if result.returncode == 0:
|
|
165
|
+
print(f"\n'{pip_name}' installed successfully. Please run your program again.\n")
|
|
166
|
+
else:
|
|
167
|
+
print(f"\nSomething went wrong installing '{pip_name}'. Check the error above.\n")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def extract_missing_module(exc_value):
|
|
171
|
+
msg = str(exc_value)
|
|
172
|
+
match = re.search(r"No module named '([^']+)'", msg)
|
|
173
|
+
if match:
|
|
174
|
+
return match.group(1).split(".")[0]
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def custom_excepthook(exc_type, exc_value, exc_tb):
|
|
179
|
+
if exc_type is ModuleNotFoundError:
|
|
180
|
+
missing = extract_missing_module(exc_value)
|
|
181
|
+
if missing:
|
|
182
|
+
suggestion, reason = suggest_pip_name(missing)
|
|
183
|
+
if suggestion:
|
|
184
|
+
print(f"\nIt looks like '{missing}' is not installed.")
|
|
185
|
+
if reason == "possible typo":
|
|
186
|
+
print(f"Did you mean the package '{suggestion}'?")
|
|
187
|
+
elif reason == "known name mismatch":
|
|
188
|
+
print(f"The correct package to install is '{suggestion}'.")
|
|
189
|
+
else:
|
|
190
|
+
print(f"'{suggestion}' exists on PyPI.")
|
|
191
|
+
|
|
192
|
+
answer = input(f"Install '{suggestion}' now? (y/n): ").strip().lower()
|
|
193
|
+
if answer == "y":
|
|
194
|
+
install_package(suggestion)
|
|
195
|
+
return
|
|
196
|
+
else:
|
|
197
|
+
print("Okay, not installing.")
|
|
198
|
+
return
|
|
199
|
+
else:
|
|
200
|
+
print(f"\n'{missing}' was not found on PyPI, even as a close match.")
|
|
201
|
+
print("Tip: run justpip.search('...') to look it up manually.")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def activate():
|
|
208
|
+
sys.excepthook = custom_excepthook
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "justpip"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Detects missing/misspelled imports, suggests the correct PyPI package, and lets you search all packages"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
|
|
11
|
+
[tool.setuptools.packages.find]
|
|
12
|
+
where = ["."]
|
justpip-1.0.0/setup.cfg
ADDED