8
8
import sys
9
9
import os
10
10
from pathlib import Path
11
- import platform
12
11
import importlib .util
13
12
import winreg
14
13
from winpython import utils
15
14
from argparse import ArgumentParser
16
15
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}"
25
16
26
17
# --- Helper functions for Registry ---
27
18
28
19
def _set_reg_value (root , key_path , name , value , reg_type = winreg .REG_SZ , verbose = False ):
29
20
"""Helper to create key and set a registry value using CreateKeyEx."""
30
21
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 } " )
31
24
try :
32
25
# 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 } " )
37
26
with winreg .CreateKeyEx (root , key_path , 0 , winreg .KEY_WRITE ) as key :
38
27
winreg .SetValueEx (key , name , 0 , reg_type , value )
39
28
except OSError as e :
@@ -42,9 +31,9 @@ def _set_reg_value(root, key_path, name, value, reg_type=winreg.REG_SZ, verbose=
42
31
def _delete_reg_key (root , key_path , verbose = False ):
43
32
"""Helper to delete a registry key, ignoring if not found."""
44
33
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 } " )
45
36
try :
46
- if verbose :
47
- print (f"{ rootkey_name } \\ { key_path } " )
48
37
# DeleteKey can only delete keys with no subkeys.
49
38
# For keys with (still) subkeys, use DeleteKeyEx on the parent key if available
50
39
winreg .DeleteKey (root , key_path )
@@ -79,7 +68,6 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
79
68
bname , ext = Path (name ).stem , Path (name ).suffix
80
69
if ext .lower () == ".exe" :
81
70
# Path for the shortcut file in the start menu folder
82
- # This depends on utils.create_winpython_start_menu_folder creating the right path
83
71
shortcut_name = str (Path (utils .create_winpython_start_menu_folder (current = current )) / bname ) + '.lnk'
84
72
data .append (
85
73
(
@@ -90,128 +78,86 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
90
78
)
91
79
return data
92
80
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
-
166
81
# --- 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
- ])
192
82
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
- ])
200
83
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)"""
203
86
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}"
214
89
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
215
161
216
162
# --- Main Register/Unregister Functions ---
217
163
@@ -223,19 +169,11 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
223
169
if verbose :
224
170
print (f'Creating WinPython registry entries for { target } ' )
225
171
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
+
239
177
# Create start menu entries
240
178
if has_pywin32 :
241
179
if verbose :
@@ -246,8 +184,7 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
246
184
except Exception as e :
247
185
print (f"Error creating shortcut for { desc } at { fname } : { e } " , file = sys .stderr )
248
186
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." )
251
188
252
189
def unregister (target , current = True , verbose = True ):
253
190
"""Unregister a Python distribution from Windows registry and remove Start Menu shortcuts"""
@@ -256,92 +193,26 @@ def unregister(target, current=True, verbose=True):
256
193
257
194
if verbose :
258
195
print (f'Removing WinPython registry entries for { target } ' )
196
+
197
+ permanent_entries , dynamic_entries , core_entries , lost_entries = register_in_registery (target )
259
198
260
199
# 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 )
334
205
335
206
# Remove start menu shortcuts
336
207
if has_pywin32 :
337
208
if verbose :
338
209
print (f'Removing WinPython menu for all icons in { target .parent } ' )
339
210
_remove_start_menu_folder (target , current = current , has_pywin32 = True )
340
211
# The original code had commented out code to delete .lnk files individually.
341
- # remove_winpython_start_menu_folder is likely the intended method.
342
212
else :
343
213
print ("Skipping start menu removal as pywin32 package is needed." )
344
214
215
+
345
216
if __name__ == "__main__" :
346
217
# Ensure we are running from the target WinPython environment
347
218
parser = ArgumentParser (description = "Register or Un-register Python file extensions, icons " \
0 commit comments