Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: launch_gui command #882

Merged
merged 42 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
db6cb11
launch gui on save for specified file
klmcadams Aug 20, 2024
d5d5b01
comments on how to implement this
klmcadams Aug 21, 2024
ca19122
first draft of launching the gui
klmcadams Aug 26, 2024
83536ec
remove comments
klmcadams Aug 26, 2024
e8ae3cf
Merge branch 'main' into feat/gui-on-load
klmcadams Aug 26, 2024
e7f37ec
chore: adding changelog file 882.added.md
pyansys-ci-bot Aug 26, 2024
cb468d5
Merge branch 'main' into feat/gui-on-load
klmcadams Aug 27, 2024
5ededcb
wip
Sep 5, 2024
80520ca
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 5, 2024
76e1a13
first draft of launch_ui function
klmcadams Sep 10, 2024
04f741f
use tempfile with name formatted
klmcadams Sep 10, 2024
a0d64a9
save
Sep 10, 2024
14e1eb1
suggestions
Sep 10, 2024
684ba90
suggestions
Sep 10, 2024
ac53baa
update UILauncher class
klmcadams Sep 10, 2024
1e1cd53
give option to delete temporary mechdb file & folder on close
klmcadams Sep 11, 2024
8b55c6a
Merge branch 'main' into feat/gui-on-load
klmcadams Sep 11, 2024
118d4d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 11, 2024
cab4089
add cleanup gui test
klmcadams Sep 12, 2024
9160fa2
Merge branch 'feat/gui-on-load' of https://github.com/ansys/pymechani…
klmcadams Sep 12, 2024
da5ae3b
sleep 1
klmcadams Sep 12, 2024
55498a9
add more comments and remove os
klmcadams Sep 12, 2024
f3e3c75
add psutil as test dep requirement
klmcadams Sep 12, 2024
76c20cb
move tempfile cleanup to embedding_scripts
klmcadams Sep 12, 2024
900c4e4
adjust run to return process
klmcadams Sep 12, 2024
f1b34ad
Merge branch 'main' into feat/gui-on-load
klmcadams Sep 12, 2024
a7eaafb
fix run_subprocess equals
klmcadams Sep 12, 2024
909eec2
Merge branch 'feat/gui-on-load' of https://github.com/ansys/pymechani…
klmcadams Sep 12, 2024
d9693f4
fix more lines
klmcadams Sep 12, 2024
c1b7813
try to test launch_gui directly
klmcadams Sep 12, 2024
1e147ac
open string not path
klmcadams Sep 12, 2024
78e3a29
fix assert check
klmcadams Sep 12, 2024
dd13a68
remove period
klmcadams Sep 12, 2024
735f47b
add pragma no cover lines & more tests
klmcadams Sep 12, 2024
3a3ad30
add dependency and adjust cleanup test
klmcadams Sep 13, 2024
ed768bb
use dry run in ui
klmcadams Sep 13, 2024
1d13113
print command for dry run
klmcadams Sep 13, 2024
66db83a
fix return
klmcadams Sep 13, 2024
f8f913f
fix mock launcher
klmcadams Sep 13, 2024
a19a0da
adjust return in graphically_launch_temp function
klmcadams Sep 16, 2024
6dbb692
Merge branch 'main' into feat/gui-on-load
klmcadams Sep 16, 2024
cba9ec5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 16, 2024
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/882.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
launch_gui command
5 changes: 5 additions & 0 deletions src/ansys/mechanical/core/embedding/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ansys.mechanical.core.embedding.appdata import UniqueUserProfile
from ansys.mechanical.core.embedding.imports import global_entry_points, global_variables
from ansys.mechanical.core.embedding.poster import Poster
from ansys.mechanical.core.embedding.ui import launch_ui
from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings

try:
Expand Down Expand Up @@ -191,6 +192,10 @@ def save_as(self, path):
"""Save the project as."""
self.DataModel.Project.SaveAs(path)

def launch_gui(self, delete_tmp_on_close: bool = True):
"""Launch the GUI."""
launch_ui(self, delete_tmp_on_close)

