Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0d672dd
FEAT: Install pyaedt via uv
eblanco-ansys Jun 30, 2025
85fd5b6
chore: adding changelog file 6338.added.md [dependabot-skip]
pyansys-ci-bot Jun 30, 2025
ed3c9d8
uv fix
tusharbana-ansys Jul 1, 2025
809dc9c
FIX: Updated pyaedtversion to the latest
eblanco-ansys Jul 1, 2025
56cbc2c
FIX: Reverted pyaedt version
eblanco-ansys Jul 1, 2025
4370b63
FIX: Increased timeout for UV
eblanco-ansys Jul 1, 2025
6a1e311
FIX: Removed extra whitespace
eblanco-ansys Jul 3, 2025
8575b4c
FIX: Applied suggested fixes
eblanco-ansys Jul 7, 2025
7818c01
Merge branch 'main' into 6317-update-the-pyaedt-installer-python-file…
eblanco-ansys Jul 8, 2025
d85ff06
Merge branch 'main' into 6317-update-the-pyaedt-installer-python-file…
eblanco-ansys Sep 15, 2025
b3ffdc0
Moved uv installation to online part
eblanco-ansys Sep 15, 2025
5cdac74
Added uv to "all" dependencies
eblanco-ansys Sep 15, 2025
ebf4ede
Merge branch 'main' into 6317-update-the-pyaedt-installer-python-file…
eblanco-ansys Sep 15, 2025
217bd47
chore: adding changelog file 6338.added.md [dependabot-skip]
pyansys-ci-bot Sep 15, 2025
b29d916
Use pip instead uv
eblanco-ansys Sep 16, 2025
d0e2dfc
Improved unzip funcion to handle nested cases and do it in temp folder
eblanco-ansys Sep 16, 2025
6f3ff32
Improved wheel argument handling
eblanco-ansys Sep 16, 2025
923d183
Install wheel using uv
eblanco-ansys Sep 16, 2025
4faaf86
Merge branch 'main' into 6317-update-the-pyaedt-installer-python-file…
eblanco-ansys Sep 16, 2025
5a06f03
Formatted file
eblanco-ansys Sep 16, 2025
e18fa6a
Added default timeout argument for slow connections
eblanco-ansys Sep 16, 2025
4408407
Reverted path functionality
eblanco-ansys Sep 17, 2025
43b28cd
Fixed bug in recursive zip extraction
eblanco-ansys Sep 17, 2025
28ef6ec
Added uv docs
eblanco-ansys Sep 17, 2025
aa2912d
Fixed words
eblanco-ansys Sep 17, 2025
d01cc8e
Merge branch 'main' into 6317-update-the-pyaedt-installer-python-file…
Samuelopez-ansys Sep 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/6338.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Install pyaedt via uv
71 changes: 71 additions & 0 deletions doc/source/Getting_started/Installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,77 @@ Finally, in the Python console, run the following commands:
add_pyaedt_to_aedt(“your_aedt_version", r“path_to_aedtlib", skip_version_manager=True)


Using uv to manage virtual environments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The project and the PyAEDT installer support using the `uv` tool to manage
package installation and speed up installs.
`uv` can be used inside a virtual environment to perform pip installs, to
install from local wheelhouses, and to improve reliability for long-running
package downloads.

You can use `uv` to install PyAEDT into your own virtual environment. The
steps below show how to create a virtual environment, activate it, install `uv`, and then
install PyAEDT. Examples are provided for Windows (PowerShell) and Linux (bash).

Create and activate a virtual environment (Windows - PowerShell)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: powershell

python -m venv C:\path\to\pyaedt_venv
C:\path\to\pyaedt_venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install uv
uv pip install pyaedt[all]

Create and activate a virtual environment (Linux)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: bash

python3 -m venv ~/pyaedt_venv
source ~/pyaedt_venv/bin/activate
python -m pip install --upgrade pip
pip install uv
uv pip install pyaedt[all]

.. note::
Virtual environments should be created with `venv` and not directly with `uv` to avoid potential issues.

Installing from a wheelhouse using uv
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to install from an offline wheelhouse, install `uv` and then use
it to perform an offline install from the wheelhouse directory. Example (Windows):

.. code:: powershell

pip install --no-cache-dir --no-index --find-links=file:///<path_to_wheelhouse> uv
uv pip install --no-cache-dir --no-index --find-links=file:///<path_to_wheelhouse> pyaedt[all]

Example (Linux):

.. code:: bash

pip install --no-cache-dir --no-index --find-links=file:///<path_to_wheelhouse> uv
uv pip install --no-cache-dir --no-index --find-links=file:///<path_to_wheelhouse> pyaedt[all]

After installation
~~~~~~~~~~~~~~~~~~
Once PyAEDT is installed in your virtual environment, you can run the
`add_pyaedt_to_aedt` helper to register the toolkits in AEDT (if applicable):

.. code:: python

