diff --git a/doc/changelog.d/935.added.md b/doc/changelog.d/935.added.md new file mode 100644 index 000000000..7b7fab721 --- /dev/null +++ b/doc/changelog.d/935.added.md @@ -0,0 +1 @@ +`ansys-mechanical-ideconfig` command \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5da7fc68b..9fb8ff1e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ viz = [ [project.scripts] ansys-mechanical = "ansys.mechanical.core.run:cli" +ansys-mechanical-ideconfig = "ansys.mechanical.core.ide_config:cli" [tool.flit.module] name = "ansys.mechanical.core" diff --git a/src/ansys/mechanical/core/ide_config.py b/src/ansys/mechanical/core/ide_config.py new file mode 100644 index 000000000..0ec78b90d --- /dev/null +++ b/src/ansys/mechanical/core/ide_config.py @@ -0,0 +1,164 @@ +# 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. + +"""Convenience CLI to run mechanical.""" + +import json +import os +from pathlib import Path +import sys +import sysconfig + +import ansys.tools.path as atp +import click + + +def _vscode_impl( + target: str = "user", + revision: int = None, +): + """Get the IDE configuration for autocomplete in VS Code. + + Parameters + ---------- + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "242". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + """ + # Update the user or workspace settings + if target == "user": + # Get the path to the user's settings.json file depending on the platform + if "win" in sys.platform: + settings_json = ( + Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" + ) # pragma: no cover + elif "lin" in sys.platform: + settings_json = ( + Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" + ) + elif target == "workspace": + # Get the current working directory + current_dir = Path.cwd() + # Get the path to the settings.json file based on the git root & .vscode folder + settings_json = current_dir / ".vscode" / "settings.json" + + # Location where the stubs are installed -> .venv/Lib/site-packages, for example + stubs_location = ( + Path(sysconfig.get_paths()["purelib"]) / "ansys" / "mechanical" / "stubs" / f"v{revision}" + ) + + # The settings to add to settings.json for autocomplete to work + settings_json_data = { + "python.autoComplete.extraPaths": [str(stubs_location)], + "python.analysis.extraPaths": [str(stubs_location)], + } + # Pretty print dictionary + pretty_dict = json.dumps(settings_json_data, indent=4) + + print(f"Update {settings_json} with the following information:\n") + + if target == "workspace": + print( + "Note: Please ensure the .vscode folder is in the root of your project or repository.\n" + ) + + print(pretty_dict) + + +def _cli_impl( + ide: str = "vscode", + target: str = "user", + revision: int = None, +): + """Provide the user with the path to the settings.json file and IDE settings. + + Parameters + ---------- + ide: str + The IDE to set up autocomplete settings. By default, it's ``vscode``. + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "242". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + """ + # Check the IDE and raise an exception if it's not VS Code + if ide == "vscode": + return _vscode_impl(target, revision) + else: + raise Exception(f"{ide} is not supported at the moment.") + + +@click.command() +@click.help_option("--help", "-h") +@click.option( + "--ide", + default="vscode", + type=str, + help="The IDE being used.", +) +@click.option( + "--target", + default="user", + type=str, + help="The type of settings to update - either ``user`` or ``workspace`` settings.", +) +@click.option( + "--revision", + default=None, + type=int, + help='The Mechanical revision number, e.g. "242" or "241". If unspecified,\ +it finds and uses the default version from ansys-tools-path.', +) +def cli(ide: str, target: str, revision: int) -> None: + """CLI tool to update settings.json files for autocomplete with ansys-mechanical-stubs. + + Parameters + ---------- + ide: str + The IDE to set up autocomplete settings. By default, it's ``vscode``. + target: str + The type of settings to update. Either "user" or "workspace" in VS Code. + By default, it's ``user``. + revision: int + The Mechanical revision number. For example, "242". + If unspecified, it finds the default Mechanical version from ansys-tools-path. + + Usage + ----- + The following example demonstrates the main use of this tool: + + $ ansys-mechanical-ideconfig --ide vscode --location user --revision 242 + + """ + exe = atp.get_mechanical_path(allow_input=False, version=revision) + version = atp.version_from_path("mechanical", exe) + + return _cli_impl( + ide, + target, + version, + ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 55efbfb0b..2c198240a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,9 +21,14 @@ # SOFTWARE. import os +from pathlib import Path +import subprocess +import sys +import sysconfig import pytest +from ansys.mechanical.core.ide_config import _cli_impl as ideconfig_cli_impl from ansys.mechanical.core.run import _cli_impl @@ -229,3 +234,165 @@ def test_cli_batch_required_args(disable_cli): _cli_impl(exe="AnsysWBU.exe", version=241, port=11) except Exception as e: assert False, f"cli raised an exception: {e}" + + +def get_settings_location() -> str: + """Get the location of settings.json for user settings. + + Returns + ------- + str + The path to the settings.json file for users on Windows and Linux. + """ + if "win" in sys.platform: + settings_json = Path(os.environ.get("APPDATA")) / "Code" / "User" / "settings.json" + elif "lin" in sys.platform: + settings_json = Path(os.environ.get("HOME")) / ".config" / "Code" / "User" / "settings.json" + + return settings_json + + +def get_stubs_location(revision: int) -> Path: + """Get the ansys-mechanical-stubs location with specified revision. + + Parameters + ---------- + revision: int + The Mechanical revision number. For example, "242". + + Returns + ------- + pathlib.Path + The path to the ansys-mechanical-stubs installation. + """ + return ( + Path(sysconfig.get_paths()["purelib"]) / "ansys" / "mechanical" / "stubs" / f"v{revision}" + ) + + +@pytest.mark.cli +def test_ideconfig_cli_ide_exception(capfd): + """Test IDE configuration raises an exception for anything but vscode.""" + with pytest.raises(Exception): + ideconfig_cli_impl( + ide="pycharm", + target="user", + revision=242, + ) + + +@pytest.mark.cli +def test_ideconfig_cli_user_settings(capfd): + """Test the IDE configuration prints correct information for user settings.""" + # Set the revision number + revision = 242 + + # Run the IDE configuration command for the user settings type + ideconfig_cli_impl( + ide="vscode", + target="user", + revision=revision, + ) + + # Get output of the IDE configuration command + out, err = capfd.readouterr() + out = out.replace("\\\\", "\\") + + # Get the path to the settings.json file based on operating system env vars + settings_json = get_settings_location() + stubs_location = get_stubs_location(revision) + + assert f"Update {settings_json} with the following information" in out + assert str(stubs_location) in out + + +@pytest.mark.cli +def test_ideconfig_cli_workspace_settings(capfd): + """Test the IDE configuration prints correct information for workplace settings.""" + # Set the revision number + revision = 241 + + # Run the IDE configuration command + ideconfig_cli_impl( + ide="vscode", + target="workspace", + revision=revision, + ) + + # Get output of the IDE configuration command + out, err = capfd.readouterr() + out = out.replace("\\\\", "\\") + + # Get the path to the settings.json file based on the current directory & .vscode folder + settings_json = Path.cwd() / ".vscode" / "settings.json" + stubs_location = get_stubs_location(revision) + + # Assert the correct settings.json file and stubs location is in the output + assert f"Update {settings_json} with the following information" in out + assert str(stubs_location) in out + assert "Please ensure the .vscode folder is in the root of your project or repository" in out + + +@pytest.mark.cli +@pytest.mark.python_env +def test_ideconfig_venv(test_env, run_subprocess, rootdir): + """Test the IDE configuration location when a virtual environment is active.""" + # Set the revision number + revision = 242 + + # Install pymechanical + subprocess.check_call( + [test_env.python, "-m", "pip", "install", "-e", "."], + cwd=rootdir, + env=test_env.env, + ) + + # Run ansys-mechanical-ideconfig in the test virtual environment + process, stdout, stderr = run_subprocess( + [ + "ansys-mechanical-ideconfig", + "--ide", + "vscode", + "--target", + "user", + "--revision", + str(revision), + ], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + stdout = stdout.decode().replace("\\\\", "\\") + + # Assert virtual environment is in the stdout + assert ".test_env" in stdout + + +@pytest.mark.cli +@pytest.mark.python_env +def test_ideconfig_default(test_env, run_subprocess, rootdir, pytestconfig): + """Test the IDE configuration location when no arguments are supplied.""" + # Get the revision number + revision = pytestconfig.getoption("ansys_version") + # Set part of the settings.json path + settings_json_fragment = Path("Code") / "User" / "settings.json" + + # Install pymechanical + subprocess.check_call( + [test_env.python, "-m", "pip", "install", "-e", "."], + cwd=rootdir, + env=test_env.env, + ) + + # Run ansys-mechanical-ideconfig in the test virtual environment + process, stdout, stderr = run_subprocess( + [ + "ansys-mechanical-ideconfig", + ], + env=test_env.env, + ) + # Decode stdout and fix extra backslashes in paths + stdout = stdout.decode().replace("\\\\", "\\") + + assert revision in stdout + assert str(settings_json_fragment) in stdout + assert ".test_env" in stdout