Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ jobs:
PYTHONUNBUFFERED: 1
run: |
. /env/bin/activate
mechanical-env pytest -m embedding_scripts -s --junitxml test_results_embedding_scripts${{ matrix.python-version }}.xml
mechanical-env pytest -m embedding_scripts -m cli -s --junitxml test_results_embedding_scripts${{ matrix.python-version }}.xml

- name: Upload coverage results
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/735.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add feature flags to ansys-mechanical cli
3 changes: 3 additions & 0 deletions doc/source/getting_started/running_mechanical.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ usage, type the following command:
port number
-i, --input-script TEXT Name of the input Python script. Cannot be mixed
with -p
--features TEXT Beta feature flags to set, as a semicolon
delimited list. Options: ['MultistageHarmonic',
'ThermalShells']
--exit Exit the application after running an input
script. You can only use this command with
--input-script argument (-i). The command
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ markers = [
"remote_session_connect: tests that connect to Mechanical and work with gRPC server inside it",
"minimum_version(num): tests that run if ansys-version is greater than or equal to the minimum version provided",
"windows_only: tests that run if the testing platform is on Windows",
"cli: tests for the Command Line Interface",
]
xfail_strict = true

Expand Down
7 changes: 6 additions & 1 deletion src/ansys/mechanical/core/embedding/appdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@
class UniqueUserProfile:
"""Create Unique User Profile (for AppData)."""

def __init__(self, profile_name):
def __init__(self, profile_name: str, dry_run: bool = False):
"""Initialize UniqueUserProfile class."""
self._default_profile = os.path.expanduser("~")
self._location = os.path.join(self._default_profile, "PyMechanical-AppData", profile_name)
self._dry_run = dry_run
self.initialize()

def initialize(self) -> None:
"""Initialize the new profile location."""
if self._dry_run:
return
if self.exists():
self.cleanup()
self.mkdirs()
self.copy_profiles()

def cleanup(self) -> None:
"""Cleanup unique user profile."""
if self._dry_run:
return
text = "The `private_appdata` option was used, but the following files were not removed: "
message = []

Expand Down
51 changes: 51 additions & 0 deletions src/ansys/mechanical/core/feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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.

"""Mechanical beta feature flags."""

import typing
import warnings


class FeatureFlags:
"""Supported feature flag names."""

ThermalShells = "Mechanical.ThermalShells"
MultistageHarmonic = "Mechanical.MultistageHarmonic"


def get_feature_flag_names() -> typing.List[str]:
"""Get the available feature flags."""
return [x for x in dir(FeatureFlags) if "_" not in x]


def _get_flag_arg(flagname: str) -> str:
"""Get the command line name for a given feature flag."""
if hasattr(FeatureFlags, flagname):
return getattr(FeatureFlags, flagname)
warnings.warn(f"Using undocumented feature flag {flagname}")
return flagname


def get_command_line_arguments(flags: typing.List[str]):
"""Get the command line arguments as an array for the given flags."""
return ["-featureflags", ";".join([_get_flag_arg(flag) for flag in flags])]
193 changes: 119 additions & 74 deletions src/ansys/mechanical/core/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
import click

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

# TODO - add logging options (reuse env var based logging initialization)
# TODO - add timeout
Expand Down Expand Up @@ -82,6 +85,101 @@ def _run(args, env, check=False, display=False):
return output


def _cli_impl(
project_file: str = None,
port: int = 0,
debug: bool = False,
input_script: str = None,
exe: str = None,
version: int = None,
graphical: bool = False,
show_welcome_screen: bool = False,
private_appdata: bool = False,
exit: bool = False,
features: str = None,
):
if project_file and input_script:
raise Exception("Cannot open a project file *and* run a script.")

if (not graphical) and project_file:
raise Exception("Cannot open a project file in batch mode.")

if port:
if project_file:
raise Exception("Cannot open in server mode with a project file.")
if input_script:
raise Exception("Cannot open in server mode with an input script.")

args = [exe, "-DSApplet"]
if (not graphical) or (not show_welcome_screen):
args.append("-AppModeMech")

if version < 232:
args.append("-nosplash")
args.append("-notabctrl")

if not graphical:
args.append("-b")

env: typing.Dict[str, str] = os.environ.copy()
if debug:
env["WBDEBUG_STOP"] = "1"

if port:
args.append("-grpc")
args.append(str(port))

if project_file:
args.append("-file")
args.append(project_file)

if input_script:
args.append("-script")
args.append(input_script)

if (not graphical) and input_script:
exit = True
if version < 241:
warnings.warn(
"Please ensure ExtAPI.Application.Close() is at the end of your script. "
"Without this command, Batch mode will not terminate.",
stacklevel=2,
)

if exit and input_script and version >= 241:
args.append("-x")