from ansys.aedt.core.extensions.installer.pyaedt_installer import add_pyaedt_to_aedt
add_pyaedt_to_aedt("your_aedt_version", r"path_to_aedtlib")

Note
~~~~
- Using `uv` inside a virtual environment improves installation reliability and
supports installation from offline wheelhouses.
- If you manage a centralized installation or custom virtual environments, you
may choose to skip installing the Version Manager when linking PyAEDT into
AEDT (see `add_pyaedt_to_aedt` options).


Install PyAEDT in Conda virtual environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create virtual environment
Expand Down
169 changes: 136 additions & 33 deletions doc/source/Resources/pyaedt_installer_from_aedt.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# ruff: noqa: F821
#
# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates.
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
Expand Down Expand Up @@ -194,19 +194,80 @@ def parse_arguments_for_pyaedt_installer(args=None):


def unzip_if_zip(path):
"""Unzip path if it is a ZIP file."""
"""Unzip path if it is a ZIP file into a temporary directory.

If the zip contains nested zip files, extract them recursively until there
are no more zip files inside. Returns the deepest directory that contains
all the .whl files (common ancestor). If no .whl files are found but nested
zip extraction occurred, returns the last extracted folder. If the provided
path is not a zip file, the original Path is returned.
"""
import zipfile
import tempfile
import shutil
import os
from pathlib import Path

# Extracted folder
unzipped_path = path
if path.suffix == ".zip":
unzipped_path = path.parent / path.stem
if unzipped_path.exists():
shutil.rmtree(unzipped_path, ignore_errors=True)
with zipfile.ZipFile(path, "r") as zip_ref:
# Extract all contents to a directory. (You can specify a different extraction path if needed.)
zip_ref.extractall(unzipped_path)
return unzipped_path
path = Path(path)
# If not a zip file, return as-is
if not path.is_file() or path.suffix.lower() != ".zip":
return path

# Create a dedicated temporary directory for extraction
top_dir = Path(tempfile.mkdtemp(prefix="pyaedt_unzip_"))

# Extract the top-level zip into the temp dir
with zipfile.ZipFile(path, "r") as zip_ref:
zip_ref.extractall(top_dir)

last_unzipped = None

# Recursively find and extract any nested zip files until none remain
while True:
nested_zips = list(top_dir.rglob("*.zip"))
if not nested_zips:
break
# Sort to get deterministic behavior (optional)
nested_zips.sort()
for zpath in nested_zips:
try:
# Extract each nested zip into a folder next to the zip file
target_dir = zpath.with_suffix("")
if target_dir.exists():
shutil.rmtree(target_dir)
with zipfile.ZipFile(zpath, "r") as zf:
zf.extractall(target_dir)
last_unzipped = target_dir
finally:
# Remove the nested zip file so it won't be processed again
try:
zpath.unlink()
except Exception:
pass

# Find all wheel files under the extracted tree
wheels = list(top_dir.rglob("*.whl"))
if wheels:
# Compute the common ancestor of all wheel parent directories
parents = [str(w.parent) for w in wheels]
common = Path(os.path.commonpath(parents))
# If the common path is below top_dir, return it; otherwise return top_dir
try:
common = common.resolve()
top_dir_resolved = top_dir.resolve()
if str(common).startswith(str(top_dir_resolved)):
return common
except Exception:
# If resolution fails for any reason, fall back to returning top_dir
return top_dir
return top_dir

# If no wheels were found but we extracted nested zips, return the last extraction dir
if last_unzipped is not None and last_unzipped.exists():
return last_unzipped

# Otherwise return the top-level extraction directory
return top_dir


