Skip to content

Commit eeb0840

Browse files
committed
flash- 2.5 beta complicated things, chatgpt + manual restored order
1 parent 15f6202 commit eeb0840

File tree

1 file changed

+93
-222
lines changed

1 file changed

+93
-222
lines changed

winpython/associate.py

+93-222
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,21 @@
88
import sys
99
import os
1010
from pathlib import Path
11-
import platform
1211
import importlib.util
1312
import winreg
1413
from winpython import utils
1514
from 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

2819
def _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=
4231
def _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

252189
def 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+
345216
if __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

Comments
 (0)