3434from tkinter import ttk
3535import webbrowser
3636import zipfile
37+ from ansys .aedt .core .generic .general_methods import is_linux
3738
3839import defusedxml
3940import PIL .Image
4041import PIL .ImageTk
41- import pyedb
4242import requests
4343
4444import ansys .aedt .core
4545from ansys .aedt .core .extensions .misc import get_aedt_version
4646from ansys .aedt .core .extensions .misc import get_port
4747from ansys .aedt .core .extensions .misc import get_process_id
4848
49- is_linux = os .name == "posix"
50- is_windows = not is_linux
5149
5250defusedxml .defuse_stdlib ()
5351
@@ -88,7 +86,17 @@ def venv_path(self):
8886
8987 @property
9088 def python_exe (self ):
91- return os .path .join (self .venv_path , "Scripts" , "python.exe" )
89+ # Use the venv "Scripts" on Windows and "bin" on POSIX; choose platform-appropriate executable name.
90+ bin_dir = "Scripts" if self .is_windows else "bin"
91+ exe_name = "python.exe" if self .is_windows else "python"
92+ return os .path .join (self .venv_path , bin_dir , exe_name )
93+
94+ @property
95+ def uv_exe (self ):
96+ # 'uv' is named 'uv.exe' on Windows, 'uv' on POSIX and lives in the venv scripts/bin dir.
97+ bin_dir = "Scripts" if self .is_windows else "bin"
98+ uv_name = "uv.exe" if self .is_windows else "uv"
99+ return os .path .join (self .venv_path , bin_dir , uv_name )
92100
93101 @property
94102 def python_version (self ):
@@ -97,18 +105,20 @@ def python_version(self):
97105
98106 @property
99107 def pyaedt_version (self ):
100- return ansys . aedt . core . version
108+ return self . get_installed_version ( "pyaedt" )
101109
102110 @property
103111 def pyedb_version (self ):
104- return pyedb . version
112+ return self . get_installed_version ( "pyedb" )
105113
106114 def __init__ (self , ui , desktop , aedt_version , personal_lib ):
107115 from ansys .aedt .core .extensions .misc import ExtensionTheme
108116
109117 self .desktop = desktop
110118 self .aedt_version = aedt_version
111119 self .personal_lib = personal_lib
120+ self .is_linux = is_linux
121+ self .is_windows = not is_linux
112122 self .change_theme_button = None
113123
114124 # Configure style for ttk buttons
@@ -130,6 +140,18 @@ def __init__(self, ui, desktop, aedt_version, personal_lib):
130140
131141 self .ini_file_path = os .path .join (os .path .dirname (__file__ ), "settings.ini" )
132142
143+ # Prepare subprocess environment so the venv is effectively activated for all runs
144+ # This prepends the venv Scripts (Windows) / bin (POSIX) directory to PATH and
145+ # sets VIRTUAL_ENV so subprocesses use the correct interpreter/tools (uv, pip, etc.).
146+ self .activated_env = None
147+ self .activate_venv ()
148+
149+ # Install uv if not present
150+ if "PYTEST_CURRENT_TEST" not in os .environ : # pragma: no cover
151+ if not os .path .exists (self .uv_exe ):
152+ print ("Installing uv..." )
153+ subprocess .run ([self .python_exe , "-m" , "pip" , "install" , "uv" ], check = True , env = self .activated_env ) # nosec
154+
133155 # Load the logo for the main window
134156 icon_path = Path (ansys .aedt .core .extensions .__path__ [0 ]) / "images" / "large" / "logo.png"
135157 im = PIL .Image .open (icon_path )
@@ -168,12 +190,12 @@ def toggle_theme(self):
168190 self .theme_color = "light"
169191
170192 def set_light_theme (self ):
171- root .configure (bg = self .theme .light ["widget_bg" ])
193+ self . root .configure (bg = self .theme .light ["widget_bg" ])
172194 self .theme .apply_light_theme (self .style )
173195 self .change_theme_button .config (text = "\u263d " )
174196
175197 def set_dark_theme (self ):
176- root .configure (bg = self .theme .dark ["widget_bg" ])
198+ self . root .configure (bg = self .theme .dark ["widget_bg" ])
177199 self .theme .apply_dark_theme (self .style )
178200 self .change_theme_button .config (text = "\u2600 " )
179201
@@ -292,6 +314,29 @@ def is_git_available():
292314 messagebox .showerror ("Error: Git Not Found" , "Git does not seem to be installed or is not accessible." )
293315 return res
294316
317+ def activate_venv (self ):
318+ """Prepare a subprocess environment that has the virtual environment activated.
319+
320+ This function does not change the current Python process, but prepares an env
321+ dictionary (stored in self.activated_env) that can be passed to subprocess.run
322+ so that commands like uv and pip resolve to the ones inside the virtualenv.
323+ """
324+ try :
325+ scripts_dir = (
326+ os .path .join (self .venv_path , "Scripts" ) if self .is_windows else os .path .join (self .venv_path , "bin" )
327+ )
328+ env = os .environ .copy ()
329+ # Prepend venv scripts/bin to PATH so executables from the venv are preferred
330+ env ["PATH" ] = scripts_dir + os .pathsep + env .get ("PATH" , "" )
331+ # Mark the virtual environment
332+ env ["VIRTUAL_ENV" ] = self .venv_path
333+ # Unset PYTHONHOME if set to avoid mixing environments
334+ env .pop ("PYTHONHOME" , None )
335+ self .activated_env = env
336+ except Exception : # pragma: no cover
337+ # Fallback to the current environment to avoid breaking functionality
338+ self .activated_env = os .environ .copy ()
339+
295340 def update_pyaedt (self ):
296341 response = messagebox .askyesno ("Disclaimer" , DISCLAIMER )
297342
@@ -304,9 +349,9 @@ def update_pyaedt(self):
304349 return
305350
306351 if self .pyaedt_version > latest_version :
307- subprocess .run ([self .python_exe , "-m" , " pip" , "install" , f"pyaedt=={ latest_version } " ], check = True ) # nosec
352+ subprocess .run ([self .uv_exe , "pip" , "install" , f"pyaedt=={ latest_version } " ], check = True , env = self . activated_env ) # nosec
308353 else :
309- subprocess .run ([self .python_exe , "-m" , " pip" , "install" , "-U" , "pyaedt" ], check = True ) # nosec
354+ subprocess .run ([self .uv_exe , "pip" , "install" , "-U" , "pyaedt" ], check = True , env = self . activated_env ) # nosec
310355
311356 self .clicked_refresh (need_restart = True )
312357
@@ -323,9 +368,17 @@ def update_pyedb(self):
323368 return
324369
325370 if self .pyedb_version > latest_version :
326- subprocess .run ([self .python_exe , "-m" , "pip" , "install" , f"pyedb=={ latest_version } " ], check = True ) # nosec
371+ subprocess .run (
372+ [self .uv_exe , "pip" , "install" , f"pyedb=={ latest_version } " ],
373+ check = True ,
374+ env = self .activated_env ,
375+ ) # nosec
327376 else :
328- subprocess .run ([self .python_exe , "-m" , "pip" , "install" , "-U" , "pyedb" ], check = True ) # nosec
377+ subprocess .run (
378+ [self .uv_exe , "pip" , "install" , "-U" , "pyedb" ],
379+ check = True ,
380+ env = self .activated_env ,
381+ ) # nosec
329382
330383 print ("Pyedb has been updated" )
331384 self .clicked_refresh (need_restart = True )
@@ -339,8 +392,14 @@ def get_pyaedt_branch(self):
339392 if response :
340393 branch_name = self .pyaedt_branch_name .get ()
341394 subprocess .run (
342- [self .python_exe , "-m" , "pip" , "install" , f"git+https://github.com/ansys/pyaedt.git@{ branch_name } " ],
395+ [
396+ self .uv_exe ,
397+ "pip" ,
398+ "install" ,
399+ f"git+https://github.com/ansys/pyaedt.git@{ branch_name } " ,
400+ ],
343401 check = True ,
402+ env = self .activated_env ,
344403 ) # nosec
345404 self .clicked_refresh (need_restart = True )
346405
@@ -353,8 +412,14 @@ def get_pyedb_branch(self):
353412 if response :
354413 branch_name = self .pyedb_branch_name .get ()
355414 subprocess .run (
356- [self .python_exe , "-m" , "pip" , "install" , f"git+https://github.com/ansys/pyedb.git@{ branch_name } " ],
415+ [
416+ self .uv_exe ,
417+ "pip" ,
418+ "install" ,
419+ f"git+https://github.com/ansys/pyedb.git@{ branch_name } " ,
420+ ],
357421 check = True ,
422+ env = self .activated_env ,
358423 ) # nosec
359424 self .clicked_refresh (need_restart = True )
360425
@@ -402,7 +467,7 @@ def version_is_leq(version, other_version):
402467
403468 # Check OS
404469 if os_system == "windows" :
405- if not is_windows :
470+ if not self . is_windows :
406471 msg .extend (["" , "This wheelhouse is not compatible with your operating system." ])
407472 correct_wheelhouse = correct_wheelhouse .replace (f"-{ os_system } -" , "-windows-" )
408473 else :
@@ -427,28 +492,21 @@ def version_is_leq(version, other_version):
427492
428493 subprocess .run (
429494 [
430- self .python_exe ,
431- "-m" ,
495+ self .uv_exe ,
432496 "pip" ,
433497 "install" ,
434498 "--force-reinstall" ,
435499 "--no-cache-dir" ,
436500 "--no-index" ,
437- f"--find-links=file:/// { str ( unzipped_path )} " ,
501+ f"--find-links={ unzipped_path . as_uri ( )} " ,
438502 "pyaedt[all]" ,
439503 ],
440504 check = True ,
505+ env = self .activated_env ,
441506 ) # nosec
442507 self .clicked_refresh (need_restart = True )
443508
444509 def reset_pyaedt_buttons_in_aedt (self ):
445- def handle_remove_error (func , path , exc_info ):
446- # Attempt to fix permission issues
447- import stat
448-
449- os .chmod (path , stat .S_IWRITE ) # Add write permission
450- func (path ) # Retry the operation
451-
452510 response = messagebox .askyesno ("Confirm Action" , "Are you sure you want to proceed?" )
453511
454512 if response :
@@ -457,6 +515,28 @@ def handle_remove_error(func, path, exc_info):
457515 add_pyaedt_to_aedt (self .aedt_version , self .personal_lib )
458516 messagebox .showinfo ("Success" , "PyAEDT panels updated in AEDT." )
459517
518+ def get_installed_version (self , package_name ):
519+ """Return the installed version of package_name inside the virtualenv.
520+
521+ This runs the venv Python to query the package metadata so we can show
522+ the updated version without restarting the current process.
523+ """
524+ try :
525+ # Prefer importlib.metadata (Python 3.8+). Use venv python to inspect
526+ cmd = [self .python_exe , "-c" , "import importlib.metadata as m; print(m.version(\" %s\" ))" % package_name ]
527+ out = subprocess .check_output (cmd , env = self .activated_env , stderr = subprocess .DEVNULL , text = True ) # nosec
528+ return out .strip ()
529+ except Exception :
530+ try :
531+ # Fallback to 'pip show' and parse Version
532+ cmd = [self .uv_exe , "pip" , "show" , package_name ]
533+ out = subprocess .check_output (cmd , env = self .activated_env , stderr = subprocess .DEVNULL , text = True ) # nosec
534+ for line in out .splitlines ():
535+ if line .startswith ("Version:" ):
536+ return line .split (":" , 1 )[1 ].strip ()
537+ except Exception : # pragma: no cover
538+ return "Please restart"
539+
460540 def clicked_refresh (self , need_restart = False ):
461541 msg = [f"Venv path: { self .venv_path } " , f"Python version: { self .python_version } " ]
462542 msg = "\n " .join (msg )
@@ -466,8 +546,23 @@ def clicked_refresh(self, need_restart=False):
466546 self .pyaedt_info .set (f"PyAEDT: { self .pyaedt_version } (Latest { get_latest_version ('pyaedt' )} )" )
467547 self .pyedb_info .set (f"PyEDB: { self .pyedb_version } (Latest { get_latest_version ('pyedb' )} )" )
468548 else :
469- self .pyaedt_info .set (f"PyAEDT: { 'Please restart' } " )
470- self .pyedb_info .set (f"PyEDB: { 'Please restart' } " )
549+ # Try to detect the newly installed versions inside the venv so we can
550+ # display the updated version immediately without forcing a restart.
551+ try :
552+ pyaedt_installed = self .get_installed_version ("pyaedt" )
553+ except Exception : # pragma: no cover
554+ pyaedt_installed = "Please restart"
555+
556+ try :
557+ pyedb_installed = self .get_installed_version ("pyedb" )
558+ except Exception : # pragma: no cover
559+ pyedb_installed = "Please restart"
560+
561+ latest_pyaedt = get_latest_version ("pyaedt" )
562+ latest_pyedb = get_latest_version ("pyedb" )
563+
564+ self .pyaedt_info .set (f"PyAEDT: { pyaedt_installed } (Latest { latest_pyaedt } )" )
565+ self .pyedb_info .set (f"PyEDB: { pyedb_installed } (Latest { latest_pyedb } )" )
471566 messagebox .showinfo ("Message" , "Done" )
472567
473568
@@ -476,7 +571,7 @@ def get_desktop_info(release_desktop=True):
476571 aedt_version = get_aedt_version ()
477572 aedt_process_id = get_process_id ()
478573
479- if aedt_process_id is not None :
574+ if aedt_process_id is not None : # pragma: no cover
480575 new_desktop = False
481576 ng = False
482577 close_on_exit = False
@@ -487,6 +582,7 @@ def get_desktop_info(release_desktop=True):
487582
488583 aedtapp = ansys .aedt .core .Desktop (new_desktop = new_desktop , version = aedt_version , port = port , non_graphical = ng )
489584 personal_lib = aedtapp .personallib
585+
490586 if release_desktop :
491587 if close_on_exit :
492588 aedtapp .close_desktop ()
@@ -496,7 +592,7 @@ def get_desktop_info(release_desktop=True):
496592 return {"desktop" : aedtapp , "aedt_version" : aedt_version , "personal_lib" : personal_lib }
497593
498594
499- if __name__ == "__main__" :
595+ if __name__ == "__main__" : # pragma: no cover
500596 kwargs = get_desktop_info ()
501597 # Initialize tkinter root window and run the app
502598 root = tkinter .Tk ()
0 commit comments