def install_pyaedt():
Expand All @@ -224,10 +285,14 @@ def install_pyaedt():
venv_dir = Path(VENV_DIR, python_version)
python_exe = venv_dir / "Scripts" / "python.exe"
pip_exe = venv_dir / "Scripts" / "pip.exe"
uv_exe = venv_dir / "Scripts" / "uv.exe"
activate_script = venv_dir / "Scripts" / "activate.bat"
else:
venv_dir = Path(VENV_DIR, python_version)
python_exe = venv_dir / "bin" / "python"
pip_exe = venv_dir / "bin" / "pip"
uv_exe = venv_dir / "bin" / "uv"
activate_script = venv_dir / "bin" / "activate"
os.environ["ANSYSEM_ROOT{}".format(args.version)] = args.edt_root
ld_library_path_dirs_to_add = [
r"{}/commonfiles/CPython/{}/linx64/Release/python/lib".format(
Expand All @@ -245,6 +310,11 @@ def install_pyaedt():
os.environ["TCL_LIBRARY"] = r"{}/commonfiles/CPython/{}/linx64/Release/python/lib/tcl8.5".format(
args.edt_root, args.python_version.replace(".", "_")
)
# Prepare environment for subprocess
env = os.environ.copy()
env["VIRTUAL_ENV"] = str(venv_dir)
env["PATH"] = str(venv_dir / "Scripts") + os.pathsep + env.get("PATH", "")
env["UV_HTTP_TIMEOUT"] = "1000" # Increase timeout for uv

if not venv_dir.exists():
print("Creating the virtual environment in {}".format(venv_dir))
Expand All @@ -254,63 +324,96 @@ def install_pyaedt():
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True) # nosec

if args.wheel and Path(args.wheel).exists():
print("Installing PyAEDT using provided wheels argument")
unzipped_path = unzip_if_zip(Path(args.wheel))
print("Installing uv using provided wheels argument")
command = [
str(pip_exe),
"install",
"--no-cache-dir",
"--no-index",
r"--find-links={}".format(str(unzipped_path)),
"uv",
]
subprocess.run(command, check=True, env=env) # nosec
print("Activating uv in the virtual environment...")
subprocess.run([str(activate_script)], check=True, env=env) # nosec
print("Installing PyAEDT using provided wheels argument")
command = [
str(uv_exe),
"pip",
"install",
"--no-cache-dir",
"--no-index",
r"--find-links={}".format(str(unzipped_path)),
]
if args.version <= "231":
command.append("pyaedt[all,dotnet]=='0.9.0'")
else:
command.append("pyaedt[all]")
subprocess.run(command, check=True) # nosec
subprocess.run(command, check=True, env=env) # nosec
else:
print("Installing PyAEDT using online sources")
subprocess.run([str(python_exe), "-m", "pip", "install", "--upgrade", "pip"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "wheel"], check=True) # nosec
# Install uv in the virtual environment
print("Installing uv in the virtual environment...")
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "uv"], check=True, env=env) # nosec

print("Installing PyAEDT using online sources with uv...")
subprocess.run([str(uv_exe), "pip", "install", "--upgrade", "pip"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "wheel"], check=True, env=env) # nosec
if args.version <= "231":
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]=='0.9.0'"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "jupyterlab"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipyvtklink"], check=True) # nosec
subprocess.run([str(uv_exe), "pip", "install", "pyaedt[all]=='0.9.0'"] , check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "jupyterlab"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "ipython", "-U"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "ipyvtklink"], check=True, env=env) # nosec
else:
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]"], check=True) # nosec
subprocess.run([str(uv_exe), "pip", "install", "pyaedt[all]"], check=True, env=env) # nosec

if args.version <= "231":
subprocess.run([str(pip_exe), "uninstall", "-y", "pywin32"], check=True) # nosec
subprocess.run([str(uv_exe), "pip", "uninstall", "-y", "pywin32"], check=True, env=env) # nosec

else:
print("Using existing virtual environment in {}".format(venv_dir))
subprocess.call([str(pip_exe), "uninstall", "-y", "pyaedt"]) # nosec


if args.wheel and Path(args.wheel).exists():
print("Installing PyAEDT using provided wheels argument")
unzipped_path = unzip_if_zip(Path(args.wheel))
print("Installing uv using provided wheels argument")
command = [
str(pip_exe),
"install",
"--no-cache-dir",
"--no-index",
r"--find-links={}".format(str(unzipped_path)),
"uv",
]
subprocess.run(command, check=True, env=env) # nosec
print("Activating uv in the virtual environment...")
subprocess.run([str(activate_script)], check=True, env=env) # nosec
print("Installing PyAEDT using provided wheels argument")
command = [
str(uv_exe),
"pip",
"install",
"--no-cache-dir",
"--no-index",
r"--find-links={}".format(str(unzipped_path)),
]
if args.version <= "231":
command.append("pyaedt[all,dotnet]=='0.9.0'")
else:
command.append("pyaedt[all]")
subprocess.run(command, check=True) # nosec
subprocess.run(command, check=True, env=env) # nosec
else:
print("Installing PyAEDT using online sources")
# Ensure uv is installed in the venv
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "uv"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "uninstall", "-y", "pyaedt"], check=True, env=env) # nosec

print("Installing PyAEDT using online sources with uv...")
if args.version <= "231":
subprocess.run([str(pip_exe), "pip=1000", "install", "pyaedt[all]=='0.9.0'"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "jupyterlab"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], check=True) # nosec
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipyvtklink"], check=True) # nosec
subprocess.run([str(uv_exe), "pip", "install", "pyaedt[all]=='0.9.0'"] , check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "jupyterlab"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "ipython", "-U"], check=True, env=env) # nosec
subprocess.run([str(uv_exe), "pip", "install", "ipyvtklink"], check=True, env=env) # nosec
else:
subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]"], check=True) # nosec
subprocess.run([str(uv_exe), "pip", "install", "pyaedt[all]"], check=True, env=env) # nosec
sys.exit(0)


Expand Down
2 changes: 2 additions & 0 deletions doc/styles/config/vocabularies/ANSYS/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,5 @@ solderballs
pin_mapping
refdes
gnd
uv
venv
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ all = [
"requests",
"fpdf2",
"rpyc>=6.0.0,<6.1",
"uv>=0.8.17",
]
examples = [
"imageio>=2.34.0,<2.38",
Expand Down
Loading