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 ADDED
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: justpip
3
+ Version: 1.0.0
4
+ Summary: Detects missing/misspelled imports, suggests the correct PyPI package, and lets you search all packages
5
+ Requires-Python: >=3.8
@@ -0,0 +1,3 @@
1
+ from .installer import activate, search, package_exists
2
+
3
+ activate()
@@ -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,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: justpip
3
+ Version: 1.0.0
4
+ Summary: Detects missing/misspelled imports, suggests the correct PyPI package, and lets you search all packages
5
+ Requires-Python: >=3.8
@@ -0,0 +1,7 @@
1
+ pyproject.toml
2
+ justpip/__init__.py
3
+ justpip/installer.py
4
+ justpip.egg-info/PKG-INFO
5
+ justpip.egg-info/SOURCES.txt
6
+ justpip.egg-info/dependency_links.txt
7
+ justpip.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ dist
2
+ justpip
@@ -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 = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+