From ca4727e5242d75c249bc3a1e86a0c2ac50cea686 Mon Sep 17 00:00:00 2001 From: GoToLoop Date: Sun, 14 Sep 2025 01:11:03 -0300 Subject: [PATCH 1/3] Theme refactoring and type hint fix --- .../kyanite_theme_syntax/__init__.py | 126 ++++++++++-------- thonnycontrib/kyanite_theme_ui/__init__.py | 46 ++++--- 2 files changed, 98 insertions(+), 74 deletions(-) diff --git a/thonnycontrib/kyanite_theme_syntax/__init__.py b/thonnycontrib/kyanite_theme_syntax/__init__.py index deb2254..c3a384a 100755 --- a/thonnycontrib/kyanite_theme_syntax/__init__.py +++ b/thonnycontrib/kyanite_theme_syntax/__init__.py @@ -1,63 +1,77 @@ '''kyanite syntax theme - theme inspired by processing 4.0b3 default theme, kyanite -''' + theme inspired by processing 4.0b3 default theme, kyanite''' from thonny import get_workbench, workbench +def load_plugin() -> None: + """Registers the 'Kyanite Syntax' theme with Thonny's workbench. -def kyanite_syntax() -> workbench.SyntaxThemeSettings: - '''based on default_light (see thonny > plugins > base_syntax_themes)''' - default_fg = '#111155' - default_bg = '#FFFFF2' - light_fg = '#94A4AF' - string_fg = '#7D4793' - open_string_bg = '#E2E7E1' - gutter_foreground = '#A4B4BF' - gutter_background = '#E2E7E1' - - return { - 'TEXT': { - 'foreground': default_fg, - 'insertbackground': default_fg, - 'background': default_bg, - }, - 'GUTTER': { - 'foreground': gutter_foreground, - 'background': gutter_background - }, - 'breakpoint': {'foreground': 'crimson'}, - 'current_line': {'background': '#D9FAFF'}, - 'definition': {'foreground': '#006699', 'font': 'BoldEditorFont'}, - 'string': {'foreground': string_fg}, - 'string3': { - 'foreground': string_fg, - 'background': None, 'font': 'EditorFont' - }, - 'open_string': {'foreground': string_fg, 'background': open_string_bg}, - 'open_string3': { - 'foreground': string_fg, - 'background': open_string_bg, - 'font': 'EditorFont', - }, - 'tab': {'background': '#F5ECD7'}, - 'keyword': {'foreground': '#33997E', 'font': 'EditorFont'}, - 'builtin': {'foreground': '#006699'}, - 'number': {'foreground': '#B04600'}, - 'comment': {'foreground': light_fg}, - 'welcome': {'foreground': light_fg}, - 'magic': {'foreground': light_fg}, - 'prompt': {'foreground': string_fg, 'font': 'BoldEditorFont'}, - 'stdin': {'foreground': 'Blue'}, - 'stdout': {'foreground': 'Black'}, - 'stderr': {'foreground': '#CC0000'}, # same as ANSI red - 'value': {'foreground': 'DarkBlue'}, - 'hyperlink': {'foreground': '#3A66DD', 'underline': True} - } - + This theme is inspired by the default Processing 4.0b3 theme and builds upon + Thonny's 'Default Light' syntax theme. It customizes foreground & background + colors for various syntax elements to create a soft, readable aesthetic.""" -def load_plugin() -> None: get_workbench().add_syntax_theme( - 'Kyanite Syntax', - 'Default Light', - kyanite_syntax - ) + 'Kyanite Syntax', 'Default Light', KYANITE_SYNTAX) + + +__all__ = 'load_plugin', + +DEFAULT_BG = '#FFFFF2' # Ivory +DEFAULT_FG = '#111155' # Dark Indigo +LIGHT_FG = '#94A4AF' # Light Slate Gray +STRING_FG = '#7D4793' # Deep Orchid +OPEN_STR_BG = '#E2E7E1' # Light Gray Green +GUTTER_BG = '#E2E7E1' # Light Gray Green +GUTTER_FG = '#A4B4BF' # Pale Steel Blue + +PALE_CYAN = '#D9FAFF' +M_TEAL_BLUE = '#006699' +LIGHT_BEIGE = '#F5ECD7' +SEA_GREEN = '#33997E' +BURNT_ORANGE = '#B04600' +CRIMSON_RED = '#CC0000' +ROYAL_BLUE = '#3A66DD' + +KYANITE_SYNTAX: workbench.SyntaxThemeSettings = { + 'TEXT': { + 'foreground': DEFAULT_FG, + 'insertbackground': DEFAULT_FG, + 'background': DEFAULT_BG + }, + + 'GUTTER': { 'foreground': GUTTER_FG, 'background': GUTTER_BG }, + + 'breakpoint': { 'foreground': 'crimson' }, + 'current_line': { 'background': PALE_CYAN }, + 'definition': { 'foreground': M_TEAL_BLUE, 'font': 'BoldEditorFont' }, + 'string': { 'foreground': STRING_FG }, + + 'string3': { + 'foreground': STRING_FG, + 'background': False, + 'font': 'EditorFont' + }, + + 'open_string': { 'foreground': STRING_FG, 'background': OPEN_STR_BG }, + + 'open_string3': { + 'foreground': STRING_FG, + 'background': OPEN_STR_BG, + 'font': 'EditorFont' + }, + + 'tab': { 'background': LIGHT_BEIGE }, + 'keyword': { 'foreground': SEA_GREEN, 'font': 'EditorFont' }, + 'builtin': { 'foreground': M_TEAL_BLUE }, + 'number': { 'foreground': BURNT_ORANGE }, + 'comment': { 'foreground': LIGHT_FG }, + 'welcome': { 'foreground': LIGHT_FG }, + 'magic': { 'foreground': LIGHT_FG }, + 'prompt': { 'foreground': STRING_FG, 'font': 'BoldEditorFont' }, + 'stdin': { 'foreground': 'Blue' }, + 'stdout': { 'foreground': 'Black' }, + 'stderr': { 'foreground': CRIMSON_RED }, # same as ANSI red + 'value': { 'foreground': 'DarkBlue' }, + 'hyperlink': { 'foreground': ROYAL_BLUE, 'underline': True } +} +'''Based on default_light (see thonny > plugins > base_syntax_themes)''' diff --git a/thonnycontrib/kyanite_theme_ui/__init__.py b/thonnycontrib/kyanite_theme_ui/__init__.py index e71ebd4..be18eb8 100755 --- a/thonnycontrib/kyanite_theme_ui/__init__.py +++ b/thonnycontrib/kyanite_theme_ui/__init__.py @@ -1,24 +1,34 @@ '''kyanite ui theme - theme inspired by processing 4.0b3 default theme, kyanite -''' + theme inspired by processing 4.0b3 default theme, kyanite''' from thonny import get_workbench from thonny.plugins.clean_ui_themes import clean - def load_plugin() -> None: - get_workbench().add_ui_theme( - 'Kyanite UI', - 'Clean Sepia', - clean( - frame_background='#6BA0C7', - text_background='#FFFFF2', - normal_detail='#C4E9FF', - high_detail='#B4D9EF', - low_detail='#A4C9DF', - normal_foreground='#002233', - high_foreground='#002233', - low_foreground='#000066', - custom_menubar=0, - ), - ) + """Registers the 'Kyanite UI' theme with Thonny's workbench. + + This theme is inspired by Processing 4.0b3's default aesthetic and builds on + Thonny's 'Clean Sepia' theme, using soft blues and ivory tones for a clean, + modern look.""" + + get_workbench().add_ui_theme('Kyanite UI', 'Clean Sepia', clean( + frame_background=STEEL_BLUE, + text_background=IVORY, + normal_detail=LIGHT_SKY_BLUE, + high_detail=POWDER_BLUE, + low_detail=LIGHT_BLUE, + normal_foreground=MIDNIGHT_BLUE, + high_foreground=MIDNIGHT_BLUE, + low_foreground=NAVY, + custom_menubar=0)) + + +__all__ = 'load_plugin', + +STEEL_BLUE = '#6BA0C7' +IVORY = '#FFFFF2' +LIGHT_SKY_BLUE = '#C4E9FF' +POWDER_BLUE = '#B4D9EF' +LIGHT_BLUE = '#A4C9DF' +MIDNIGHT_BLUE = '#002233' +NAVY = '#000066' From e229a6b76546711c6019af0a91f5a7c40e38e207 Mon Sep 17 00:00:00 2001 From: GoToLoop Date: Sun, 14 Sep 2025 01:32:59 -0300 Subject: [PATCH 2/3] Attempting to ignore install_jdk.py for theme branch --- thonnycontrib/thonny-py5mode/install_jdk.py | 266 ++++++++++---------- 1 file changed, 132 insertions(+), 134 deletions(-) diff --git a/thonnycontrib/thonny-py5mode/install_jdk.py b/thonnycontrib/thonny-py5mode/install_jdk.py index 6e0091e..a61c332 100644 --- a/thonnycontrib/thonny-py5mode/install_jdk.py +++ b/thonnycontrib/thonny-py5mode/install_jdk.py @@ -1,7 +1,10 @@ -'''thonny-py5mode JDK installer. -Checks for JDK and, if not found, installs it to Thonny's user directory.''' +'''thonny-py5mode JDK installer + checks for JDK and, if not found, installs it to the Thonny config directory +''' -import re, shutil, jdk +import re +import shutil +import jdk from pathlib import Path, PurePath from threading import Thread @@ -9,7 +12,7 @@ from os import environ as env, scandir, rename, PathLike from os.path import islink, realpath -from typing import Any, Callable, Literal, TypeAlias +from typing import Callable, Literal, TypeAlias from collections.abc import Iterable, Iterator import tkinter as tk @@ -22,56 +25,44 @@ StrPath: TypeAlias = str | PathLike[str] '''A type representing string-based filesystem paths.''' -PathAction: TypeAlias = Callable[[StrPath], Any] +PathAction: TypeAlias = Callable[[StrPath], None] '''Represents an action applied to a single path-like object.''' -JDK_PATTERN = re.compile(r""" +_JDK_PATTERN = re.compile(r""" (?:java|jdk) # Match 'java' or 'jdk' (non-capturing group) -? # Match optional hyphen '-' (\d+) # Capture JDK major version number as group(1) """, re.IGNORECASE | re.VERBOSE) -'''Captures the major version number from strings like "java-17" or "jdk21".''' -REQUIRE_JDK, VERSION_JDK = 17, '17' -'''JDK minimum required version to run Processing.''' +_REQUIRE_JDK, _VERSION_JDK = 17, '17' # Minimum required version +_JDK_DIR = 'jdk-' + _VERSION_JDK # JDK subfolder name -DOWNLOAD_JDK = '21' -'''JDK version to download.''' +_THONNY_USER_PATH = Path(THONNY_USER_DIR) # Thonny folder's full path string +_JDK_PATH = _THONNY_USER_PATH / _JDK_DIR # Path for JDK subfolder +_JDK_HOME = str(_JDK_PATH) # JDK subfolder's full path string -JDK_DIR = 'jdk-' + DOWNLOAD_JDK -'''JDK install subfolder name.''' +workbench = get_workbench() # Workbench singleton instance -THONNY_USER_PATH = Path(THONNY_USER_DIR) -'''Thonny user folder's full path string.''' - -JDK_PATH = THONNY_USER_PATH / JDK_DIR -'''Path for JDK installation subfolder.''' - -WORKBENCH = get_workbench() -'''Thonny's workbench singleton instance.''' - -def install_jdk() -> None: # Module's main entry-point function +def install_jdk(): # Module's main entry-point function '''Call this function from where this module is imported.''' - - if is_java_home_set(): return # JAVA_HOME already points to required version + if is_java_home_set(): return # JAVA_HOME points to a required JDK version # Set a local JAVA_HOME to the detected JDK found in THONNY_USER_DIR: if path := get_thonny_jdk_install(): set_java_home(path) # Otherwise, if Thonny doesn't have a proper JDK version... - else: ui_utils.show_dialog(JdkDialog()) # ... ask permission to download 1. + else: ui_utils.show_dialog(JdkDialog()) # ... ask permission to download it. def is_java_home_set() -> bool: '''Check system for existing JDK that meets the py5 version requirements.''' - if java_home := env.get('JAVA_HOME'): # Check if JAVA_HOME is already set system_jdk = 'TBD' # JDK version To-Be-Determined if islink(java_home): java_home = realpath(java_home) # If symlink, resolve actual path - if match := JDK_PATTERN.search(java_home): + if match := _JDK_PATTERN.search(java_home): system_jdk = match.group(1) # Get JDK version from 1st match group if is_valid_jdk_version(system_jdk) and is_valid_jdk_path(java_home): @@ -83,43 +74,41 @@ def is_java_home_set() -> bool: def get_thonny_jdk_install() -> PurePath | Literal['']: '''Check Thonny's user folder for a JDK installation subfolder and return its path. Otherwise, return an empty string.''' - for subfolder in get_all_thonny_folders(): # Loop over each subfolder name # Use regexp to check if subfolder contains a valid JDK name: - if match := JDK_PATTERN.search(subfolder): + if match := _JDK_PATTERN.search(subfolder): # Check JDK major version from 1st match group: - if is_valid_jdk_version( match.group(1) ): + if is_valid_jdk_version(match.group(1)): # Create a full path by joining THONNY_USER_DIR + folder name: - jdk_path = adjust_jdk_path(THONNY_USER_PATH / subfolder) + jdk_path = adjust_jdk_path(_THONNY_USER_PATH / subfolder) - # Check and return a valid JDK subfolder from THONNY_USER_DIR: + # Check and return a valid JDK subfolder in THONNY_USER_DIR: if is_valid_jdk_path(jdk_path): return jdk_path return '' # No JDK with required version found in THONNY_USER_DIR -def set_java_home(jdk_path: StrPath) -> None: +def set_java_home(jdk_path: StrPath): '''Add JDK path to config file (tools > options > general > env vars).''' + jdk_path = str(adjust_jdk_path(jdk_path)) + env['JAVA_HOME'] = jdk_path # Python's process points to Thonny's JDK - jdk_path = str(adjust_jdk_path(jdk_path)) # Platform-adjusted path - env['JAVA_HOME'] = jdk_path # Python's process points to Thonny's JDK too + jdk_path_entry = create_java_home_entry_from_path(jdk_path) + env_vars: set[str] = set(workbench.get_option('general.environment')) - java_home_entry = create_java_home_entry_from_path(jdk_path) - env_vars = dict.fromkeys(WORKBENCH.get_option('general.environment')) - - if java_home_entry not in env_vars: - entries = [ *drop_all_java_home_entries(env_vars), java_home_entry ] - WORKBENCH.set_option('general.environment', entries) - showinfo('JAVA_HOME', jdk_path, parent=WORKBENCH) + if jdk_path_entry not in env_vars: + entries = [*drop_all_java_home_entries(env_vars)] + entries.append(jdk_path_entry) + workbench.set_option('general.environment', entries) + showinfo('JAVA_HOME', jdk_path, parent=workbench) def adjust_jdk_path(jdk_path: StrPath) -> PurePath: '''Adjust JDK path for the specificity of current platform.''' - jdk_path = PurePath(jdk_path) # if MacOS, append "/Contents/Home/" to form the actual JDK path for it: - if jdk.OS is jdk.OperatingSystem.MAC and jdk_path.name != 'Home': + if jdk.OS is jdk.OperatingSystem.MAC and jdk_path.parts[-1] != 'Home': jdk_path = jdk_path / 'Contents' / 'Home' return jdk_path @@ -142,7 +131,7 @@ def _non_java_home_predicate(entry: str) -> bool: def is_valid_jdk_version(jdk_version: str) -> bool: '''Check if JDK version meets minimum version requirement.''' - return jdk_version.isdigit() and int(jdk_version) >= REQUIRE_JDK + return jdk_version.isdigit() and int(jdk_version) >= _REQUIRE_JDK def is_valid_jdk_path(jdk_path: StrPath) -> bool: @@ -157,85 +146,141 @@ def get_all_thonny_folders() -> list[str]: return sorted((e.name for e in entries if e.is_dir()), reverse=True) +class DownloadJDK(Thread): + '''Background thread for downloading & installing JDK into Thonny's folder. + + - Removes any preexisting JDK folders matching the expected version. + - Downloads and extracts the required JDK version. + - Renames the downloaded folder to the expected format. + - Sets JAVA_HOME both in system environment and Thonny configuration. + ''' + def run(self): + '''Download and setup JDK (installs to Thonny's config directory)''' + # Delete existing Thonny's JDK subfolders matching jdk-: + self.process_match_jdk_dirs(shutil.rmtree) + + # Download and extract JDK subfolder into Thonny's user folder: + jdk.install(_VERSION_JDK, path=THONNY_USER_DIR) + + # Rename extracted Thonny's JDK subfolder to jdk-: + self.process_match_jdk_dirs(self.rename_folder, True) + + set_java_home(_JDK_HOME) # Add a Thonny's JAVA_HOME entry for it + + + @staticmethod + def process_match_jdk_dirs(action: PathAction, only_1st=False): + '''Apply an action to JDK-matching subfolders in Thonny's folder.''' + for path in DownloadJDK.get_all_thonny_folder_paths(): + if path.name.startswith(_JDK_DIR): # Folder name matches + action(path) # Callback to run on each matching folder path + if only_1st: break # Stop at 1st match occurrence + + + @staticmethod + def get_all_thonny_folder_paths() -> Iterator[Path]: + '''Find all subfolder paths within Thonny's user folder''' + return filter(Path.is_dir, _THONNY_USER_PATH.iterdir()) + + + @staticmethod + def rename_folder(path: StrPath): + '''Rename a JDK subfolder to the expected jdk- format.''' + rename(path, _JDK_PATH) + + + class JdkDialog(ui_utils.CommonDialog): '''User-facing dialog prompting install of required JDK for py5 sketches. + - Presents user with option to proceed or cancel the JDK installation. - Displays a horizontal indeterminate-sized progress bar during download. - Launches a background thread to handle installation tasks. - - Shows a success message when installation is complete.''' + - Shows a success message when installation is complete. + ''' + _TITLE = tr('Install JDK ' + _VERSION_JDK + ' for py5') - _TITLE = tr('Install JDK ' + DOWNLOAD_JDK + ' for py5') - - _PROGRESS = tr('Downloading and extracting JDK ' + DOWNLOAD_JDK + ' ...') + _PROGRESS = tr(f'Downloading and extracting JDK {_REQUIRE_JDK} ...') _OK, _CANCEL, _DONE = map(tr, ('Proceed', 'Cancel', 'JDK done')) - _MSG = 'JDK ' + DOWNLOAD_JDK + tr(' extracted to ') + THONNY_USER_DIR + tr( + _MSG = 'JDK ' + _VERSION_JDK + tr(' extracted to ') + THONNY_USER_DIR + tr( '\n\nYou can now run py5 sketches.') _INSTALL_JDK = tr( - "Thonny requires at least JDK " + VERSION_JDK + " to run py5 sketches. " - "It'll need to download about 180 MB.") + "Thonny requires JDK " + _VERSION_JDK + " to run py5 sketches. " + "It'll need to download about 180 MB." + ) - _PROGRESS_BAR_Y_PADDING = 0, 15 + _PAD = 0, 15 - def __init__(self, master=WORKBENCH, skip_diag_attribs=False, **kw): + def __init__(self, master=workbench, skip_diag_attribs=False, **kw): super().__init__(master, skip_diag_attribs, **kw) - # Set dialog properties: title, fixed size, close button disabled: - self.title(self._TITLE) # Dialog title for JDK installation - self.resizable(height=tk.FALSE, width=tk.FALSE) # Prevent its resizing - self.protocol('WM_DELETE_WINDOW', '{#}') # Disable its close button - # Window/Frame: - main_frame = self.main_frame = ttk.Frame(self) - main_frame.grid(ipadx=15, ipady=15, sticky=tk.NSEW) - main_frame.rowconfigure(0, weight=1) - main_frame.columnconfigure(0, weight=1) + self.main_frame = ttk.Frame(self) + self.main_frame.grid(ipadx=15, ipady=15, sticky=tk.NSEW) + self.main_frame.rowconfigure(0, weight=1) + self.main_frame.columnconfigure(0, weight=1) + + self.title(self._TITLE) + self.resizable(height=tk.FALSE, width=tk.FALSE) + self.protocol('WM_DELETE_WINDOW', '{#}') # Block window close button # Display install message: - message_label = ttk.Label(main_frame, text=self._INSTALL_JDK) + message_label = ttk.Label(self.main_frame, text=self._INSTALL_JDK) message_label.grid(pady=0, columnspan=2) - # OK proceed button: - ok_button = self.ok_button = ttk.Button( - main_frame, - text=self._OK, - command=self._proceed, - default=tk.ACTIVE) + # OK button: + self.ok_button = ttk.Button( + self.main_frame, + text=self._OK, + command=self._proceed, + default=tk.ACTIVE + ) - ok_button.grid(row=2, column=0, padx=15, pady=15, sticky=tk.W) - ok_button.focus_set() + self.ok_button.grid( + row=2, column=0, + padx=15, pady=15, + sticky=tk.W + ) + + self.ok_button.focus_set() # Cancel button: - cancel_button = self.cancel_button = ttk.Button( - main_frame, - text=self._CANCEL, - command=self._close) + self.cancel_button = ttk.Button( + self.main_frame, + text=self._CANCEL, + command=self._close + ) - cancel_button.grid(row=2, column=1, padx=15, pady=15, sticky=tk.E) + self.cancel_button.grid( + row=2, column=1, + padx=15, pady=15, + sticky=tk.E + ) - def _proceed(self) -> None: + def _proceed(self): '''Starts JDK downloader thread.''' - # Get rid of both OK & Cancel buttons: if self.ok_button: self.ok_button.destroy() if self.cancel_button: self.cancel_button.destroy() # Progress bar label: dl_label = ttk.Label(self.main_frame, text=self._PROGRESS) - dl_label.grid(row=1, columnspan=2, pady=self._PROGRESS_BAR_Y_PADDING) + dl_label.grid(row=1, columnspan=2, pady=self._PAD) # Progress bar: progress_bar = ttk.Progressbar(self.main_frame, mode='indeterminate') progress_bar.grid( row=2, column=0, columnspan=2, - padx=15, pady=self._PROGRESS_BAR_Y_PADDING, - sticky=tk.EW) + padx=15, pady=self._PAD, + sticky=tk.EW + ) - # Start progress bar animation + download thread: + # Start progress bar animation and download thread: if self.main_frame: self.main_frame.tkraise() download_thread = DownloadJDK() @@ -245,66 +290,19 @@ def _proceed(self) -> None: self._monitor(download_thread, progress_bar) - def _monitor(self, download: Thread, progress: ttk.Progressbar) -> None: + def _monitor(self, download: Thread, progress: ttk.Progressbar): '''Animate progress bar while JDK installs and extracts.''' - if download.is_alive(): self.after(100, lambda: self._monitor(download, progress)) return - # Destroy this JDK dialog instance once download has finished: progress.stop() self._close() - showinfo(self._DONE, self._MSG, parent=WORKBENCH) + showinfo(self._DONE, self._MSG, parent=workbench) - def _close(self) -> None: + def _close(self): '''Fully shutdown the JdkDialog instance.''' self.destroy() self.main_frame = self.ok_button = self.cancel_button = None - - - -class DownloadJDK(Thread): - '''Background thread for downloading & installing JDK into Thonny's folder. - - Removes any preexisting JDK folders matching the expected version. - - Downloads and extracts the required JDK version. - - Renames the downloaded folder to the expected format. - - Sets JAVA_HOME on Thonny configuration.''' - - def run(self) -> None: - '''Download and setup JDK (installs to Thonny's user directory)''' - - # Delete existing Thonny's JDK subfolders matching jdk-: - self.process_match_jdk_dirs(shutil.rmtree) - - # Download and extract JDK subfolder into Thonny's user folder: - jdk.install(DOWNLOAD_JDK, path=THONNY_USER_DIR) - - # Rename extracted Thonny's JDK subfolder to jdk-: - self.process_match_jdk_dirs(self.rename_folder, True) - - set_java_home(JDK_PATH) # Add a Thonny's JAVA_HOME entry for it - - - @staticmethod - def process_match_jdk_dirs(action: PathAction, only_1st=False) -> None: - '''Apply an action to JDK-matching subfolders in Thonny's folder.''' - - for path in DownloadJDK.get_all_thonny_folder_paths(): - if path.name.startswith(JDK_DIR): # Folder name matches - action(path) # Callback to run on each matching folder path - if only_1st: break # Stop at 1st match occurrence - - - @staticmethod - def get_all_thonny_folder_paths() -> Iterator[Path]: - '''Find all subfolder paths within Thonny's user folder''' - return filter(Path.is_dir, THONNY_USER_PATH.iterdir()) - - - @staticmethod - def rename_folder(path: StrPath) -> None: - '''Rename a JDK subfolder to the expected jdk- format.''' - rename(path, JDK_PATH) From 6b8961b0f094e6f2b4db0bfbe72017a3c32d4da0 Mon Sep 17 00:00:00 2001 From: GoToLoop Date: Mon, 15 Sep 2025 22:40:40 -0300 Subject: [PATCH 3/3] rc5 --- thonnycontrib/thonny-py5mode/install_jdk.py | 266 ++++++++++---------- 1 file changed, 134 insertions(+), 132 deletions(-) diff --git a/thonnycontrib/thonny-py5mode/install_jdk.py b/thonnycontrib/thonny-py5mode/install_jdk.py index a61c332..6e0091e 100644 --- a/thonnycontrib/thonny-py5mode/install_jdk.py +++ b/thonnycontrib/thonny-py5mode/install_jdk.py @@ -1,10 +1,7 @@ -'''thonny-py5mode JDK installer - checks for JDK and, if not found, installs it to the Thonny config directory -''' +'''thonny-py5mode JDK installer. +Checks for JDK and, if not found, installs it to Thonny's user directory.''' -import re -import shutil -import jdk +import re, shutil, jdk from pathlib import Path, PurePath from threading import Thread @@ -12,7 +9,7 @@ from os import environ as env, scandir, rename, PathLike from os.path import islink, realpath -from typing import Callable, Literal, TypeAlias +from typing import Any, Callable, Literal, TypeAlias from collections.abc import Iterable, Iterator import tkinter as tk @@ -25,44 +22,56 @@ StrPath: TypeAlias = str | PathLike[str] '''A type representing string-based filesystem paths.''' -PathAction: TypeAlias = Callable[[StrPath], None] +PathAction: TypeAlias = Callable[[StrPath], Any] '''Represents an action applied to a single path-like object.''' -_JDK_PATTERN = re.compile(r""" +JDK_PATTERN = re.compile(r""" (?:java|jdk) # Match 'java' or 'jdk' (non-capturing group) -? # Match optional hyphen '-' (\d+) # Capture JDK major version number as group(1) """, re.IGNORECASE | re.VERBOSE) +'''Captures the major version number from strings like "java-17" or "jdk21".''' -_REQUIRE_JDK, _VERSION_JDK = 17, '17' # Minimum required version -_JDK_DIR = 'jdk-' + _VERSION_JDK # JDK subfolder name +REQUIRE_JDK, VERSION_JDK = 17, '17' +'''JDK minimum required version to run Processing.''' -_THONNY_USER_PATH = Path(THONNY_USER_DIR) # Thonny folder's full path string -_JDK_PATH = _THONNY_USER_PATH / _JDK_DIR # Path for JDK subfolder -_JDK_HOME = str(_JDK_PATH) # JDK subfolder's full path string +DOWNLOAD_JDK = '21' +'''JDK version to download.''' -workbench = get_workbench() # Workbench singleton instance +JDK_DIR = 'jdk-' + DOWNLOAD_JDK +'''JDK install subfolder name.''' -def install_jdk(): # Module's main entry-point function +THONNY_USER_PATH = Path(THONNY_USER_DIR) +'''Thonny user folder's full path string.''' + +JDK_PATH = THONNY_USER_PATH / JDK_DIR +'''Path for JDK installation subfolder.''' + +WORKBENCH = get_workbench() +'''Thonny's workbench singleton instance.''' + +def install_jdk() -> None: # Module's main entry-point function '''Call this function from where this module is imported.''' - if is_java_home_set(): return # JAVA_HOME points to a required JDK version + + if is_java_home_set(): return # JAVA_HOME already points to required version # Set a local JAVA_HOME to the detected JDK found in THONNY_USER_DIR: if path := get_thonny_jdk_install(): set_java_home(path) # Otherwise, if Thonny doesn't have a proper JDK version... - else: ui_utils.show_dialog(JdkDialog()) # ... ask permission to download it. + else: ui_utils.show_dialog(JdkDialog()) # ... ask permission to download 1. def is_java_home_set() -> bool: '''Check system for existing JDK that meets the py5 version requirements.''' + if java_home := env.get('JAVA_HOME'): # Check if JAVA_HOME is already set system_jdk = 'TBD' # JDK version To-Be-Determined if islink(java_home): java_home = realpath(java_home) # If symlink, resolve actual path - if match := _JDK_PATTERN.search(java_home): + if match := JDK_PATTERN.search(java_home): system_jdk = match.group(1) # Get JDK version from 1st match group if is_valid_jdk_version(system_jdk) and is_valid_jdk_path(java_home): @@ -74,41 +83,43 @@ def is_java_home_set() -> bool: def get_thonny_jdk_install() -> PurePath | Literal['']: '''Check Thonny's user folder for a JDK installation subfolder and return its path. Otherwise, return an empty string.''' + for subfolder in get_all_thonny_folders(): # Loop over each subfolder name # Use regexp to check if subfolder contains a valid JDK name: - if match := _JDK_PATTERN.search(subfolder): + if match := JDK_PATTERN.search(subfolder): # Check JDK major version from 1st match group: - if is_valid_jdk_version(match.group(1)): + if is_valid_jdk_version( match.group(1) ): # Create a full path by joining THONNY_USER_DIR + folder name: - jdk_path = adjust_jdk_path(_THONNY_USER_PATH / subfolder) + jdk_path = adjust_jdk_path(THONNY_USER_PATH / subfolder) - # Check and return a valid JDK subfolder in THONNY_USER_DIR: + # Check and return a valid JDK subfolder from THONNY_USER_DIR: if is_valid_jdk_path(jdk_path): return jdk_path return '' # No JDK with required version found in THONNY_USER_DIR -def set_java_home(jdk_path: StrPath): +def set_java_home(jdk_path: StrPath) -> None: '''Add JDK path to config file (tools > options > general > env vars).''' - jdk_path = str(adjust_jdk_path(jdk_path)) - env['JAVA_HOME'] = jdk_path # Python's process points to Thonny's JDK - jdk_path_entry = create_java_home_entry_from_path(jdk_path) - env_vars: set[str] = set(workbench.get_option('general.environment')) + jdk_path = str(adjust_jdk_path(jdk_path)) # Platform-adjusted path + env['JAVA_HOME'] = jdk_path # Python's process points to Thonny's JDK too - if jdk_path_entry not in env_vars: - entries = [*drop_all_java_home_entries(env_vars)] - entries.append(jdk_path_entry) - workbench.set_option('general.environment', entries) - showinfo('JAVA_HOME', jdk_path, parent=workbench) + java_home_entry = create_java_home_entry_from_path(jdk_path) + env_vars = dict.fromkeys(WORKBENCH.get_option('general.environment')) + + if java_home_entry not in env_vars: + entries = [ *drop_all_java_home_entries(env_vars), java_home_entry ] + WORKBENCH.set_option('general.environment', entries) + showinfo('JAVA_HOME', jdk_path, parent=WORKBENCH) def adjust_jdk_path(jdk_path: StrPath) -> PurePath: '''Adjust JDK path for the specificity of current platform.''' + jdk_path = PurePath(jdk_path) # if MacOS, append "/Contents/Home/" to form the actual JDK path for it: - if jdk.OS is jdk.OperatingSystem.MAC and jdk_path.parts[-1] != 'Home': + if jdk.OS is jdk.OperatingSystem.MAC and jdk_path.name != 'Home': jdk_path = jdk_path / 'Contents' / 'Home' return jdk_path @@ -131,7 +142,7 @@ def _non_java_home_predicate(entry: str) -> bool: def is_valid_jdk_version(jdk_version: str) -> bool: '''Check if JDK version meets minimum version requirement.''' - return jdk_version.isdigit() and int(jdk_version) >= _REQUIRE_JDK + return jdk_version.isdigit() and int(jdk_version) >= REQUIRE_JDK def is_valid_jdk_path(jdk_path: StrPath) -> bool: @@ -146,141 +157,85 @@ def get_all_thonny_folders() -> list[str]: return sorted((e.name for e in entries if e.is_dir()), reverse=True) -class DownloadJDK(Thread): - '''Background thread for downloading & installing JDK into Thonny's folder. - - - Removes any preexisting JDK folders matching the expected version. - - Downloads and extracts the required JDK version. - - Renames the downloaded folder to the expected format. - - Sets JAVA_HOME both in system environment and Thonny configuration. - ''' - def run(self): - '''Download and setup JDK (installs to Thonny's config directory)''' - # Delete existing Thonny's JDK subfolders matching jdk-: - self.process_match_jdk_dirs(shutil.rmtree) - - # Download and extract JDK subfolder into Thonny's user folder: - jdk.install(_VERSION_JDK, path=THONNY_USER_DIR) - - # Rename extracted Thonny's JDK subfolder to jdk-: - self.process_match_jdk_dirs(self.rename_folder, True) - - set_java_home(_JDK_HOME) # Add a Thonny's JAVA_HOME entry for it - - - @staticmethod - def process_match_jdk_dirs(action: PathAction, only_1st=False): - '''Apply an action to JDK-matching subfolders in Thonny's folder.''' - for path in DownloadJDK.get_all_thonny_folder_paths(): - if path.name.startswith(_JDK_DIR): # Folder name matches - action(path) # Callback to run on each matching folder path - if only_1st: break # Stop at 1st match occurrence - - - @staticmethod - def get_all_thonny_folder_paths() -> Iterator[Path]: - '''Find all subfolder paths within Thonny's user folder''' - return filter(Path.is_dir, _THONNY_USER_PATH.iterdir()) - - - @staticmethod - def rename_folder(path: StrPath): - '''Rename a JDK subfolder to the expected jdk- format.''' - rename(path, _JDK_PATH) - - - class JdkDialog(ui_utils.CommonDialog): '''User-facing dialog prompting install of required JDK for py5 sketches. - - Presents user with option to proceed or cancel the JDK installation. - Displays a horizontal indeterminate-sized progress bar during download. - Launches a background thread to handle installation tasks. - - Shows a success message when installation is complete. - ''' - _TITLE = tr('Install JDK ' + _VERSION_JDK + ' for py5') + - Shows a success message when installation is complete.''' - _PROGRESS = tr(f'Downloading and extracting JDK {_REQUIRE_JDK} ...') + _TITLE = tr('Install JDK ' + DOWNLOAD_JDK + ' for py5') + + _PROGRESS = tr('Downloading and extracting JDK ' + DOWNLOAD_JDK + ' ...') _OK, _CANCEL, _DONE = map(tr, ('Proceed', 'Cancel', 'JDK done')) - _MSG = 'JDK ' + _VERSION_JDK + tr(' extracted to ') + THONNY_USER_DIR + tr( + _MSG = 'JDK ' + DOWNLOAD_JDK + tr(' extracted to ') + THONNY_USER_DIR + tr( '\n\nYou can now run py5 sketches.') _INSTALL_JDK = tr( - "Thonny requires JDK " + _VERSION_JDK + " to run py5 sketches. " - "It'll need to download about 180 MB." - ) + "Thonny requires at least JDK " + VERSION_JDK + " to run py5 sketches. " + "It'll need to download about 180 MB.") - _PAD = 0, 15 + _PROGRESS_BAR_Y_PADDING = 0, 15 - def __init__(self, master=workbench, skip_diag_attribs=False, **kw): + def __init__(self, master=WORKBENCH, skip_diag_attribs=False, **kw): super().__init__(master, skip_diag_attribs, **kw) - # Window/Frame: - self.main_frame = ttk.Frame(self) - self.main_frame.grid(ipadx=15, ipady=15, sticky=tk.NSEW) - self.main_frame.rowconfigure(0, weight=1) - self.main_frame.columnconfigure(0, weight=1) + # Set dialog properties: title, fixed size, close button disabled: + self.title(self._TITLE) # Dialog title for JDK installation + self.resizable(height=tk.FALSE, width=tk.FALSE) # Prevent its resizing + self.protocol('WM_DELETE_WINDOW', '{#}') # Disable its close button - self.title(self._TITLE) - self.resizable(height=tk.FALSE, width=tk.FALSE) - self.protocol('WM_DELETE_WINDOW', '{#}') # Block window close button + # Window/Frame: + main_frame = self.main_frame = ttk.Frame(self) + main_frame.grid(ipadx=15, ipady=15, sticky=tk.NSEW) + main_frame.rowconfigure(0, weight=1) + main_frame.columnconfigure(0, weight=1) # Display install message: - message_label = ttk.Label(self.main_frame, text=self._INSTALL_JDK) + message_label = ttk.Label(main_frame, text=self._INSTALL_JDK) message_label.grid(pady=0, columnspan=2) - # OK button: - self.ok_button = ttk.Button( - self.main_frame, - text=self._OK, - command=self._proceed, - default=tk.ACTIVE - ) + # OK proceed button: + ok_button = self.ok_button = ttk.Button( + main_frame, + text=self._OK, + command=self._proceed, + default=tk.ACTIVE) - self.ok_button.grid( - row=2, column=0, - padx=15, pady=15, - sticky=tk.W - ) - - self.ok_button.focus_set() + ok_button.grid(row=2, column=0, padx=15, pady=15, sticky=tk.W) + ok_button.focus_set() # Cancel button: - self.cancel_button = ttk.Button( - self.main_frame, - text=self._CANCEL, - command=self._close - ) + cancel_button = self.cancel_button = ttk.Button( + main_frame, + text=self._CANCEL, + command=self._close) - self.cancel_button.grid( - row=2, column=1, - padx=15, pady=15, - sticky=tk.E - ) + cancel_button.grid(row=2, column=1, padx=15, pady=15, sticky=tk.E) - def _proceed(self): + def _proceed(self) -> None: '''Starts JDK downloader thread.''' + # Get rid of both OK & Cancel buttons: if self.ok_button: self.ok_button.destroy() if self.cancel_button: self.cancel_button.destroy() # Progress bar label: dl_label = ttk.Label(self.main_frame, text=self._PROGRESS) - dl_label.grid(row=1, columnspan=2, pady=self._PAD) + dl_label.grid(row=1, columnspan=2, pady=self._PROGRESS_BAR_Y_PADDING) # Progress bar: progress_bar = ttk.Progressbar(self.main_frame, mode='indeterminate') progress_bar.grid( row=2, column=0, columnspan=2, - padx=15, pady=self._PAD, - sticky=tk.EW - ) + padx=15, pady=self._PROGRESS_BAR_Y_PADDING, + sticky=tk.EW) - # Start progress bar animation and download thread: + # Start progress bar animation + download thread: if self.main_frame: self.main_frame.tkraise() download_thread = DownloadJDK() @@ -290,19 +245,66 @@ def _proceed(self): self._monitor(download_thread, progress_bar) - def _monitor(self, download: Thread, progress: ttk.Progressbar): + def _monitor(self, download: Thread, progress: ttk.Progressbar) -> None: '''Animate progress bar while JDK installs and extracts.''' + if download.is_alive(): self.after(100, lambda: self._monitor(download, progress)) return + # Destroy this JDK dialog instance once download has finished: progress.stop() self._close() - showinfo(self._DONE, self._MSG, parent=workbench) + showinfo(self._DONE, self._MSG, parent=WORKBENCH) - def _close(self): + def _close(self) -> None: '''Fully shutdown the JdkDialog instance.''' self.destroy() self.main_frame = self.ok_button = self.cancel_button = None + + + +class DownloadJDK(Thread): + '''Background thread for downloading & installing JDK into Thonny's folder. + - Removes any preexisting JDK folders matching the expected version. + - Downloads and extracts the required JDK version. + - Renames the downloaded folder to the expected format. + - Sets JAVA_HOME on Thonny configuration.''' + + def run(self) -> None: + '''Download and setup JDK (installs to Thonny's user directory)''' + + # Delete existing Thonny's JDK subfolders matching jdk-: + self.process_match_jdk_dirs(shutil.rmtree) + + # Download and extract JDK subfolder into Thonny's user folder: + jdk.install(DOWNLOAD_JDK, path=THONNY_USER_DIR) + + # Rename extracted Thonny's JDK subfolder to jdk-: + self.process_match_jdk_dirs(self.rename_folder, True) + + set_java_home(JDK_PATH) # Add a Thonny's JAVA_HOME entry for it + + + @staticmethod + def process_match_jdk_dirs(action: PathAction, only_1st=False) -> None: + '''Apply an action to JDK-matching subfolders in Thonny's folder.''' + + for path in DownloadJDK.get_all_thonny_folder_paths(): + if path.name.startswith(JDK_DIR): # Folder name matches + action(path) # Callback to run on each matching folder path + if only_1st: break # Stop at 1st match occurrence + + + @staticmethod + def get_all_thonny_folder_paths() -> Iterator[Path]: + '''Find all subfolder paths within Thonny's user folder''' + return filter(Path.is_dir, THONNY_USER_PATH.iterdir()) + + + @staticmethod + def rename_folder(path: StrPath) -> None: + '''Rename a JDK subfolder to the expected jdk- format.''' + rename(path, JDK_PATH)