def new(self):
"""Clear to a new application."""
self.DataModel.Project.New()
Expand Down
64 changes: 64 additions & 0 deletions src/ansys/mechanical/core/embedding/cleanup_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Clean up temporary mechdb files after GUI is closed."""

from pathlib import Path, PurePath
import shutil
import sys
import time

import psutil


def cleanup_gui(pid, temp_mechdb) -> None:
"""Remove the temporary mechdb file after it is closed.

Parameters
----------
pid: int
The process ID of the open temporary mechdb file.
temp_mechdb: Path
The path of the temporary mechdb file.
"""
# While the pid exists, sleep
while psutil.pid_exists(pid):
time.sleep(1)

# Delete the temporary mechdb file once the GUI is closed
Path.unlink(temp_mechdb)

# Delete the temporary mechdb Mech_Files folder
dirname = PurePath(temp_mechdb).parent
basename = PurePath(temp_mechdb).name
pd_index = basename.index(".")
temp_folder = f"{basename[0:pd_index]}_Mech_Files"
temp_folder_path = PurePath.joinpath(dirname, temp_folder)
shutil.rmtree(temp_folder_path)


if __name__ == "__main__":
# Convert the process id (pid) argument into an integer
pid = int(sys.argv[1])
# Convert the temporary mechdb path into a Path
temp_mechdb_path = Path(sys.argv[2])
# Remove the temporary mechdb file when the GUI is closed
cleanup_gui(pid, temp_mechdb_path)
185 changes: 185 additions & 0 deletions src/ansys/mechanical/core/embedding/ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Run Mechanical UI from Python."""

import atexit
import os
from pathlib import Path
from subprocess import Popen
import sys
import tempfile
import typing


class UILauncher:
"""Launch the GUI using a temporary mechdb file."""

def save_original(self, app: "ansys.mechanical.core.embedding.App") -> None:
"""Save the active mechdb file.

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
"""
app.save()

def save_temp_copy(
self, app: "ansys.mechanical.core.embedding.App"
) -> typing.Union[Path, Path]:
"""Save a new mechdb file with a temporary name.

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
"""
# Identify the mechdb of the saved session from save_original()
project_directory = Path(app.DataModel.Project.ProjectDirectory)
project_directory_parent = project_directory.parent
print(project_directory_parent)
mechdb_file = os.path.join(
project_directory_parent, f"{project_directory.parts[-1].split('_')[0]}.mechdb"
)

# Get name of NamedTemporaryFile
temp_file_name = tempfile.NamedTemporaryFile(
dir=project_directory_parent, suffix=".mechdb", delete=True
).name

# Save app with name of temporary file
app.save_as(temp_file_name)

return mechdb_file, temp_file_name

def open_original(self, app: "ansys.mechanical.core.embedding.App", mechdb_file: Path) -> None:
"""Open the original mechdb file from save_original().

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
mechdb_file: pathlib.Path
The full path to the active mechdb file.
"""
app.open(mechdb_file)

def graphically_launch_temp(
self, app: "ansys.mechanical.core.embedding.App", mechdb_file: Path, temp_file: Path
) -> Popen:
"""Launch the GUI for the mechdb file with a temporary name from save_temp_copy().

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
mechdb_file: pathlib.Path
The full path to the active mechdb file.
temp_file: pathlib.Path
The full path to the temporary mechdb file.

Returns
-------
subprocess.Popen
The subprocess that launches the GUI for the temporary mechdb file.
"""
p = Popen(
[
"ansys-mechanical",
"--project-file",
temp_file,
"--graphical",
"--revision",
str(app.version),
],
)

return p

def _cleanup_gui(self, process, temp_mechdb_path):
cleanup_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cleanup_gui.py")
Popen([sys.executable, cleanup_script, str(process.pid), temp_mechdb_path])


def _is_saved(app: "ansys.mechanical.core.embedding.App") -> bool:
"""Check if the mechdb file has been saved and raise an exception if not.

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
"""
try:
app.save()
except:
raise Exception("The App must have already been saved before using launch_ui!")
return True


def _launch_ui(
app: "ansys.mechanical.core.embedding.App", delete_tmp_on_close: bool, launcher: UILauncher
) -> None:
"""Launch the Mechanical UI if the mechdb file has been saved.

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
delete_tmp_on_close: bool
Whether to delete the temporary mechdb file when the GUI is closed.
By default, this is ``True``.
launcher: UILauncher
Launch the GUI using a temporary mechdb file.
"""
if _is_saved(app):
launcher.save_original(app)
mechdb_file, temp_file = launcher.save_temp_copy(app)
launcher.open_original(app, mechdb_file)
p = launcher.graphically_launch_temp(app, mechdb_file, temp_file)

