88import sys
99import os
1010from pathlib import Path
11- import platform
1211import importlib .util
1312import winreg
1413from winpython import utils
1514from argparse import ArgumentParser
1615
17- # --- Constants ---
18- KEY_C = r"Software\Classes\%s"
19- KEY_CP = r"Software\Classes"
20- KEY_S = r"Software\Python"
21- KEY_S0 = KEY_S + r"\WinPython" # was PythonCore before PEP-0514
22- EWI = "Edit with IDLE"
23- EWS = "Edit with Spyder"
24- DROP_HANDLER_CLSID = "{60254CA5-953B-11CF-8C96-00AA00B8708C}"
2516
2617# --- Helper functions for Registry ---
2718
2819def _set_reg_value (root , key_path , name , value , reg_type = winreg .REG_SZ , verbose = False ):
2920 """Helper to create key and set a registry value using CreateKeyEx."""
3021 rootkey_name = "HKEY_CURRENT_USER" if root == winreg .HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
22+ if verbose :
23+ print (f"{ rootkey_name } \\ { key_path } \\ { name if name else '' } :{ value } " )
3124 try :
3225 # Use CreateKeyEx with context manager for automatic closing
33- # KEY_WRITE access is needed to set values
34-
35- if verbose :
36- print (f"{ rootkey_name } \\ { key_path } \\ { name if name else '' } :{ value } " )
3726 with winreg .CreateKeyEx (root , key_path , 0 , winreg .KEY_WRITE ) as key :
3827 winreg .SetValueEx (key , name , 0 , reg_type , value )
3928 except OSError as e :
@@ -42,9 +31,9 @@ def _set_reg_value(root, key_path, name, value, reg_type=winreg.REG_SZ, verbose=
4231def _delete_reg_key (root , key_path , verbose = False ):
4332 """Helper to delete a registry key, ignoring if not found."""
4433 rootkey_name = "HKEY_CURRENT_USER" if root == winreg .HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
34+ if verbose :
35+ print (f"{ rootkey_name } \\ { key_path } " )
4536 try :
46- if verbose :
47- print (f"{ rootkey_name } \\ { key_path } " )
4837 # DeleteKey can only delete keys with no subkeys.
4938 # For keys with (still) subkeys, use DeleteKeyEx on the parent key if available
5039 winreg .DeleteKey (root , key_path )
@@ -79,7 +68,6 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
7968 bname , ext = Path (name ).stem , Path (name ).suffix
8069 if ext .lower () == ".exe" :
8170 # Path for the shortcut file in the start menu folder
82- # This depends on utils.create_winpython_start_menu_folder creating the right path
8371 shortcut_name = str (Path (utils .create_winpython_start_menu_folder (current = current )) / bname ) + '.lnk'
8472 data .append (
8573 (
@@ -90,128 +78,86 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
9078 )
9179 return data
9280
93- # --- Registry Entry Definitions ---
94-
95- # Structure: (key_path, value_name, value, reg_type)
96- # Use None for value_name to set the default value of the key
97- REGISTRY_ENTRIES = []
98-
99- # --- Extensions ---
100- EXTENSIONS = {
101- ".py" : "Python.File" ,
102- ".pyw" : "Python.NoConFile" ,
103- ".pyc" : "Python.CompiledFile" ,
104- ".pyo" : "Python.CompiledFile" ,
105- }
106- for ext , file_type in EXTENSIONS .items ():
107- REGISTRY_ENTRIES .append ((KEY_C % ext , None , file_type ))
108-
109- # --- MIME types ---
110- MIME_TYPES = {
111- ".py" : "text/plain" ,
112- ".pyw" : "text/plain" ,
113- }
114- for ext , mime_type in MIME_TYPES .items ():
115- REGISTRY_ENTRIES .append ((KEY_C % ext , "Content Type" , mime_type ))
116-
117- # --- Verbs (Open, Edit with IDLE, Edit with Spyder) ---
118- # These depend on the python/pythonw/spyder paths
119- def _get_verb_entries (target ):
120- python = str ((Path (target ) / "python.exe" ).resolve ())
121- pythonw = str ((Path (target ) / "pythonw.exe" ).resolve ())
122- spyder_exe = str ((Path (target ).parent / "Spyder.exe" ).resolve ())
123-
124- # Command string for Spyder, fallback to script if exe not found
125- spyder_cmd = rf'"{ spyder_exe } " "%1"' if Path (spyder_exe ).is_file () else rf'"{ pythonw } " "{ target } \Scripts\spyder" "%1"'
126-
127- verbs_data = [
128- # Open verb
129- (rf"{ KEY_CP } \Python.File\shell\open\command" , None , rf'"{ python } " "%1" %*' ),
130- (rf"{ KEY_CP } \Python.NoConFile\shell\open\command" , None , rf'"{ pythonw } " "%1" %*' ),
131- (rf"{ KEY_CP } \Python.CompiledFile\shell\open\command" , None , rf'"{ python } " "%1" %*' ),
132- # Edit with IDLE verb
133- (rf"{ KEY_CP } \Python.File\shell\{ EWI } \command" , None , rf'"{ pythonw } " "{ target } \Lib\idlelib\idle.pyw" -n -e "%1"' ),
134- (rf"{ KEY_CP } \Python.NoConFile\shell\{ EWI } \command" , None , rf'"{ pythonw } " "{ target } \Lib\idlelib\idle.pyw" -n -e "%1"' ),
135- # Edit with Spyder verb
136- (rf"{ KEY_CP } \Python.File\shell\{ EWS } \command" , None , spyder_cmd ),
137- (rf"{ KEY_CP } \Python.NoConFile\shell\{ EWS } \command" , None , spyder_cmd ),
138- ]
139- return verbs_data
140-
141- # --- Drop support ---
142- DROP_SUPPORT_FILE_TYPES = ["Python.File" , "Python.NoConFile" , "Python.CompiledFile" ]
143- for file_type in DROP_SUPPORT_FILE_TYPES :
144- REGISTRY_ENTRIES .append ((rf"{ KEY_C % file_type } \shellex\DropHandler" , None , DROP_HANDLER_CLSID ))
145-
146- # --- Icons ---
147- def _get_icon_entries (target ):
148- dlls_path = str (Path (target ) / "DLLs" )
149- icon_data = [
150- (rf"{ KEY_CP } \Python.File\DefaultIcon" , None , rf"{ dlls_path } \py.ico" ),
151- (rf"{ KEY_CP } \Python.NoConFile\DefaultIcon" , None , rf"{ dlls_path } \py.ico" ),
152- (rf"{ KEY_CP } \Python.CompiledFile\DefaultIcon" , None , rf"{ dlls_path } \pyc.ico" ),
153- ]
154- return icon_data
155-
156- # --- Descriptions ---
157- DESCRIPTIONS = {
158- "Python.File" : "Python File" ,
159- "Python.NoConFile" : "Python File (no console)" ,
160- "Python.CompiledFile" : "Compiled Python File" ,
161- }
162- for file_type , desc in DESCRIPTIONS .items ():
163- REGISTRY_ENTRIES .append ((KEY_C % file_type , None , desc ))
164-
165-
16681# --- PythonCore entries (PEP-0514 and WinPython specific) ---
167- def _get_pythoncore_entries (target ):
168- python_infos = utils .get_python_infos (target ) # ('3.11', 64)
169- short_version = python_infos [0 ] # e.g., '3.11'
170- long_version = utils .get_python_long_version (target ) # e.g., '3.11.5'
171-
172- SupportUrl = "https://winpython.github.io"
173- SysArchitecture = f'{ python_infos [1 ]} bit' # e.g., '64bit'
174- SysVersion = short_version # e.g., '3.11'
175- Version = long_version # e.g., '3.11.5'
176- DisplayName = f'Python { Version } ({ SysArchitecture } )'
177-
178- python_exe = str ((Path (target ) / "python.exe" ).resolve ())
179- pythonw_exe = str ((Path (target ) / "pythonw.exe" ).resolve ())
180-
181- core_entries = []
182-
183- # Main version key (WinPython\3.11)
184- version_key = f"{ KEY_S0 } \\ { short_version } "
185- core_entries .extend ([
186- (version_key , 'DisplayName' , DisplayName ),
187- (version_key , 'SupportUrl' , SupportUrl ),
188- (version_key , 'SysVersion' , SysVersion ),
189- (version_key , 'SysArchitecture' , SysArchitecture ),
190- (version_key , 'Version' , Version ),
191- ])
19282
193- # InstallPath key (WinPython\3.11\InstallPath)
194- install_path_key = f"{ version_key } \\ InstallPath"
195- core_entries .extend ([
196- (install_path_key , None , str (Path (target ) / '' )), # Default value is the install dir
197- (install_path_key , 'ExecutablePath' , python_exe ),
198- (install_path_key , 'WindowedExecutablePath' , pythonw_exe ),
199- ])
20083
201- # InstallGroup key (WinPython\3.11\InstallPath\InstallGroup)
202- core_entries . append (( f" { install_path_key } \\ InstallGroup" , None , f"Python { short_version } " ))
84+ def register_in_registery ( target , current = True , reg_type = winreg . REG_SZ , verbose = True ) -> tuple [ list [ any ], ...]:
85+ """Register in Windows (like regedit)"""
20386
204- # Modules key (WinPython\3.11\Modules) - seems to be a placeholder key
205- core_entries .append ((f"{ version_key } \\ Modules" , None , "" ))
206-
207- # PythonPath key (WinPython\3.11\PythonPath)
208- core_entries .append ((f"{ version_key } \\ PythonPath" , None , rf"{ target } \Lib;{ target } \DLLs" ))
209-
210- # Help key (WinPython\3.11\Help\Main Python Documentation)
211- core_entries .append ((f"{ version_key } \\ Help\\ Main Python Documentation" , None , rf"{ target } \Doc\python{ long_version } .chm" ))
212-
213- return core_entries
87+ # --- Constants ---
88+ DROP_HANDLER_CLSID = "{60254CA5-953B-11CF-8C96-00AA00B8708C}"
21489
90+ # --- CONFIG ---
91+ target_path = Path (target ).resolve ()
92+ python_exe = str (target_path / "python.exe" )
93+ pythonw_exe = str (target_path / "pythonw.exe" )
94+ spyder_exe = str (target_path .parent / "Spyder.exe" )
95+ icon_py = str (target / "DLLs" / "py.ico" )
96+ icon_pyc = str (target / "DLLs" / "pyc.ico" )
97+ idle_path = str (target / "Lib" / "idlelib" / "idle.pyw" )
98+ doc_path = str (target / "Doc" / "html" / "index.html" )
99+ python_infos = utils .get_python_infos (target ) # ('3.11', 64)
100+ short_version = python_infos [0 ] # e.g., '3.11'
101+ version = utils .get_python_long_version (target ) # e.g., '3.11.5'
102+ arch = f'{ python_infos [1 ]} bit' # e.g., '64bit'
103+ display = f"Python { version } ({ arch } )"
104+
105+ permanent_entries = [] # key_path, name, value
106+ dynamic_entries = [] # key_path, name, value
107+ core_entries = [] # key_path, name, value
108+ lost_entries = [] # intermediate keys to remove later
109+ # --- File associations ---
110+ ext_map = {".py" : "Python.File" , ".pyw" : "Python.NoConFile" , ".pyc" : "Python.CompiledFile" }
111+ ext_label = {".py" : "Python File" , ".pyw" : "Python File (no console)" , ".pyc" : "Compiled Python File" }
112+ for ext , ftype in ext_map .items ():
113+ permanent_entries .append ((f"Software\\ Classes\\ { ext } " , None , ftype ))
114+ if ext in (".py" , ".pyw" ):
115+ permanent_entries .append ((f"Software\\ Classes\\ { ext } " , "Content Type" , "text/plain" ))
116+
117+ # --- Descriptions, Icons, DropHandlers ---
118+ for ext , ftype in ext_map .items ():
119+ dynamic_entries .append ((f"Software\\ Classes\\ { ftype } " , None , ext_label [ext ]))
120+ dynamic_entries .append ((f"Software\\ Classes\\ { ftype } \\ DefaultIcon" , None , icon_py if "Compiled" not in ftype else icon_pyc ))
121+ dynamic_entries .append ((f"Software\\ Classes\\ { ftype } \\ shellex\\ DropHandler" , None , DROP_HANDLER_CLSID ))
122+ lost_entries .append ((f"Software\\ Classes\\ { ftype } \\ shellex" , None , None ))
123+
124+ # --- Shell commands ---
125+ for ext , ftype in ext_map .items ():
126+ dynamic_entries .append ((f"Software\\ Classes\\ { ftype } \\ shell\\ open\\ command" , None , f'"{ pythonw_exe if ftype == 'Python.NoConFile' else python_exe } if " "%1" %*' ))
127+ lost_entries .append ((f"Software\\ Classes\\ { ftype } \\ shell\\ open" , None , None ))
128+ lost_entries .append ((f"Software\\ Classes\\ { ftype } \\ shell" , None , None ))
129+
130+ dynamic_entries .append ((rf"Software\Classes\Python.File\shell\Edit with IDLE\command" , None , f'"{ pythonw_exe } " "{ idle_path } " -n -e "%1"' ))
131+ dynamic_entries .append ((rf"Software\Classes\Python.NoConFile\shell\Edit with IDLE\command" , None , f'"{ pythonw_exe } " "{ idle_path } " -n -e "%1"' ))
132+ lost_entries .append ((rf"Software\Classes\Python.File\shell\Edit with IDLE" , None , None ))
133+ lost_entries .append ((rf"Software\Classes\Python.NoConFile\shell\Edit with IDLE" , None , None ))
134+
135+ if Path (spyder_exe ).exists ():
136+ dynamic_entries .append ((rf"Software\Classes\Python.File\shell\Edit with Spyder\command" , None , f'"{ spyder_exe } " "%1"' ))
137+ dynamic_entries .append ((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder\command" , None , f'"{ spyder_exe } " "%1"' ))
138+ lost_entries .append ((rf"Software\Classes\Python.File\shell\Edit with Spyder" , None , None ))
139+ lost_entries .append ((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder" , None , None ))
140+
141+ # --- WinPython Core registry entries (PEP 514 style) ---
142+ base = f"Software\\ Python\\ WinPython\\ { short_version } "
143+ core_entries .append ((base , "DisplayName" , display ))
144+ core_entries .append ((base , "SupportUrl" , "https://winpython.github.io" ))
145+ core_entries .append ((base , "SysVersion" , short_version ))
146+ core_entries .append ((base , "SysArchitecture" , arch ))
147+ core_entries .append ((base , "Version" , version ))
148+
149+ core_entries .append ((f"{ base } \\ InstallPath" , None , str (target )))
150+ core_entries .append ((f"{ base } \\ InstallPath" , "ExecutablePath" , python_exe ))
151+ core_entries .append ((f"{ base } \\ InstallPath" , "WindowedExecutablePath" , pythonw_exe ))
152+ core_entries .append ((f"{ base } \\ InstallPath\\ InstallGroup" , None , f"Python { short_version } " ))
153+
154+ core_entries .append ((f"{ base } \\ Modules" , None , "" ))
155+ core_entries .append ((f"{ base } \\ PythonPath" , None , f"{ target } \\ Lib;{ target } \\ DLLs" ))
156+ core_entries .append ((f"{ base } \\ Help\\ Main Python Documentation" , None , doc_path ))
157+ lost_entries .append ((f"{ base } \\ Help" , None , None ))
158+ lost_entries .append ((f"Software\\ Python\\ WinPython" , None , None ))
159+
160+ return permanent_entries , dynamic_entries , core_entries , lost_entries
215161
216162# --- Main Register/Unregister Functions ---
217163
@@ -223,19 +169,11 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
223169 if verbose :
224170 print (f'Creating WinPython registry entries for { target } ' )
225171
226- # Set static registry entries
227- for key_path , name , value in REGISTRY_ENTRIES :
228- _set_reg_value (root , key_path , name , value , verbose = verbose )
229-
230- # Set dynamic registry entries (verbs, icons, pythoncore)
231- dynamic_entries = []
232- dynamic_entries .extend (_get_verb_entries (target ))
233- dynamic_entries .extend (_get_icon_entries (target ))
234- dynamic_entries .extend (_get_pythoncore_entries (target ))
235-
236- for key_path , name , value in dynamic_entries :
237- _set_reg_value (root , key_path , name , value )
238-
172+ permanent_entries , dynamic_entries , core_entries , lost_entries = register_in_registery (target )
173+ # Set registry entries for given target
174+ for key_path , name , value in permanent_entries + dynamic_entries + core_entries :
175+ _set_reg_value (root , key_path , name , value , verbose = verbose )
176+
239177 # Create start menu entries
240178 if has_pywin32 :
241179 if verbose :
@@ -246,8 +184,7 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
246184 except Exception as e :
247185 print (f"Error creating shortcut for { desc } at { fname } : { e } " , file = sys .stderr )
248186 else :
249- print ("Skipping start menu shortcut creation as pywin32 package is needed." )
250-
187+ print ("Skipping start menu shortcut creation as pywin32 package is needed." )
251188
252189def unregister (target , current = True , verbose = True ):
253190 """Unregister a Python distribution from Windows registry and remove Start Menu shortcuts"""
@@ -256,92 +193,26 @@ def unregister(target, current=True, verbose=True):
256193
257194 if verbose :
258195 print (f'Removing WinPython registry entries for { target } ' )
196+
197+ permanent_entries , dynamic_entries , core_entries , lost_entries = register_in_registery (target )
259198
260199 # List of keys to attempt to delete, ordered from most specific to general
261- keys_to_delete = []
262-
263- # Add dynamic keys first (helps DeleteKey succeed)
264- dynamic_entries = []
265- dynamic_entries .extend (_get_verb_entries (target ))
266- dynamic_entries .extend (_get_icon_entries (target ))
267- dynamic_entries .extend (_get_pythoncore_entries (target ))
268-
269- # Collect parent keys from dynamic entries
270- dynamic_parent_keys = {entry [0 ] for entry in dynamic_entries }
271- # Add keys from static entries
272- static_parent_keys = {entry [0 ] for entry in REGISTRY_ENTRIES }
273-
274- # Combine and add the key templates that might become empty and should be removed
275- python_infos = utils .get_python_infos (target )
276- short_version = python_infos [0 ]
277- version_key_base = f"{ KEY_S0 } \\ { short_version } "
278-
279- # Keys from static REGISTRY_ENTRIES (mostly Class registrations)
280- keys_to_delete .extend ([
281- KEY_C % file_type + rf"\shellex\DropHandler" for file_type in DROP_SUPPORT_FILE_TYPES
282- ])
283- keys_to_delete .extend ([
284- KEY_C % file_type + rf"\shellex" for file_type in DROP_SUPPORT_FILE_TYPES
285- ])
286- #keys_to_delete.extend([
287- # KEY_C % file_type + rf"\DefaultIcon" for file_type in set(EXTENSIONS.values()) # Use values as file types
288- #])
289- keys_to_delete .extend ([
290- KEY_C % file_type + rf"\shell\{ EWI } \command" for file_type in ["Python.File" , "Python.NoConFile" ] # Specific types for IDLE verb
291- ])
292- keys_to_delete .extend ([
293- KEY_C % file_type + rf"\shell\{ EWS } \command" for file_type in ["Python.File" , "Python.NoConFile" ] # Specific types for Spyder verb
294- ])
295- # General open command keys (cover all file types)
296- keys_to_delete .extend ([
297- KEY_C % file_type + rf"\shell\open\command" for file_type in ["Python.File" , "Python.NoConFile" , "Python.CompiledFile" ]
298- ])
299-
300-
301- # Keys from dynamic entries (Verbs, Icons, PythonCore) - add parents
302- # Verbs
303- keys_to_delete .extend ([KEY_C % file_type + rf"\shell\{ EWI } " for file_type in ["Python.File" , "Python.NoConFile" ]])
304- keys_to_delete .extend ([KEY_C % file_type + rf"\shell\{ EWS } " for file_type in ["Python.File" , "Python.NoConFile" ]])
305- keys_to_delete .extend ([KEY_C % file_type + rf"\shell\open" for file_type in ["Python.File" , "Python.NoConFile" , "Python.CompiledFile" ]])
306- keys_to_delete .extend ([KEY_C % file_type + rf"\shell" for file_type in ["Python.File" , "Python.NoConFile" , "Python.CompiledFile" ]]) # Shell parent
307-
308- # Icons
309- keys_to_delete .extend ([KEY_C % file_type + rf"\DefaultIcon" for file_type in set (EXTENSIONS .values ())]) # Already added above? Check for duplicates or order
310- keys_to_delete .extend ([KEY_C % file_type for file_type in set (EXTENSIONS .values ())]) # Parent keys for file types
311-
312- # Extensions/Descriptions parents
313- # keys_to_delete.extend([KEY_C % ext for ext in EXTENSIONS.keys()]) # e.g., .py, .pyw
314-
315- # PythonCore keys (from most specific down to the base)
316- keys_to_delete .extend ([
317- f"{ version_key_base } \\ InstallPath\\ InstallGroup" ,
318- f"{ version_key_base } \\ InstallPath" ,
319- f"{ version_key_base } \\ Modules" ,
320- f"{ version_key_base } \\ PythonPath" ,
321- f"{ version_key_base } \\ Help\\ Main Python Documentation" ,
322- f"{ version_key_base } \\ Help" ,
323- version_key_base , # e.g., Software\Python\WinPython\3.11
324- KEY_S0 , # Software\Python\WinPython
325- #KEY_S, # Software\Python (only if WinPython key is the only subkey - risky to delete)
326- ])
327-
328- # Attempt to delete keys
329- # Use a set to avoid duplicates, then sort by length descending to try deleting children first
330- # (although DeleteKey only works on empty keys anyway, so explicit ordering is clearer)
331-
332- for key in keys_to_delete :
333- _delete_reg_key (root , key , verbose = verbose )
200+ keys_to_delete = sorted (list (set (key_path for key_path , name , value in (dynamic_entries + core_entries + lost_entries ))), key = len , reverse = True )
201+
202+ rootkey_name = "HKEY_CURRENT_USER" if root == winreg .HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
203+ for key_path in keys_to_delete :
204+ _delete_reg_key (root , key_path , verbose = verbose )
334205
335206 # Remove start menu shortcuts
336207 if has_pywin32 :
337208 if verbose :
338209 print (f'Removing WinPython menu for all icons in { target .parent } ' )
339210 _remove_start_menu_folder (target , current = current , has_pywin32 = True )
340211 # The original code had commented out code to delete .lnk files individually.
341- # remove_winpython_start_menu_folder is likely the intended method.
342212 else :
343213 print ("Skipping start menu removal as pywin32 package is needed." )
344214
215+
345216if __name__ == "__main__" :
346217 # Ensure we are running from the target WinPython environment
347218 parser = ArgumentParser (description = "Register or Un-register Python file extensions, icons " \
0 commit comments