Skip to content

Commit c039bab

Browse files
eblanco-ansyspre-commit-ci[bot]pyansys-ci-bot
authored
FEAT: Update extension handling (#6758)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent af1cadc commit c039bab

15 files changed

+481
-196
lines changed

doc/changelog.d/6758.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update extension handling

src/ansys/aedt/core/extensions/customize_automation_tab.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,10 @@ def add_automation_tab(
142142
icon_file = Path(ansys.aedt.core.extensions.__file__).parent / "images" / "large" / "pyansys.png"
143143
else:
144144
icon_file = Path(icon_file)
145-
file_name = icon_file.name
146-
dest_dir = lib_dir / product / toolkit_name / "images" / "large"
147-
dest_file = dest_dir / file_name
148-
dest_dir.parent.mkdir(parents=True, exist_ok=True)
149-
dest_dir.mkdir(parents=True, exist_ok=True)
150-
shutil.copy(str(icon_file), str(dest_file))
151-
relative_image_path = dest_file.relative_to(lib_dir / product)
152145
button_kwargs = dict(
153146
label=name,
154147
isLarge="1",
155-
image=str(relative_image_path.as_posix()),
148+
image=str(icon_file.as_posix()),
156149
script=f"{toolkit_name}/{template}",
157150
)
158151
ET.SubElement(panel_element, "button", **button_kwargs)
@@ -404,8 +397,13 @@ def add_script_to_menu(
404397
build_file_data = build_file_data.replace("##JUPYTER_EXE##", str(jupyter_executable))
405398
build_file_data = build_file_data.replace("##TOOLKIT_NAME##", str(name))
406399
build_file_data = build_file_data.replace("##EXTENSION_TEMPLATES##", str(templates_dir))
407-
if dest_script_path:
408-
build_file_data = build_file_data.replace("##PYTHON_SCRIPT##", str(dest_script_path))
400+
if copy_to_personal_lib and dest_script_path:
401+
extension_dir = dest_script_path.parent
402+
else:
403+
extension_dir = Path(ansys.aedt.core.extensions.__file__).parent / "installer"
404+
build_file_data = build_file_data.replace("##BASE_EXTENSION_LOCATION##", str(extension_dir))
405+
if script_file:
406+
build_file_data = build_file_data.replace("##PYTHON_SCRIPT##", str(os.path.basename(script_file)))
409407
if version_agnostic:
410408
build_file_data = build_file_data.replace(" % version", "")
411409
with open(tool_dir / (template_file + ".py"), "w") as out_file:

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

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ def launch_extension(self, category: str, option: str):
637637
script_field = None
638638
option_label = option
639639
logger = logging.getLogger("Global")
640-
if option not in self.toolkits.get(self.current_category, {}) and option != "Custom":
640+
if option not in self.toolkits.get(self.current_category, {}) and option != "Custom": # pragma: no cover
641641
toolkit_dir = Path(self.desktop.personallib) / "Toolkits"
642642
xml_dir = toolkit_dir / self.current_category
643643
tabconfig_path = xml_dir / "TabConfig.xml"
@@ -673,21 +673,33 @@ def launch_extension(self, category: str, option: str):
673673
return
674674
icon = EXTENSIONS_PATH / "images" / "large" / "pyansys.png"
675675
if is_custom and script_field is None:
676-
add_script_to_menu(
677-
name=option_label,
678-
script_file=str(script_file),
679-
product=category,
680-
executable_interpreter=sys.executable,
681-
personal_lib=self.desktop.personallib,
682-
aedt_version=self.desktop.aedt_version_id,
683-
copy_to_personal_lib=False,
684-
icon_file=str(icon),
685-
is_custom=is_custom
686-
)
676+
try:
677+
add_script_to_menu(
678+
name=option_label,
679+
script_file=str(script_file),
680+
product=category,
681+
executable_interpreter=sys.executable,
682+
personal_lib=self.desktop.personallib,
683+
aedt_version=self.desktop.aedt_version_id,
684+
copy_to_personal_lib=True,
685+
icon_file=str(icon),
686+
is_custom=True,
687+
)
688+
except Exception as e:
689+
self.desktop.logger.error(
690+
"Failed to install custom extension %s: %s",
691+
option_label,
692+
e,
693+
)
694+
messagebox.showerror("Error", f"Failed to pin custom extension: {e}")
695+
return
696+
687697
# Refresh the custom extensions
688698
self.load_extensions(category)
689-
self.desktop.logger.info(f"Extension {option_label} pinned successfully. If the extension is not visible,"
690-
f" create a new AEDT session or create a new project.")
699+
self.desktop.logger.info(
700+
"Extension %s pinned successfully. If the extension is not visible, create a new AEDT session or create a new project.",
701+
option_label,
702+
)
691703

692704
# if hasattr(self.desktop, "odesktop"):
693705
# self.desktop.odesktop.RefreshToolkitUI()
@@ -916,6 +928,30 @@ def pin_extension(self, category: str, option: str):
916928
icon = (
917929
EXTENSIONS_PATH / "images" / "large" / "pyansys.png"
918930
)
931+
# If the user selected a script, copy it into personal lib Toolkits/<product>/<option>/Lib
932+
if not script_file:
933+
self.desktop.logger.info("No script selected for custom extension. Aborting pin.")
934+
return
935+
try:
936+
add_script_to_menu(
937+
name=option,
938+
script_file=str(script_file),
939+
product=category,
940+
executable_interpreter=sys.executable,
941+
personal_lib=self.desktop.personallib,
942+
aedt_version=self.desktop.aedt_version_id,
943+
copy_to_personal_lib=True,
944+
icon_file=str(icon),
945+
is_custom=True,
946+
)
947+
msg = (f"Extension {option} pinned successfully.\n"
948+
f"If the extension is not visible create a new AEDT session or create a new project.")
949+
self.desktop.logger.info(msg)
950+
self.log_message(msg)
951+
except Exception as e:
952+
self.desktop.logger.error(f"Failed to pin custom extension {option}: {e}")
953+
messagebox.showerror("Error", f"Failed to pin custom extension: {e}")
954+
return
919955
else:
920956
if self.toolkits[self.current_category][option].get("script", None):
921957
script_file = (
@@ -1293,7 +1329,7 @@ def check_extension_pinned(self, category: str, option: str):
12931329
bool
12941330
True if the extension is pinned, False otherwise.
12951331
"""
1296-
if option.lower() == "custom":
1332+
if option.lower() == "custom": # pragma: no cover
12971333
return False # Custom extensions are not tracked
12981334

12991335
try:
@@ -1340,11 +1376,11 @@ def launch_web_url(self, category: str, option: str):
13401376
def check_for_pyaedt_update_on_startup(self):
13411377
"""Spawn a background thread to check PyPI for a newer PyAEDT release.
13421378
"""
1343-
def worker():
1379+
def worker(): # pragma: no cover
13441380
log = logging.getLogger("Global")
13451381
try:
13461382
latest, declined_file = check_for_pyaedt_update(self.desktop.personallib)
1347-
if not latest: # pragma: no cover
1383+
if not latest:
13481384
log.debug("PyAEDT update check: no prompt required or latest unavailable.")
13491385
return
13501386
try:
@@ -1354,7 +1390,7 @@ def worker():
13541390
)
13551391
except Exception:
13561392
log.debug("PyAEDT update check: failed to schedule popup.", exc_info=True)
1357-
except Exception: # pragma: no cover
1393+
except Exception:
13581394
log.debug("PyAEDT update check: worker failed.", exc_info=True)
13591395

13601396
threading.Thread(target=worker, daemon=True).start()
@@ -1423,11 +1459,12 @@ def open_changelog():
14231459

14241460
def decline():
14251461
try:
1426-
declined_file_path.parent.mkdir(
1427-
parents=True, exist_ok=True
1462+
from ansys.aedt.core.extensions.misc import (
1463+
decline_pyaedt_update,
14281464
)
1429-
declined_file_path.write_text(
1430-
latest_version, encoding="utf-8"
1465+
1466+
decline_pyaedt_update(
1467+
declined_file_path, latest_version
14311468
)
14321469
except Exception:
14331470
logging.getLogger("Global").debug(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def __add_pyaedt_tabs(personal_lib, aedt_version, skip_version_manager):
8989
template_name,
9090
icon_file=icon_file,
9191
product="Project",
92-
copy_to_personal_lib=True,
92+
copy_to_personal_lib=False,
9393
executable_interpreter=None,
9494
panel="Panel_PyAEDT_Installer",
9595
personal_lib=personal_lib,

src/ansys/aedt/core/extensions/misc.py

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def compare_versions(local: str, remote: str) -> bool:
118118

119119
def to_tuple(v: str):
120120
out = []
121-
for token in v.split("."):
121+
# Remove dev/rc suffixes and split by dots
122+
version_clean = v.split("dev")[0].split("rc")[0]
123+
for token in version_clean.split("."):
122124
try:
123125
out.append(int(token))
124126
except Exception: # pragma: no cover
@@ -130,8 +132,49 @@ def to_tuple(v: str):
130132
except Exception: # pragma: no cover
131133
return False
132134

135+
def read_version_file(file_path: Path) -> Tuple[Optional[str], bool]:
136+
"""Read version file and return (last_known_version, show_updates).
137+
138+
File format:
139+
Line 1: last known version
140+
Line 2: show_updates preference ("true" or "false")
141+
"""
142+
if not file_path.is_file():
143+
return None, True # First start - don't show popup
144+
145+
try:
146+
content = file_path.read_text(encoding="utf-8").strip()
147+
lines = content.split("\n")
148+
if len(lines) >= 2:
149+
last_version = lines[0].strip()
150+
show_updates = lines[1].strip().lower() == "true"
151+
return last_version, show_updates
152+
elif len(lines) == 1:
153+
# Legacy format - only version, assume user wants updates
154+
return lines[0].strip(), True
155+
else: # pragma: no cover
156+
return None, True
157+
except Exception: # pragma: no cover
158+
return None, True
159+
160+
def write_version_file(file_path: Path, version: str, show_updates: bool):
161+
"""Write version and preference to file."""
162+
try:
163+
file_path.parent.mkdir(parents=True, exist_ok=True)
164+
content = f"{version}\n{str(show_updates).lower()}"
165+
file_path.write_text(content, encoding="utf-8")
166+
except Exception: # pragma: no cover
167+
log.debug("PyAEDT update check: failed to write version file.", exc_info=True)
168+
133169
log = logging.getLogger("Global")
134170

171+
# Get current PyAEDT version
172+
try:
173+
from ansys.aedt.core import __version__ as current_version
174+
except ImportError: # pragma: no cover
175+
log.debug("PyAEDT update check: could not import version.")
176+
return None, None
177+
135178
latest = get_latest_version("pyaedt")
136179
if not latest or latest == "Unknown":
137180
log.debug("PyAEDT update check: latest version unavailable.")
@@ -144,20 +187,36 @@ def to_tuple(v: str):
144187
log.debug("PyAEDT update check: personal lib path not found.", exc_info=True)
145188
return None, None
146189

147-
declined_file = toolkit_dir / ".pyaedt_version"
148-
declined_version = None
149-
if declined_file.is_file():
150-
try:
151-
declined_version = declined_file.read_text(encoding="utf-8").strip()
152-
except Exception: # pragma: no cover
153-
declined_version = None
190+
version_file = toolkit_dir / ".pyaedt_version"
191+
last_known_version, show_updates = read_version_file(version_file)
192+
193+
# If this is first start (no file exists), record the latest known release
194+
# (not the installed version) so we won't prompt until a newer release appears.
195+
if last_known_version is None:
196+
write_version_file(version_file, latest, False)
197+
log.debug("PyAEDT update check: first start, recording latest release.")
198+
return None, None
199+
200+
# If the user already has the latest version installed, never show the popup.
201+
if current_version == latest: # pragma: no cover
202+
if last_known_version != latest:
203+
write_version_file(version_file, latest, False)
204+
return None, None
205+
206+
# Check if there's a newer version available compared to installed package
207+
has_newer_version = compare_versions(current_version, latest)
154208

155-
prompt_user = declined_version is None or compare_versions(declined_version, latest)
209+
if not has_newer_version:
210+
if last_known_version != latest:
211+
write_version_file(version_file, latest, show_updates)
212+
return None, None
213+
version_changed = compare_versions(last_known_version, latest)
214+
prompt_user = show_updates or version_changed
156215

157216
if not prompt_user:
158217
return None, None
159218

160-
return latest, declined_file
219+
return latest, version_file
161220

162221

163222
@dataclass
@@ -1043,3 +1102,15 @@ def hide_tooltip(self): # pragma: no cover
10431102
self.tipwindow = None
10441103
if tw:
10451104
tw.destroy()
1105+
1106+
1107+
def decline_pyaedt_update(declined_file_path: Path, latest_version: str):
1108+
"""Record that the user declined the update notification."""
1109+
try:
1110+
declined_file_path.parent.mkdir(parents=True, exist_ok=True)
1111+
if latest_version is None:
1112+
return
1113+
content = f"{latest_version}\nfalse"
1114+
declined_file_path.write_text(content, encoding="utf-8")
1115+
except Exception: # pragma: no cover
1116+
logging.getLogger("Global").debug("Failed to write declined update file", exc_info=True)

src/ansys/aedt/core/extensions/templates/jupyter.py_build

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,12 @@ def main():
4848
proj_dir = oDesktop.GetProjectDirectory()
4949
os.chdir(proj_dir)
5050
# Extension directory
51-
current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
52-
pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##"))
51+
base_extensions_loc = r"##BASE_EXTENSION_LOCATION##"
5352
# Jupyter interpreter
5453
jupyter_exe = r"##JUPYTER_EXE##" % version
5554
# Check if Jupyter version and AEDT release match
5655
python_exe = pyaedt_utils.sanitize_interpreter_path(jupyter_exe, version_short)
57-
template = os.path.join(pyaedt_toolkit_dir, "jupyter_template.ipynb")
56+
template = os.path.join(base_extensions_loc, "jupyter_template.ipynb")
5857
target = os.path.join(proj_dir, pyaedt_utils.generate_unique_name("pyaedt", ".ipynb", n=3))
5958
# Check executable
6059
jupyter_exe_flag = pyaedt_utils.check_file(jupyter_exe, oDesktop)
@@ -86,7 +85,7 @@ def main():
8685
subprocess.Popen(command)
8786
else:
8887
if notebook_dir:
89-
command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target),"--notebook-dir",'"{0}"'.format(notebook_dir)]
88+
command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target), "--notebook-dir", '"{0}"'.format(notebook_dir)]
9089
else:
9190
command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target)]
9291
subprocess.Popen(" ".join(command))

src/ansys/aedt/core/extensions/templates/pyaedt_console.py_build

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,14 @@ import pyaedt_utils
4545
def main():
4646
# Get AEDT version
4747
version_short = oDesktop.GetVersion()[2:6].replace(".", "")
48-
# Extension directory
49-
current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
50-
pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##"))
48+
# Base extensions location
49+
base_extensions_loc = r"##BASE_EXTENSION_LOCATION##"
5150
# CPython interpreter
5251
python_exe = r"##IPYTHON_EXE##" % version
5352
# Check if CPython interpreter and AEDT release match
5453
python_exe = pyaedt_utils.sanitize_interpreter_path(python_exe, version_short)
5554
# Console launcher
56-
pyaedt_script = os.path.join(pyaedt_toolkit_dir, "console_setup.py")
55+
pyaedt_script = os.path.normpath(os.path.join(base_extensions_loc, "console_setup.py"))
5756
# Check python executable
5857
python_exe_flag = pyaedt_utils.check_file(python_exe, oDesktop)
5958
if not python_exe_flag:
@@ -70,22 +69,10 @@ def main():
7069
if not command:
7170
pyaedt_utils.show_error("No terminal found on system.", oDesktop)
7271
pyaedt_utils.set_ansys_em_environment(oDesktop)
73-
command.extend([
74-
python_exe,
75-
"-i",
76-
pyaedt_script,
77-
str(oDesktop.GetProcessID()),
78-
str(oDesktop.GetVersion()[:6]),
79-
])
72+
command.extend([python_exe, "-i", pyaedt_script, str(oDesktop.GetProcessID()), str(oDesktop.GetVersion()[:6])])
8073
subprocess.Popen(command)
8174
else:
82-
command = [
83-
'"{}"'.format(python_exe),
84-
"-i",
85-
'"{}"'.format(pyaedt_script),
86-
str(oDesktop.GetProcessID()),
87-
str(oDesktop.GetVersion()[:6]),
88-
]
75+
command = ['"{}"'.format(python_exe), "-i", '"{}"'.format(pyaedt_script), str(oDesktop.GetProcessID()), str(oDesktop.GetVersion()[:6])]
8976
subprocess.Popen(" ".join(command))
9077

9178

0 commit comments

Comments
 (0)