# If the user wants the temporary file to be deleted
if delete_tmp_on_close:
atexit.register(launcher._cleanup_gui, p, temp_file)
klmcadams marked this conversation as resolved.
Show resolved Hide resolved
else:
# Let the user know that the mechdb started above will not automatically get cleaned up
print(
f"""Opened a new mechanical session based on {mechdb_file} named {temp_file}.
PyMechanical will not delete it after use."""
)


def launch_ui(app: "ansys.mechanical.core.embedding.App", delete_tmp_on_close: bool = True) -> None:
"""Launch the Mechanical UI.

Precondition: Mechanical has to have already been saved
Side effect: If Mechanical has ever been saved, it overwrites that save.

Parameters
----------
app: ansys.mechanical.core.embedding.app.App
A Mechanical embedding application.
delete_tmp_on_close: bool
Whether to delete the temporary mechdb file when the GUI is closed.
By default, this is ``True``.
"""
_launch_ui(app, delete_tmp_on_close, UILauncher())
2 changes: 1 addition & 1 deletion src/ansys/mechanical/core/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from ansys.mechanical.core.embedding.appdata import UniqueUserProfile
from ansys.mechanical.core.feature_flags import get_command_line_arguments, get_feature_flag_names

DRY_RUN = False
DRY_RUN = False # control this with an env var to opt into dry run?
klmcadams marked this conversation as resolved.
Show resolved Hide resolved
"""Dry run constant."""

# TODO - add logging options (reuse env var based logging initialization)
Expand Down
62 changes: 62 additions & 0 deletions tests/embedding/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import pytest

from ansys.mechanical.core.embedding.ui import _launch_ui
import ansys.mechanical.core.embedding.utils as utils


Expand Down Expand Up @@ -323,3 +324,64 @@ def test_app_execute_script(embedded_app):
with pytest.raises(Exception):
# This will throw an exception since no module named test available
embedded_app.execute_script("import test")


klmcadams marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.embedding
def test_launch_ui(embedded_app, tmp_path: pytest.TempPathFactory):
klmcadams marked this conversation as resolved.
Show resolved Hide resolved
"""Test the _launch_ui function with a mock launcher."""

class MockLauncher:
klmcadams marked this conversation as resolved.
Show resolved Hide resolved
"""Mock Launcher to test launch_gui functionality."""

def __init__(self):
"""Initialize the MockLauncher class."""
self.ops = []

def save_original(self, app):
"""Save the active mechdb file."""
self.ops.append("save")

def save_temp_copy(self, app):
"""Save a new mechdb file with a temporary name."""
# Identify the mechdb of the saved session from save_original()
self.ops.append("get_saved_session")

# Get name of NamedTemporaryFile
self.ops.append("get_name_temp_file")

# Save app with name of temporary file
self.ops.append("save_as")

return "", ""

def open_original(self, app, mechdb_file):
"""Open the original mechdb file from save_original()."""
self.ops.append("open_orig_mechdb")

def graphically_launch_temp(self, app, mechdb_file, temp_file):
"""Launch the GUI for the mechdb file with a temporary name from save_temp_copy()."""
self.ops.append("launch_temp_mechdb")

return []

# Create an instance of the MockLauncher object
m = MockLauncher()

# Check that _launch_ui raises an Exception when it hasn't been saved yet
with pytest.raises(Exception):
_launch_ui(embedded_app, m)

mechdb_path = os.path.join(tmp_path, "test.mechdb")
# Save a test.mechdb file
embedded_app.save(mechdb_path)
# Launch the UI with the mock class
_launch_ui(embedded_app, False, m)
# Close the embedded_app
embedded_app.close()

assert m.ops[0] == "save"
assert m.ops[1] == "get_saved_session"
assert m.ops[2] == "get_name_temp_file"
assert m.ops[3] == "save_as"
assert m.ops[4] == "open_orig_mechdb"
assert m.ops[5] == "launch_temp_mechdb"
Loading