Skip to content

Commit 1be9e94

Browse files
committed
WheelHouse integrated management
1 parent cf4fea9 commit 1be9e94

File tree

3 files changed

+138
-16
lines changed

3 files changed

+138
-16
lines changed

winpython/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
OTHER DEALINGS IN THE SOFTWARE.
2929
"""
3030

31-
__version__ = '16.0.20250513'
31+
__version__ = '16.1.20250524'
3232
__license__ = __doc__
3333
__project_url__ = 'http://winpython.github.io/'

winpython/wheelhouse.py

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
#
2-
# WheelHouse.py
1+
#!/usr/bin/env python3
2+
"""
3+
WheelHouse.py - manage WinPython local WheelHouse.
4+
"""
5+
36
import sys
47
from pathlib import Path
58
from collections import defaultdict
9+
import shutil
10+
import subprocess
11+
from typing import Dict, List, Optional, Tuple
612

713
# Use tomllib if available (Python 3.11+), otherwise fall back to tomli
814
try:
@@ -14,10 +20,9 @@
1420
print("Please install tomli for Python < 3.11: pip install tomli")
1521
sys.exit(1)
1622

17-
18-
19-
def parse_pylock_toml(path):
20-
with open(Path(path), "rb") as f:
23+
def parse_pylock_toml(path: Path) -> Dict[str, Dict[str, str | List[str]]]:
24+
"""Parse a pylock.toml file and extract package information."""
25+
with open(path, "rb") as f:
2126
data = tomllib.load(f)
2227

2328
# This dictionary maps package names to (version, [hashes])
@@ -46,9 +51,9 @@ def parse_pylock_toml(path):
4651

4752
return package_hashes
4853

49-
50-
def write_requirements_txt(package_hashes, output_path="requirements.txt"):
51-
with open(Path(output_path), "w") as f:
54+
def write_requirements_txt(package_hashes: Dict[str, Dict[str, str | List[str]]], output_path: Path) -> None:
55+
"""Write package requirements to a requirements.txt file."""
56+
with open(output_path, "w") as f:
5257
for name, data in sorted(package_hashes.items()):
5358
version = data["version"]
5459
hashes = data["hashes"]
@@ -63,13 +68,119 @@ def write_requirements_txt(package_hashes, output_path="requirements.txt"):
6368

6469
print(f"✅ requirements.txt written to {output_path}")
6570

66-
def pylock_to_req(path, output_path=None):
71+
def pylock_to_req(path: Path, output_path: Optional[Path] = None) -> None:
72+
"""Convert a pylock.toml file to requirements.txt."""
6773
pkgs = parse_pylock_toml(path)
6874
if not output_path:
69-
output_path = path.parent / (path.stem.replace('pylock','requirement_with_hash')+ '.txt')
75+
output_path = path.parent / (path.stem.replace('pylock', 'requirement_with_hash') + '.txt')
7076
write_requirements_txt(pkgs, output_path)
7177

72-
if __name__ == "__main__":
78+
def run_pip_command(command: List[str], check: bool = True, capture_output=True) -> Tuple[bool, Optional[str]]:
79+
"""Run a pip command and return the result."""
80+
print('\n', ' '.join(command),'\n')
81+
try:
82+
result = subprocess.run(
83+
command,
84+
capture_output=capture_output,
85+
text=True,
86+
check=check
87+
)
88+
return (result.returncode == 0), (result.stderr or result.stdout)
89+
except subprocess.CalledProcessError as e:
90+
return False, e.stderr
91+
except FileNotFoundError:
92+
return False, "pip or Python not found."
93+
except Exception as e:
94+
return False, f"Unexpected error: {e}"
95+
96+
def get_wheels(requirements: Path, wheeldir: Path, from_local: Optional[Path] = None
97+
, only_check: bool = True,post_install: bool = False) -> bool:
98+
"""Download or check Python wheels based on requirements."""
99+
added = []
100+
if from_local:
101+
added += ['--no-index', '--trusted-host=None', f'--find-links={from_local}']
102+
pre_checks = [sys.executable, "-m", "pip", "install", "--dry-run", "--no-deps", "--require-hashes", "-r", str(requirements)] + added
103+
instruction = [sys.executable, "-m", "pip", "download", "--no-deps", "--require-hashes", "-r", str(requirements), "--dest", str(wheeldir)] + added
104+
post_install_cmd = [sys.executable, "-m", "pip", "install", "--no-deps", "--require-hashes", "-r", str(requirements)] + added
105+
106+
# Run pip dry-run, only if a move of directory
107+
if from_local and from_local != wheeldir:
108+
success, output = run_pip_command(pre_checks, check=False)
109+
if not success:
110+
print("❌ Dry-run failed. Here's the output:\n")
111+
print(output or "")
112+
return False
113+
114+
print("✅ Requirements can be installed successfully (dry-run passed).\n")
115+
116+
# All ok
117+
if only_check and not post_install:
118+
return True
119+
120+
# Want to install
121+
if not only_check and post_install:
122+
success, output = run_pip_command(post_install_cmd, check=False, capture_output=False)
123+
if not success:
124+
print("❌ Installation failed. Here's the output:\n")
125+
print(output or "")
126+
return False
127+
return True
128+
129+
# Otherwise download also, but not install direct
130+
success, output = run_pip_command(instruction)
131+
if not success:
132+
print("❌ Download failed. Here's the output:\n")
133+
print(output or "")
134+
return False
135+
136+
return True
137+
138+
def get_pylock_wheels(wheelhouse: Path, lockfile: Path, from_local: Optional[Path] = None) -> None:
139+
"""Get wheels for a pylock file."""
140+
filename = Path(lockfile).name
141+
wheelhouse.mkdir(parents=True, exist_ok=True)
142+
trusted_wheelhouse = wheelhouse / "included.wheels"
143+
trusted_wheelhouse.mkdir(parents=True, exist_ok=True)
144+
145+
filename_lock = wheelhouse / filename
146+
filename_req = wheelhouse / (Path(lockfile).stem.replace('pylock', 'requirement_with_hash') + '.txt')
147+
148+
pylock_to_req(Path(lockfile), filename_req)
149+
150+
if not str(Path(lockfile)) == str(filename_lock):
151+
shutil.copy2(lockfile, filename_lock)
152+
153+
# We create a destination for wheels that is specific, so we can check all is there
154+
destination_wheelhouse = wheelhouse / Path(lockfile).name.replace('.toml', '.wheels')
155+
in_trusted = False
156+
157+
if from_local is None:
158+
# Try from trusted WheelHouse
159+
print(f"\n\n*** Checking if we can install from our Local WheelHouse: ***\n {trusted_wheelhouse}\n\n")
160+
in_trusted = get_wheels(filename_req, destination_wheelhouse, from_local=trusted_wheelhouse, only_check=True)
161+
if in_trusted:
162+
print(f"\n\n*** We can install from Local WheelHouse: ***\n {trusted_wheelhouse}\n\n")
163+
user_input = input("Do you want to continue and install from {trusted_wheelhouse} ? (yes/no):")
164+
if user_input.lower() == "yes":
165+
in_installed = get_wheels(filename_req, trusted_wheelhouse, from_local=trusted_wheelhouse, only_check=True, post_install=True)
166+
167+
if not in_trusted:
168+
post_install = True if from_local and Path(from_local).is_dir and Path(from_local).samefile(destination_wheelhouse) else False
169+
if post_install:
170+
print(f"\n\n*** Installing from Local WheelHouse: ***\n {destination_wheelhouse}\n\n")
171+
else:
172+
print(f"\n\n*** Re-Checking if we can install from: {'pypi.org' if not from_local or from_local == '' else from_local}\n\n")
173+
174+
in_pylock = get_wheels(filename_req, destination_wheelhouse, from_local=from_local, only_check=False, post_install=post_install)
175+
if in_pylock:
176+
if not post_install:
177+
print(f"\n\n*** You can now install from this dedicated WheelHouse: ***\n {destination_wheelhouse}")
178+
print(f"\n via:\n wppm {filename_lock} -wh {destination_wheelhouse}\n")
179+
else:
180+
print(f"\n\n*** We can't install {filename} ! ***\n\n")
181+
182+
def main() -> None:
183+
"""Main entry point for the script."""
73184
if len(sys.argv) != 2:
74185
print("Usage: python pylock_to_requirements.py pylock.toml")
75186
sys.exit(1)
@@ -80,5 +191,8 @@ def pylock_to_req(path, output_path=None):
80191
sys.exit(1)
81192

82193
pkgs = parse_pylock_toml(path)
83-
dest = path.parent / (path.stem.replace('pylock','requirement_with_hash')+ '.txt')
194+
dest = path.parent / (path.stem.replace('pylock', 'requirement_with_hash') + '.txt')
84195
write_requirements_txt(pkgs, dest)
196+
197+
if __name__ == "__main__":
198+
main()

winpython/wppm.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,21 +231,22 @@ def main(test=False):
231231
description="WinPython Package Manager: handle a WinPython Distribution and its packages",
232232
formatter_class=RawTextHelpFormatter,
233233
)
234-
parser.add_argument("fname", metavar="package", nargs="?", default="", type=str, help="optional package name or package wheel")
234+
parser.add_argument("fname", metavar="package or lockfile", nargs="?", default="", type=str, help="optional package name or package wheel")
235235
parser.add_argument("-v", "--verbose", action="store_true", help="show more details on packages and actions")
236236
parser.add_argument( "--register", dest="registerWinPython", action="store_true", help=registerWinPythonHelp)
237237
# parser.add_argument( "--register_forall", action="store_true", help="Register distribution for all users")
238238
parser.add_argument("--unregister", dest="unregisterWinPython", action="store_true", help=unregisterWinPythonHelp)
239239
# parser.add_argument( "--unregister_forall", action="store_true", help="un-Register distribution for all users")
240240
parser.add_argument("--fix", action="store_true", help="make WinPython fix")
241241
parser.add_argument("--movable", action="store_true", help="make WinPython movable")
242+
parser.add_argument("-wh", "--wheelhouse", default=None, type=str, help="wheelhouse location to search for wheels: wppm pylock.toml -wh directory_of_wheels")
242243
parser.add_argument("-ls", "--list", action="store_true", help="list installed packages matching the given [optional] package expression: wppm -ls, wppm -ls pand")
243244
parser.add_argument("-lsa", dest="all", action="store_true",help=f"list details of package names matching given regular expression: wppm -lsa pandas -l1")
244245
parser.add_argument("-p",dest="pipdown",action="store_true",help="show Package dependencies of the given package[option]: wppm -p pandas[test]")
245246
parser.add_argument("-r", dest="pipup", action="store_true", help=f"show Reverse dependancies of the given package[option]: wppm -r pytest[test]")
246247
parser.add_argument("-l", "--levels", type=int, default=2, help="show 'LEVELS' levels of dependencies (with -p, -r), default is 2: wppm -p pandas -l1")
247248
parser.add_argument("-t", "--target", default=sys.prefix, help=f'path to target Python distribution (default: "{sys.prefix}")')
248-
parser.add_argument("-i", "--install", action="store_true", help="install a given package wheel (use pip for more features)")
249+
parser.add_argument("-i", "--install", action="store_true", help="install a given package wheel or pylock file (use pip for more features)")
249250
parser.add_argument("-u", "--uninstall", action="store_true", help="uninstall package (use pip for more features)")
250251

251252

@@ -331,7 +332,14 @@ def main(test=False):
331332
sys.exit()
332333
else:
333334
raise FileNotFoundError(f"File not found: {args.fname}")
335+
else:
334336
try:
337+
filename = Path(args.fname).name
338+
if filename.split('.')[0] == "pylock" and filename.split('.')[-1] == 'toml':
339+
print(' a lock file !', args.fname, dist.target)
340+
from winpython import wheelhouse as wh
341+
wh.get_pylock_wheels(Path(dist.target).parent/ "WheelHouse", Path(args.fname), args.wheelhouse)
342+
sys.exit()
335343
if args.uninstall:
336344
package = dist.find_package(args.fname)
337345
dist.uninstall(package)

0 commit comments

Comments
 (0)