1- #
2- # WheelHouse.py
1+ #!/usr/bin/env python3
2+ """
3+ WheelHouse.py - manage WinPython local WheelHouse.
4+ """
5+
36import sys
47from pathlib import Path
58from 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
814try :
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 ()
0 commit comments