Skip to content

Commit fd6f543

Browse files
eblanco-ansyspyansys-ci-botpre-commit-ci[bot]SMoraisAnsys
authored
FEAT: version-manager-uv-support (#6655)
Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sébastien Morais <[email protected]>
1 parent 62eb0f3 commit fd6f543

File tree

4 files changed

+629
-29
lines changed

4 files changed

+629
-29
lines changed

doc/changelog.d/6655.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Version-manager-uv-support

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ unit-tests = [
5050
"pytest-cov>=4.0.0,<7.1",
5151
"mock>=5.1.0,<5.3",
5252
"fpdf2",
53+
"requests",
5354
]
5455
integration-tests = [
5556
"matplotlib>=3.5.0,<3.11",

src/ansys/aedt/core/extensions/installer/version_manager.py

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,18 @@
3434
from tkinter import ttk
3535
import webbrowser
3636
import zipfile
37+
from ansys.aedt.core.generic.general_methods import is_linux
3738

3839
import defusedxml
3940
import PIL.Image
4041
import PIL.ImageTk
41-
import pyedb
4242
import requests
4343

4444
import ansys.aedt.core
4545
from ansys.aedt.core.extensions.misc import get_aedt_version
4646
from ansys.aedt.core.extensions.misc import get_port
4747
from ansys.aedt.core.extensions.misc import get_process_id
4848

49-
is_linux = os.name == "posix"
50-
is_windows = not is_linux
5149

5250
defusedxml.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

Comments
 (0)