profile: UniqueUserProfile = None
if private_appdata:
new_profile_name = f"Mechanical-{os.getpid()}"
profile = UniqueUserProfile(new_profile_name, DRY_RUN)
profile.update_environment(env)

if not DRY_RUN:
version_name = atp.SUPPORTED_ANSYS_VERSIONS[version]
if graphical:
mode = "Graphical"
else:
mode = "Batch"
print(f"Starting Ansys Mechanical version {version_name} in {mode} mode...")
if port:
# TODO - Mechanical doesn't write anything to the stdout in grpc mode
# when logging is off.. Ideally we let Mechanical write it, so
# the user only sees the message when the server is ready.
print(f"Serving on port {port}")

if features is not None:
args.extend(get_command_line_arguments(features.split(";")))

if DRY_RUN:
return args, env
else:
_run(args, env, False, True)

if private_appdata:
profile.cleanup()


@click.command()
@click.help_option("--help", "-h")
@click.option(
Expand All @@ -102,6 +200,13 @@ def _run(args, env, check=False, display=False):
type=int,
help="Start mechanical in server mode with the given port number",
)
@click.option(
"--features",
type=str,
default=None,
help=f"Beta feature flags to set, as a semicolon delimited list.\
Options: {get_feature_flag_names()}",
)
@click.option(
"-i",
"--input-script",
Expand Down Expand Up @@ -156,6 +261,7 @@ def cli(
show_welcome_screen: bool,
private_appdata: bool,
exit: bool,
features: str,
):
"""CLI tool to run mechanical.

Expand All @@ -167,80 +273,19 @@ def cli(

Starting Ansys Mechanical version 2023R2 in graphical mode...
"""
if project_file and input_script:
raise Exception("Cannot open a project file *and* run a script.")

if (not graphical) and project_file:
raise Exception("Cannot open a project file in batch mode.")

if port:
if project_file:
raise Exception("Cannot open in server mode with a project file.")
if input_script:
raise Exception("Cannot open in server mode with an input script.")

exe = atp.get_mechanical_path(allow_input=False, version=revision)
version = atp.version_from_path("mechanical", exe)

version_name = atp.SUPPORTED_ANSYS_VERSIONS[version]

args = [exe, "-DSApplet"]
if (not graphical) or (not show_welcome_screen):
args.append("-AppModeMech")

if version < 232:
args.append("-nosplash")
args.append("-notabctrl")

if graphical:
mode = "Graphical"
else:
mode = "Batch"
args.append("-b")

if debug:
os.environ["WBDEBUG_STOP"] = "1"

if port:
args.append("-grpc")
args.append(str(port))

if project_file:
args.append("-file")
args.append(project_file)

if input_script:
args.append("-script")
args.append(input_script)

if (not graphical) and input_script:
exit = True
if version < 241:
warnings.warn(
"Please ensure ExtAPI.Application.Close() is at the end of your script. "
"Without this command, Batch mode will not terminate.",
stacklevel=2,
)

if exit and input_script and version >= 241:
args.append("-x")

profile: UniqueUserProfile = None
env: typing.Dict[str, str] = None
if private_appdata:
env = os.environ.copy()
new_profile_name = f"Mechanical-{os.getpid()}"
profile = UniqueUserProfile(new_profile_name)
profile.update_environment(env)

print(f"Starting Ansys Mechanical version {version_name} in {mode} mode...")
if port:
# TODO - Mechanical doesn't write anything to the stdout in grpc mode
# when logging is off.. Ideally we let Mechanical write it, so
# the user only sees the message when the server is ready.
print(f"Serving on port {port}")

_run(args, env, False, True)

if private_appdata:
profile.cleanup()
return _cli_impl(
project_file,
port,
debug,
input_script,
exe,
version,
graphical,
show_welcome_screen,
private_appdata,
exit,
features,
)
11 changes: 9 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from ansys.mechanical.core.errors import MechanicalExitedError
from ansys.mechanical.core.examples import download_file
from ansys.mechanical.core.misc import get_mechanical_bin
from ansys.mechanical.core.run import _run
import ansys.mechanical.core.run

# to run tests with multiple markers
# pytest -q --collect-only -m "remote_session_launch"
Expand Down Expand Up @@ -176,7 +176,7 @@ def run_subprocess():
def func(args, env=None, check: bool = None):
if check is None:
check = _CHECK_PROCESS_RETURN_CODE
stdout, stderr = _run(args, env, check)
stdout, stderr = ansys.mechanical.core.run._run(args, env, check)
return stdout, stderr

return func
Expand All @@ -189,6 +189,13 @@ def rootdir():
yield base.parent


@pytest.fixture()
def disable_cli():
ansys.mechanical.core.run.DRY_RUN = True
yield
ansys.mechanical.core.run.DRY_RUN = False


@pytest.fixture()
def test_env():
"""Create a virtual environment scoped to the test."""
Expand Down
Loading