diff --git a/.gitignore b/.gitignore index bc429f201b3e..68ec0e77b294 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ _build # No benchmarks output /benchmarks/ benchmark_*.json +/pyrightconfig.json # Ruff cache diff --git a/.vscode/tools/settings.template.json b/.vscode/tools/settings.template.json index 4b07a6a8f9ad..e9ade2972128 100644 --- a/.vscode/tools/settings.template.json +++ b/.vscode/tools/settings.template.json @@ -78,8 +78,5 @@ }, "[restructuredtext]": { "editor.tabSize": 2 - }, - // Python extra paths - // Note: this is filled up when "./isaaclab.sh -i" is run - "python.analysis.extraPaths": [] + } } diff --git a/.vscode/tools/setup_vscode.py b/.vscode/tools/setup_vscode.py index 8d29dafee080..42f5507e2f68 100644 --- a/.vscode/tools/setup_vscode.py +++ b/.vscode/tools/setup_vscode.py @@ -3,197 +3,75 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This script sets up the vs-code settings for the Isaac Lab project. +"""Set up VS Code and Cursor for an Isaac Lab repository or external project. -This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into -the ".vscode/settings.json" file. - -This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path -when the "setup_python_env.sh" is run as part of the vs-code launch configuration. +The script writes editor settings and a machine-local ``pyrightconfig.json``. The Pyright +configuration extends the project's checked-in ``pyproject.toml`` and adds import roots +discovered from the active Python environment. This supports source, editable, wheel, and +Isaac Sim binaries installations without storing machine-specific paths in version control. """ +import argparse +import pathlib import re -import subprocess import sys -import os -import pathlib - - -ISAACLAB_DIR = pathlib.Path(__file__).parents[2] -"""Path to the Isaac Lab directory.""" - -# Try to find IsaacSim dir -_isaacsim_probe = subprocess.run( - [sys.executable, "-c", "import isaacsim; import os; print(os.environ.get('ISAAC_PATH', ''))"], - capture_output=True, - text=True, - check=False, - # avoid EULA prompt - stdin=subprocess.DEVNULL, -) -if _isaacsim_probe.returncode == 0 and _isaacsim_probe.stdout.strip(): - isaacsim_dir = _isaacsim_probe.stdout.strip() -else: - isaacsim_dir = os.path.join(ISAACLAB_DIR, "_isaac_sim") - -# check if the isaac-sim directory exists -if not os.path.exists(isaacsim_dir): - print( - f"[WARN] Could not find the isaac-sim directory: {isaacsim_dir}." - "\n\tIsaac Sim does not appear to be installed. VS Code settings will be generated" - "\n\twithout Isaac Sim extra paths." - ) - isaacsim_dir = "" - -ISAACSIM_DIR = isaacsim_dir -"""Path to the isaac-sim directory.""" - - -def overwrite_python_analysis_extra_paths(isaaclab_settings: str) -> str: - """Overwrite the python.analysis.extraPaths in the Isaac Lab settings file. - The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the - "{ISAACSIM_DIR}/.vscode/settings.json" file. +from isaaclab.utils.editor import build_extra_paths, resolve_isaacsim_dir, write_pyright_config - If the isaac-sim settings file does not exist, the extraPaths are not overwritten. +PROJECT_DIR = pathlib.Path(__file__).parents[2] +"""Path to the repository or generated project's root directory.""" - Args: - isaaclab_settings: The settings string to use as template. - Returns: - The settings string with overwritten python analysis extra paths. - """ - # isaac-sim settings - isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json") - - # we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions - # if this file does not exist, we will not add any extra paths - if ISAACSIM_DIR and os.path.exists(isaacsim_vscode_filename): - # read the path names from the isaac-sim settings file - with open(isaacsim_vscode_filename) as f: - vscode_settings = f.read() - # extract the path names - # search for the python.analysis.extraPaths section and extract the contents - settings = re.search( - r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL - ) - settings = settings.group(0) - settings = settings.split('"python.analysis.extraPaths": [')[-1] - settings = settings.split("]")[0] - - # read the path names from the isaac-sim settings file - path_names = settings.split(",") - path_names = [path_name.strip().strip('"') for path_name in path_names] - path_names = [path_name for path_name in path_names if len(path_name) > 0] - - # change the path names to be relative to the Isaac Lab directory - rel_path = os.path.relpath(ISAACSIM_DIR, ISAACLAB_DIR) - path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names] - else: - path_names = [] - - # add the path names that are in the Isaac Lab extensions directory - isaaclab_extensions = os.listdir(os.path.join(ISAACLAB_DIR, "source")) - path_names.extend(['"${workspaceFolder}/source/' + ext + '"' for ext in isaaclab_extensions]) - - # combine them into a single string - path_names = ",\n\t\t".expandtabs(4).join(path_names) - # deal with the path separator being different on Windows and Unix - path_names = path_names.replace("\\", "/") - - # replace the path names in the Isaac Lab settings file with the path names parsed - isaaclab_settings = re.sub( - r"\"python.analysis.extraPaths\": \[.*?\]", - '"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4), - isaaclab_settings, - flags=re.DOTALL, - ) - # return the Isaac Lab settings string - return isaaclab_settings - - -def overwrite_default_python_interpreter(isaaclab_settings: str) -> str: - """Overwrite the default python interpreter in the Isaac Lab settings file. - - The default python interpreter is replaced with the path to the python interpreter used by the - isaac-sim project. This is necessary because the default python interpreter is the one shipped with - isaac-sim. +def overwrite_default_python_interpreter(settings: str, isaacsim_dir: pathlib.Path | None) -> str: + """Set the editor's default Python interpreter. Args: - isaaclab_settings: The settings string to use as template. + settings: VS Code settings template. + isaacsim_dir: Isaac Sim installation directory, or None. Returns: - The settings string with overwritten default python interpreter. + Settings with the interpreter path updated. """ - # read executable name - python_exe = sys.executable.replace("\\", "/") - - # We make an exception for replacing the default interpreter if the - # path (/kit/python/bin/python3) indicates that we are using a local/container - # installation of IsaacSim. We will preserve the calling script as the default, python.sh. - # We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH - # (among other envars) that we need for all of our dependencies to be accessible. - if "kit/python/bin/python3" in python_exe: - return isaaclab_settings - # replace the default python interpreter in the Isaac Lab settings file with the path to the - # python interpreter in the Isaac Lab directory - isaaclab_settings = re.sub( - r"\"python.defaultInterpreterPath\": \".*?\"", - f'"python.defaultInterpreterPath": "{python_exe}"', - isaaclab_settings, + python_exe = pathlib.Path(sys.executable) + if "kit/python/bin/python3" in python_exe.as_posix() and isaacsim_dir is not None: + wrapper = isaacsim_dir / "python.sh" + if wrapper.is_file(): + python_exe = wrapper + return re.sub( + r'"python\.defaultInterpreterPath": ".*?"', + f'"python.defaultInterpreterPath": "{python_exe.as_posix()}"', + settings, flags=re.DOTALL, ) - # return the Isaac Lab settings file - return isaaclab_settings def main(): - # Isaac Lab template settings - isaaclab_vscode_template_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "settings.template.json") - # make sure the Isaac Lab template settings file exists - if not os.path.exists(isaaclab_vscode_template_filename): - raise FileNotFoundError( - f"Could not find the Isaac Lab template settings file: {isaaclab_vscode_template_filename}" - ) - # read the Isaac Lab template settings file - with open(isaaclab_vscode_template_filename) as f: - isaaclab_template_settings = f.read() - - # overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names - isaaclab_settings = overwrite_python_analysis_extra_paths(isaaclab_template_settings) - # overwrite the default python interpreter in the Isaac Lab settings file with the path to the - # python interpreter used to call this script - isaaclab_settings = overwrite_default_python_interpreter(isaaclab_settings) - - # add template notice to the top of the file - header_message = ( - "// This file is a template and is automatically generated by the setup_vscode.py script.\n" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--isaac_path", help="Absolute path to the Isaac Sim installation.") + args = parser.parse_args() + + try: + isaacsim_dir = resolve_isaacsim_dir(PROJECT_DIR, args.isaac_path) + except ValueError as error: + parser.error(str(error)) + write_pyright_config(PROJECT_DIR, build_extra_paths(PROJECT_DIR, isaacsim_dir)) + + settings_template = PROJECT_DIR / ".vscode" / "tools" / "settings.template.json" + if not settings_template.is_file(): + raise FileNotFoundError(f"Could not find the VS Code settings template: {settings_template}") + settings = overwrite_default_python_interpreter(settings_template.read_text(encoding="utf-8"), isaacsim_dir) + header = ( + "// This file is automatically generated by setup_vscode.py.\n" "// Do not edit this file directly.\n" - "// \n" - f"// Generated from: {isaaclab_vscode_template_filename}\n" + f"// Generated from: {settings_template}\n" ) - isaaclab_settings = header_message + isaaclab_settings - - # write the Isaac Lab settings file - isaaclab_vscode_filename = os.path.join(ISAACLAB_DIR, ".vscode", "settings.json") - with open(isaaclab_vscode_filename, "w") as f: - f.write(isaaclab_settings) - - # copy the launch.json file if it doesn't exist - isaaclab_vscode_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "launch.json") - isaaclab_vscode_template_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "launch.template.json") - if not os.path.exists(isaaclab_vscode_launch_filename): - # read template launch settings - with open(isaaclab_vscode_template_launch_filename) as f: - isaaclab_template_launch_settings = f.read() - # add header - header_message = header_message.replace( - isaaclab_vscode_template_filename, isaaclab_vscode_template_launch_filename - ) - isaaclab_launch_settings = header_message + isaaclab_template_launch_settings - # write the Isaac Lab launch settings file - with open(isaaclab_vscode_launch_filename, "w") as f: - f.write(isaaclab_launch_settings) + (PROJECT_DIR / ".vscode" / "settings.json").write_text(header + settings, encoding="utf-8") + + launch_file = PROJECT_DIR / ".vscode" / "launch.json" + launch_template = PROJECT_DIR / ".vscode" / "tools" / "launch.template.json" + if not launch_file.exists(): + launch_header = header.replace(str(settings_template), str(launch_template)) + launch_file.write_text(launch_header + launch_template.read_text(encoding="utf-8"), encoding="utf-8") if __name__ == "__main__": diff --git a/docs/source/developer-tools/template_generator.rst b/docs/source/developer-tools/template_generator.rst index c69288396663..80fb06e50439 100644 --- a/docs/source/developer-tools/template_generator.rst +++ b/docs/source/developer-tools/template_generator.rst @@ -242,12 +242,28 @@ External projects should build their environment harness from public APIs and maintain project-local fixtures. Copying ``env_test_utils.py`` into a project is vendoring it, so the project must track upstream changes to that copy. -To configure VS Code, run the generated setup task or invoke it directly: +To configure VS Code or Cursor, run the generated setup task or invoke it directly: .. code-block:: bash uv run python .vscode/tools/setup_vscode.py +The command selects the active interpreter and creates a git-ignored +``pyrightconfig.json``. This child configuration inherits the checked-in +Pyright policy from ``pyproject.toml`` and adds the generated project's +``src`` import root, installed Isaac Lab packages, and any discovered Isaac +Sim extensions. When using the ``isaacsim`` extra, include it while generating +the configuration: + +.. code-block:: bash + + uv run --extra isaacsim python .vscode/tools/setup_vscode.py + +In VS Code, use Pylance and select the interpreter that ran the setup command. +In Cursor, install the ``detachhead.basedpyright`` extension instead of Pylance, +select the same interpreter, and reload the window. Both language servers read +the generated ``pyrightconfig.json``. + Create an internal task ----------------------- diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst index 23ea18956bdc..330d5d9a0714 100644 --- a/docs/source/overview/developer-guide/vs_code.rst +++ b/docs/source/overview/developer-guide/vs_code.rst @@ -1,11 +1,15 @@ .. _setup-vs-code: -Setting up Visual Studio Code ------------------------------ +Setting up VS Code or Cursor +---------------------------- -**This is optional. You do not need to use VScode to use Isaac Lab** +Editor setup is optional and is not required to run Isaac Lab. The repository +includes shared settings for `Visual Studio Code `_ +and compatible editors such as `Cursor `_. Complete one +of the :ref:`Isaac Lab installation methods ` before +configuring your editor. -`Visual Studio Code `_ has proven an invaluable tool for the development of Isaac Lab. The Isaac Lab repository includes the VSCode files for setting up your development environment. These are included in the ``.vscode`` directory and include the following files: +The ``.vscode`` directory contains the checked-in templates and tasks: .. code-block:: bash @@ -15,42 +19,123 @@ Setting up Visual Studio Code │   ├── settings.template.json │   └── setup_vscode.py ├── extensions.json - ├── launch.json # <- this is generated by setup_vscode.py - ├── settings.json # <- this is generated by setup_vscode.py + ├── launch.json # generated by setup_vscode.py + ├── settings.json # generated by setup_vscode.py └── tasks.json +Configure a source checkout +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. attention:: +Open the Isaac Lab repository root in your editor. Then run the setup command +that matches your installation from a terminal in that directory. - The following instructions on setting up Visual Studio Code only work with - :ref:`Isaac Sim Binaries Installation ` and not with - :ref:`Python Environment with Isaac Sim `. +.. tab-set:: + .. tab-item:: uv (recommended) -To setup the IDE, please follow these instructions: + For the default Newton environment, run: -1. Open the ``IsaacLab`` directory on Visual Studio Code IDE -2. Run VSCode `Tasks `__, by - pressing ``Ctrl+Shift+P``, selecting ``Tasks: Run Task`` and running the - ``setup_python_env`` in the drop down menu. + .. code-block:: bash - .. image:: ../../_static/vscode_tasks.png - :width: 600px - :align: center - :alt: VSCode Tasks + uv run python .vscode/tools/setup_vscode.py + If you use Isaac Sim from the ``isaacsim`` extra, include the extra so the + setup command can discover its extensions: -.. note:: - If this is your first time running tasks in VS Code, you may be prompted to select how to handle warnings. Simply follow - the prompts until the task window closes. + .. code-block:: bash -If everything executes correctly, it should create the following files: + uv run --extra isaacsim python .vscode/tools/setup_vscode.py -* ``.vscode/launch.json``: Contains the launch configurations for debugging python code. -* ``.vscode/settings.json``: Contains the settings for the python interpreter and the python environment. + .. tab-item:: Activated Python environment -For more information on VSCode support for Omniverse, please refer to the -following links: + Activate the uv, venv, or conda environment where Isaac Lab is installed, + then run: + + .. code-block:: bash + + python .vscode/tools/setup_vscode.py + + .. tab-item:: Downloaded Isaac Sim package + + Run the setup script through the Isaac Lab launcher after completing the + :ref:`downloaded package installation `: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./isaaclab.sh -p .vscode/tools/setup_vscode.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + isaaclab.bat -p .vscode\tools\setup_vscode.py + + The ``setup_python_env`` task in the command palette runs the same launcher + workflow for a downloaded package. + +The command creates or updates these machine-local files: + +* ``.vscode/launch.json``: Debugging configurations. An existing file is preserved. +* ``.vscode/settings.json``: The interpreter and shared editor settings. +* ``pyrightconfig.json``: Import paths for Pyright-compatible language servers. + +The generated files are ignored by Git because interpreter and extension paths +vary between machines. Rerun the command after changing Python environments or +Isaac Sim installations. If Isaac Sim is not installed, the command prints a +warning and still configures the local Isaac Lab packages. + +The checked-in ``[tool.pyright]`` table in ``pyproject.toml`` makes packages +under ``source`` available immediately after cloning. The generated +``pyrightconfig.json`` inherits that policy and adds Isaac Sim extensions plus +Isaac Lab packages found in the active Python environment. This covers source, +editable, and wheel installations without storing absolute paths in Git. + +Configure VS Code +^^^^^^^^^^^^^^^^^ + +Install the extensions recommended by the repository when VS Code prompts you. +At minimum, install the Python and Pylance extensions. Run the setup command +above, then use **Python: Select Interpreter** from the command palette to select +the same interpreter used by the command. For the recommended uv installation, +this is ``.venv/bin/python`` on Linux or ``.venv\Scripts\python.exe`` on Windows. + +Configure Cursor +^^^^^^^^^^^^^^^^ + +Cursor cannot use Pylance because Pylance is licensed for official VS Code +builds. Install the Python extension and the `basedpyright +`__ +extension (``detachhead.basedpyright``). Then: + +1. Run the same setup command shown above for your installation. +2. Select the interpreter that ran the command. +3. Reload the Cursor window so basedpyright rereads ``pyrightconfig.json``. + +No Cursor-specific path list is required. Pylance and basedpyright read the +same Pyright configuration. + +Troubleshoot editor imports +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If an import is still unresolved: + +1. Confirm the selected editor interpreter matches ``python`` in the setup command. +2. Rerun setup with ``--extra isaacsim`` if the missing import is from + ``omni``, ``pxr``, or ``isaacsim``. +3. Reload the editor window. +4. Inspect the generated ``extraPaths`` in the root ``pyrightconfig.json``. + +Remove simulator extension directories that the project does not use if +language-server indexing consumes too much memory. + +For more information about VS Code support in Isaac Sim, see: * `Isaac Sim VSCode support `__ @@ -84,26 +169,24 @@ To use it: and press the green play button or ``F5``. VS Code will connect to the debugpy server running on ``localhost:3000``. -Configuring the python interpreter +Configuring the Python interpreter ---------------------------------- -In the provided configuration, we set the default python interpreter to use the -python executable provided by Omniverse. This is specified in the -``.vscode/settings.json`` file: +The setup command records the interpreter that ran it in +``.vscode/settings.json``. For example, a uv source checkout on Linux uses: .. code-block:: json { - "python.defaultInterpreterPath": "${workspaceFolder}/_isaac_sim/python.sh", + "python.defaultInterpreterPath": "/path/to/IsaacLab/.venv/bin/python", } -If you want to use a different python interpreter (for instance, from your conda or uv environment), -you need to change the python interpreter used by selecting and activating the python interpreter -of your choice in the bottom left corner of VSCode, or opening the command palette (``Ctrl+Shift+P``) -and selecting ``Python: Select Interpreter``. +The editor selection takes precedence over this default. If you change +environments, rerun setup and select the new interpreter from the status bar or +with **Python: Select Interpreter** in the command palette. -For more information on how to set python interpreter for VSCode, please -refer to the `VSCode documentation `_. +For more information about selecting a Python interpreter, see the +`VS Code documentation `_. Setting up formatting and linting diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index c71865eb4086..b86c4313265b 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -771,7 +771,7 @@ The first launch downloads Isaac Sim extensions and can take more than ten minut you to accept the NVIDIA Omniverse EULA; set ``OMNI_KIT_ACCEPT_EULA=yes`` for a non-interactive environment. Run a project script with ``python my_script.py``. -Generate VS Code settings for the current workspace with: +Generate VS Code or Cursor settings for the current workspace with: .. code-block:: bash @@ -779,8 +779,10 @@ Generate VS Code settings for the current workspace with: .. warning:: - This command generates ``.vscode/settings.json`` in the workspace. If the file already exists, - it asks before overwriting it. + This command generates ``.vscode/settings.json`` and ``pyrightconfig.json`` in the workspace. + The Pyright configuration inherits an existing ``[tool.pyright]`` table and adds paths discovered + from the active Python environment. If ``.vscode/settings.json`` already exists, the command asks + before overwriting it. .. _installation-method-binary: .. _isaaclab-binaries-installation: diff --git a/docs/source/setup/quickstart.rst b/docs/source/setup/quickstart.rst index 2934b2097e35..4270c6e48727 100644 --- a/docs/source/setup/quickstart.rst +++ b/docs/source/setup/quickstart.rst @@ -36,6 +36,26 @@ Training outputs, including checkpoints, are saved under ``logs/``. Add uv run isaaclab train --help +Configure an editor (optional) +------------------------------ + +To enable import completion and debugging in VS Code or Cursor, generate the +machine-local editor configuration from the repository root: + +.. code-block:: bash + + uv run python .vscode/tools/setup_vscode.py + +If you use the ``isaacsim`` extra, include it so the command can discover the +Isaac Sim extensions: + +.. code-block:: bash + + uv run --extra isaacsim python .vscode/tools/setup_vscode.py + +VS Code uses Pylance. Cursor users should install basedpyright instead. See +:ref:`setup-vs-code` for complete editor and troubleshooting instructions. + .. The quickstart media is generated by tools/docs/media/generate_quickstart.sh. .. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/quickstart_task_categories.gif diff --git a/pyproject.toml b/pyproject.toml index 78d2120635d6..6437b464ce79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -325,6 +325,11 @@ exclude = [ ".vscode", ] +# Make the in-repo packages resolvable by the language server (Pylance / basedpyright) +# without an editable install, so imports like ``isaaclab.assets`` work out of the box. +# The glob keeps new source packages discoverable without maintaining a duplicate package list. +extraPaths = ["source/*"] + typeCheckingMode = "basic" pythonVersion = "3.12" pythonPlatform = "Linux" diff --git a/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst b/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst new file mode 100644 index 000000000000..3ed215622ff0 --- /dev/null +++ b/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst @@ -0,0 +1,4 @@ +Fixed +^^^^^ + +* Fixed VS Code and Cursor import resolution for source, editable, wheel, and Isaac Sim binaries installations. diff --git a/source/isaaclab/isaaclab/__main__.py b/source/isaaclab/isaaclab/__main__.py index 5ea66bbe64c3..781ba02b96c9 100644 --- a/source/isaaclab/isaaclab/__main__.py +++ b/source/isaaclab/isaaclab/__main__.py @@ -4,14 +4,10 @@ # SPDX-License-Identifier: BSD-3-Clause import argparse -import os -import re +import pathlib import sys -import textwrap -import tomllib - -from isaaclab.paths import ISAACLAB_ROOT +from isaaclab.utils.editor import build_extra_paths, resolve_isaacsim_dir, write_pyright_config VSCODE_SETTINGS_TEMPLATE = """ { @@ -21,11 +17,8 @@ "python.languageServer": "Pylance", "python.jediEnabled": false, - // Those paths are automatically filled by isaaclab (see: 'python -m isaaclab --help') + // This path is automatically filled by isaaclab "python.defaultInterpreterPath": "PYTHON.DEFAULTINTERPRETERPATH", - "python.analysis.extraPaths": [ - PYTHON.ANALYSIS.EXTRAPATHS - ], // Use "black" as a formatter "python.formatting.provider": "black", @@ -33,108 +26,38 @@ // Use "flake8" for linting "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, + "python.linting.flake8Enabled": true } """ -def generate_vscode_settings(): - def _mock_python_modules(ext_path: str, ext_name: str) -> None: - # parse config/extension.toml - cprint(f" |-- Parsing extension config ({ext_path})") - config_path = os.path.join(ext_path, "config", "extension.toml") - try: - with open(config_path, "rb") as f: - config = tomllib.load(f) - except Exception as e: - cprint(f" | |-- [Warning] {e}") - return - # get python modules - for item in config.get("python", {}).get("module", []): - if list(item.keys()) == ["name"]: - # skip tests - if item.get("name", "").endswith(".tests"): - continue - # mock __init__.py for each submodule (if not exists) - submodule_path = ext_path - for submodule in item.get("name", "").split("."): - init_path = os.path.join(submodule_path, "__init__.py") - if not os.path.isfile(init_path): - try: - cprint(f" |-- Mocking {init_path}") - with open(init_path, "w") as f: - f.write("# Generated by 'isaaclab' package") - except Exception as e: - cprint(f" | |-- [Warning] {e}") - continue - submodule_path = os.path.join(submodule_path, submodule) - - def _get_paths(base_path: str, mock_python_modules: bool = False) -> list[str]: - paths = [] - if os.path.isdir(base_path): - for folder in os.listdir(base_path): - folder_path = os.path.join(base_path, folder) - if os.path.isdir(folder_path): - paths.append(folder_path) - cprint(f"Registering extension: {folder_path}") - if mock_python_modules: - _mock_python_modules(folder_path, re.split(r"-\d+", folder)[0]) - return paths - - try: - import omni.kit_app # importing 'omni.kit_app' will bootstrap kernel - - kit_path = os.path.dirname(os.path.abspath(os.path.realpath(omni.kit_app.__file__))) - except ModuleNotFoundError: - print("Unable to find 'omniverse-kit' package") - # exit() - try: - import isaacsim - - isaacsim_path = os.path.dirname(os.path.abspath(os.path.realpath(isaacsim.__file__))) - except ModuleNotFoundError: - print("Unable to find 'isaacsim' package") - # exit() - - cwd = os.getcwd() - vscode_settings_path = os.path.join(cwd, ".vscode", "settings.json") - # check if .vscode/settings.json exists - if os.path.exists(vscode_settings_path): +def generate_vscode_settings(isaac_path: str | None = None, verbose: bool = False): + """Generate editor settings and a Pyright configuration in the current workspace. + + Args: + isaac_path: Explicit Isaac Sim installation path, or None to discover it. + verbose: Whether to print every generated Pyright search path. + """ + project_dir = pathlib.Path.cwd() + vscode_settings_path = project_dir / ".vscode" / "settings.json" + if vscode_settings_path.exists(): print(f"VS Code settings already exists: {vscode_settings_path}") if input("Overwrite? (y/N): ").lower() not in ["y", "yes"]: print("Cancelled: VS Code settings not overwritten") return - # get extensions paths - extensions_paths = [] - # - omniverse-kit - folder_path = os.path.join(kit_path, "kernel", "py") - if os.path.isdir(folder_path): - extensions_paths.append(folder_path) - for folder in ["exts", "extscore"]: - extensions_paths.extend(_get_paths(os.path.join(kit_path, folder), mock_python_modules=True)) - # - isaacsim - for folder in ["exts", "extscache", "extsDeprecated", "extsUser"]: - extensions_paths.extend(_get_paths(os.path.join(isaacsim_path, folder), mock_python_modules=True)) - # - isaaclab - isaaclab_path = str(ISAACLAB_ROOT) - for folder in ["source"]: - extensions_paths.extend(_get_paths(os.path.join(isaaclab_path, folder), mock_python_modules=True)) - - # update 'python.defaultInterpreterPath' - template = VSCODE_SETTINGS_TEMPLATE[:] - template = template.replace("PYTHON.DEFAULTINTERPRETERPATH", sys.executable) - - # update 'python.analysis.extraPaths' - content = "\n".join([f'"{path}",' for path in extensions_paths]) - content = textwrap.indent(content, prefix=" " * 8)[8:] - template = template.replace("PYTHON.ANALYSIS.EXTRAPATHS", content) - - # create .vscode/settings.json - os.makedirs(os.path.join(cwd, ".vscode"), exist_ok=True) - with open(vscode_settings_path, "w") as f: - f.write(template) - print("VS Code settings generated at", vscode_settings_path) + isaacsim_dir = resolve_isaacsim_dir(project_dir, isaac_path) + extra_paths = build_extra_paths(project_dir, isaacsim_dir) + write_pyright_config(project_dir, extra_paths) + if verbose: + for path in extra_paths: + print(f"Registered Pyright search path: {path}") + + settings = VSCODE_SETTINGS_TEMPLATE.replace("PYTHON.DEFAULTINTERPRETERPATH", pathlib.Path(sys.executable).as_posix()) + vscode_settings_path.parent.mkdir(parents=True, exist_ok=True) + vscode_settings_path.write_text(settings, encoding="utf-8") + print(f"VS Code settings generated at {vscode_settings_path}") + print(f"Pyright configuration generated at {project_dir / 'pyrightconfig.json'}") def main(): @@ -142,12 +65,16 @@ def main(): if len(sys.argv) > 1 and sys.argv[1] == "--generate-vscode-settings": parser = argparse.ArgumentParser() parser.add_argument("--generate-vscode-settings", action="store_true", help="Generate VS Code settings.") + parser.add_argument("--isaac_path", help="Absolute path to the Isaac Sim installation.") parser.add_argument("--verbose", action="store_true", help="Print discovered extension paths.") args = parser.parse_args() - - global cprint - cprint = print if args.verbose else lambda *args, **kwargs: None - generate_vscode_settings() + try: + if args.isaac_path or args.verbose: + generate_vscode_settings(isaac_path=args.isaac_path, verbose=args.verbose) + else: + generate_vscode_settings() + except ValueError as error: + parser.error(str(error)) return from isaaclab.cli import cli diff --git a/source/isaaclab/isaaclab/utils/editor.py b/source/isaaclab/isaaclab/utils/editor.py new file mode 100644 index 000000000000..4272d0d012e9 --- /dev/null +++ b/source/isaaclab/isaaclab/utils/editor.py @@ -0,0 +1,156 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Utilities for generating editor import paths for Isaac Lab projects.""" + +import importlib.metadata +import importlib.util +import json +import os +import pathlib +import re +import subprocess +import sys + + +def resolve_isaacsim_dir(project_dir: pathlib.Path, isaac_path: str | None = None) -> pathlib.Path | None: + """Resolve the Isaac Sim installation directory. + + Args: + project_dir: Project root containing an optional ``_isaac_sim`` link. + isaac_path: Explicit Isaac Sim path, or None to discover the installation. + + Returns: + The resolved installation directory, or None if Isaac Sim is unavailable. + + Raises: + ValueError: If an explicit path does not identify a directory. + """ + if isaac_path: + explicit_path = pathlib.Path(isaac_path).expanduser() + if not _is_isaacsim_dir(explicit_path): + raise ValueError(f"Not an Isaac Sim directory (missing .vscode/settings.json): {explicit_path}") + return explicit_path.resolve() + + env_path = os.environ.get("ISAAC_PATH") + if env_path and _is_isaacsim_dir(pathlib.Path(env_path)): + return pathlib.Path(env_path).resolve() + + probe = subprocess.run( + [sys.executable, "-c", "import isaacsim, os; print(os.environ.get('ISAAC_PATH', ''))"], + capture_output=True, + text=True, + check=False, + stdin=subprocess.DEVNULL, + ) + for line in reversed(probe.stdout.splitlines()): + candidate = pathlib.Path(line.strip()).expanduser() + if line.strip() and _is_isaacsim_dir(candidate): + return candidate.resolve() + + fallback = project_dir / "_isaac_sim" + return fallback.resolve() if _is_isaacsim_dir(fallback) else None + + +def read_isaacsim_extra_paths(isaacsim_dir: pathlib.Path | None) -> list[pathlib.Path]: + """Read Isaac Sim's Python extension paths. + + Args: + isaacsim_dir: Isaac Sim installation directory, or None. + + Returns: + Absolute extension search paths. + """ + if isaacsim_dir is None: + print("[WARN] Isaac Sim was not found; simulator extension paths were not added.") + return [] + + settings_file = isaacsim_dir / ".vscode" / "settings.json" + if not settings_file.is_file(): + print(f"[WARN] Isaac Sim VS Code settings were not found: {settings_file}") + return [] + + settings = settings_file.read_text(encoding="utf-8") + match = re.search(r'"python\.analysis\.extraPaths"\s*:\s*\[(.*?)\]', settings, flags=re.DOTALL) + if match is None: + print(f"[WARN] python.analysis.extraPaths was not found in {settings_file}") + return [] + + paths = [] + for encoded_path in re.findall(r'"((?:\\.|[^"\\])*)"', match.group(1)): + path = pathlib.Path(json.loads(f'"{encoded_path}"')) + paths.append(path if path.is_absolute() else isaacsim_dir / path) + return paths + + +def find_isaaclab_package_paths() -> list[pathlib.Path]: + """Find Isaac Lab package roots visible to the active interpreter. + + Returns: + Import roots for installed Isaac Lab packages. + """ + paths = [] + package_names = sorted(name for name in importlib.metadata.packages_distributions() if name.startswith("isaaclab")) + for package_name in package_names: + spec = importlib.util.find_spec(package_name) + if spec is None: + continue + if spec.submodule_search_locations: + paths.extend(pathlib.Path(location).parent for location in spec.submodule_search_locations) + elif spec.origin: + paths.append(pathlib.Path(spec.origin).parent) + return paths + + +def build_extra_paths(project_dir: pathlib.Path, isaacsim_dir: pathlib.Path | None) -> list[str]: + """Build Pyright search paths for simulator, local, and installed packages. + + Args: + project_dir: Project root used to discover local ``source/*`` or ``src`` packages. + isaacsim_dir: Isaac Sim installation directory, or None. + + Returns: + Deduplicated paths, relative to the project where practical. + """ + paths = read_isaacsim_extra_paths(isaacsim_dir) + source_dir = project_dir / "source" + if source_dir.is_dir(): + paths.extend(path for path in sorted(source_dir.iterdir()) if path.is_dir()) + src_dir = project_dir / "src" + if src_dir.is_dir(): + paths.append(src_dir) + paths.extend(find_isaaclab_package_paths()) + + formatted_paths = [] + seen = set() + resolved_project_dir = project_dir.resolve() + for path in paths: + resolved_path = path.resolve() + try: + formatted_path = resolved_path.relative_to(resolved_project_dir).as_posix() + except ValueError: + formatted_path = resolved_path.as_posix() + if formatted_path not in seen: + seen.add(formatted_path) + formatted_paths.append(formatted_path) + return formatted_paths + + +def write_pyright_config(project_dir: pathlib.Path, extra_paths: list[str]): + """Write a machine-local Pyright configuration that preserves project policy. + + Args: + project_dir: Project root where the configuration is written. + extra_paths: Additional import search paths. + """ + config: dict[str, object] = {"extraPaths": extra_paths} + if (project_dir / "pyproject.toml").is_file(): + config["extends"] = "./pyproject.toml" + (project_dir / "pyrightconfig.json").write_text(json.dumps(config, indent=4) + "\n", encoding="utf-8") + + +def _is_isaacsim_dir(path: pathlib.Path) -> bool: + """Check whether a directory contains the settings used for extension discovery.""" + return path.is_dir() and (path / ".vscode" / "settings.json").is_file() diff --git a/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py b/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py index 9a8951730e8f..04735d78beac 100644 --- a/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py +++ b/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py @@ -49,6 +49,21 @@ def test_legacy_vscode_option_uses_compatibility_dispatcher(): generate.assert_called_once_with() +def test_installed_vscode_generator_uses_pyright_config(tmp_path, monkeypatch): + """The installed workflow must not emit the conflicting Pylance extraPaths setting.""" + monkeypatch.chdir(tmp_path) + with ( + mock.patch.object(package_main, "resolve_isaacsim_dir", return_value=None), + mock.patch.object(package_main, "build_extra_paths", return_value=["/sim/exts/example"]), + mock.patch.object(package_main, "write_pyright_config") as write_pyright_config, + ): + package_main.generate_vscode_settings() + + settings = (tmp_path / ".vscode" / "settings.json").read_text() + assert "python.analysis.extraPaths" not in settings + write_pyright_config.assert_called_once_with(tmp_path, ["/sim/exts/example"]) + + @pytest.mark.parametrize( ("command", "runner"), [ diff --git a/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip b/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip new file mode 100644 index 000000000000..3259ba2642e0 --- /dev/null +++ b/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip @@ -0,0 +1 @@ +The template regression suite covers editor configuration generation. diff --git a/source/isaaclab_rl/test/test_template_generator.py b/source/isaaclab_rl/test/test_template_generator.py index 65bc3d786ae1..5a7b938bc31e 100644 --- a/source/isaaclab_rl/test/test_template_generator.py +++ b/source/isaaclab_rl/test/test_template_generator.py @@ -7,6 +7,7 @@ import ast import importlib.util +import json import pkgutil import subprocess import sys @@ -17,6 +18,8 @@ import pytest import tomllib +from isaaclab.utils import editor as editor_utils + ROOT_DIR = Path(__file__).resolve().parents[3] TEMPLATE_TOOL_DIR = ROOT_DIR / "tools" / "template" sys.path.insert(0, str(TEMPLATE_TOOL_DIR)) @@ -302,11 +305,63 @@ def test_external_project_uses_src_layout_and_installed_isaaclab_commands(tmp_pa "build-backend": "uv_build", } assert package["project"]["dependencies"] == ["isaaclab[rsl-rl,skrl]"] + assert package["tool"]["pyright"] == { + "include": ["src", "scripts", "tests"], + "exclude": ["**/__pycache__", "**/logs", ".git", ".venv", ".vscode"], + "typeCheckingMode": "basic", + } assert package["project"]["entry-points"]["isaaclab.tasks"] == {project_name: f"{project_name}.tasks"} assert package["tool"]["uv"]["build-backend"]["module-name"] == project_name assert (project_dir / "src" / project_name / "__init__.py").is_file() assert not (project_dir / "source").exists() assert {path.name for path in (project_dir / "scripts").iterdir()} == {"list_envs.py"} + assert "pyrightconfig.json" in (project_dir / ".gitignore").read_text() + assert ( + "python.analysis.extraPaths" not in (project_dir / ".vscode" / "tools" / "settings.template.json").read_text() + ) + assert (project_dir / ".vscode" / "tools" / "setup_vscode.py").read_bytes() == ( + ROOT_DIR / ".vscode" / "tools" / "setup_vscode.py" + ).read_bytes() + + +def test_editor_setup_combines_simulator_local_and_installed_paths(tmp_path, monkeypatch): + """The generated Pyright child config must preserve project policy and cover every installation mode.""" + project_dir = tmp_path / "project" + (project_dir / "source" / "local_package").mkdir(parents=True) + (project_dir / "src" / "generated_project").mkdir(parents=True) + (project_dir / "pyproject.toml").write_text('[tool.pyright]\ntypeCheckingMode = "basic"\n') + isaacsim_dir = tmp_path / "isaacsim" + (isaacsim_dir / ".vscode").mkdir(parents=True) + (isaacsim_dir / ".vscode" / "settings.json").write_text( + '{"python.analysis.extraPaths": ["exts/isaacsim.core.api", "extscache/omni.kit.foo"]}' + ) + installed_root = tmp_path / "editable" / "isaaclab" + installed_root.mkdir(parents=True) + + monkeypatch.setattr(editor_utils, "find_isaaclab_package_paths", lambda: [installed_root]) + + extra_paths = editor_utils.build_extra_paths(project_dir, isaacsim_dir) + editor_utils.write_pyright_config(project_dir, extra_paths) + config = json.loads((project_dir / "pyrightconfig.json").read_text()) + + assert config["extends"] == "./pyproject.toml" + assert config["extraPaths"] == [ + (isaacsim_dir / "exts" / "isaacsim.core.api").as_posix(), + (isaacsim_dir / "extscache" / "omni.kit.foo").as_posix(), + "source/local_package", + "src", + installed_root.as_posix(), + ] + + +@pytest.mark.parametrize("path_exists", [False, True]) +def test_editor_setup_rejects_invalid_explicit_isaac_sim_path(tmp_path, path_exists): + """An invalid user-selected installation must not silently select a different Isaac Sim.""" + invalid_path = tmp_path / "invalid" + if path_exists: + invalid_path.mkdir() + with pytest.raises(ValueError, match="Not an Isaac Sim directory"): + editor_utils.resolve_isaacsim_dir(tmp_path, str(invalid_path)) def _all_libraries() -> list[dict]: diff --git a/tools/template/generator.py b/tools/template/generator.py index eecb5381c6d8..c4aefcf6bff3 100644 --- a/tools/template/generator.py +++ b/tools/template/generator.py @@ -9,7 +9,7 @@ import subprocess import jinja2 -from common import MULTI_AGENT_ALGORITHMS, SINGLE_AGENT_ALGORITHMS, TASKS_DIR, TEMPLATE_DIR +from common import MULTI_AGENT_ALGORITHMS, ROOT_DIR, SINGLE_AGENT_ALGORITHMS, TASKS_DIR, TEMPLATE_DIR jinja_env = jinja2.Environment( loader=jinja2.FileSystemLoader(TEMPLATE_DIR), @@ -199,6 +199,10 @@ def _external(specification: dict) -> None: print(" |-- Copying vscode files...") vscode_dir = os.path.join(project_dir, ".vscode") shutil.copytree(os.path.join(TEMPLATE_DIR, "external", ".vscode"), vscode_dir, dirs_exist_ok=True) + shutil.copyfile( + os.path.join(ROOT_DIR, ".vscode", "tools", "setup_vscode.py"), + os.path.join(vscode_dir, "tools", "setup_vscode.py"), + ) template = jinja_env.get_template("external/.vscode/tasks.json") _write_file(os.path.join(vscode_dir, "tasks.json"), content=template.render(**specification)) template = jinja_env.get_template("external/.vscode/tools/launch.template.json") diff --git a/tools/template/templates/external/.gitignore b/tools/template/templates/external/.gitignore index d7e3d459d3d1..d8f5bdf5325e 100644 --- a/tools/template/templates/external/.gitignore +++ b/tools/template/templates/external/.gitignore @@ -14,3 +14,4 @@ wandb/ .vscode/settings.json .vscode/launch.json +pyrightconfig.json diff --git a/tools/template/templates/external/.vscode/tools/settings.template.json b/tools/template/templates/external/.vscode/tools/settings.template.json index c1528d65dd73..d66c6f4f0322 100644 --- a/tools/template/templates/external/.vscode/tools/settings.template.json +++ b/tools/template/templates/external/.vscode/tools/settings.template.json @@ -72,8 +72,5 @@ }, "[restructuredtext]": { "editor.tabSize": 2 - }, - // Python extra paths - // Note: this is filled up when vscode is set up for the first time - "python.analysis.extraPaths": [] + } } diff --git a/tools/template/templates/external/.vscode/tools/setup_vscode.py b/tools/template/templates/external/.vscode/tools/setup_vscode.py deleted file mode 100644 index 9fcd115748ec..000000000000 --- a/tools/template/templates/external/.vscode/tools/setup_vscode.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""This script sets up the vs-code settings for the Isaac Lab project. - -This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into -the ".vscode/settings.json" file. - -This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path -when the "setup_python_env.sh" is run as part of the vs-code launch configuration. -""" - -import os -import pathlib -import re -import subprocess -import sys - -ISAACLAB_DIR = pathlib.Path(__file__).parents[2] -"""Path to the Isaac Lab directory.""" - -# Try to find IsaacSim dir -_isaacsim_probe = subprocess.run( - [sys.executable, "-c", "import isaacsim; import os; print(os.environ.get('ISAAC_PATH', ''))"], - capture_output=True, - text=True, - check=False, - # avoid EULA prompt - stdin=subprocess.DEVNULL, -) -if _isaacsim_probe.returncode == 0 and _isaacsim_probe.stdout.strip(): - isaacsim_dir = _isaacsim_probe.stdout.strip() -else: - isaacsim_dir = os.path.join(ISAACLAB_DIR, "_isaac_sim") - -# check if the isaac-sim directory exists -if not os.path.exists(isaacsim_dir): - print( - f"[WARN] Could not find the isaac-sim directory: {isaacsim_dir}." - "\n\tIsaac Sim does not appear to be installed. VS Code settings will be generated" - "\n\twithout Isaac Sim extra paths." - ) - isaacsim_dir = "" - -ISAACSIM_DIR = isaacsim_dir -"""Path to the isaac-sim directory.""" - - -def overwrite_python_analysis_extra_paths(isaaclab_settings: str) -> str: - """Overwrite the python.analysis.extraPaths in the Isaac Lab settings file. - - The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the - "{ISAACSIM_DIR}/.vscode/settings.json" file. - - If the isaac-sim settings file does not exist, the extraPaths are not overwritten. - - Args: - isaaclab_settings: The settings string to use as template. - - Returns: - The settings string with overwritten python analysis extra paths. - """ - # isaac-sim settings - isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json") - - # we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions - # if this file does not exist, we will not add any extra paths - if ISAACSIM_DIR and os.path.exists(isaacsim_vscode_filename): - # read the path names from the isaac-sim settings file - with open(isaacsim_vscode_filename) as f: - vscode_settings = f.read() - # extract the path names - # search for the python.analysis.extraPaths section and extract the contents - settings = re.search( - r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL - ) - settings = settings.group(0) - settings = settings.split('"python.analysis.extraPaths": [')[-1] - settings = settings.split("]")[0] - - # read the path names from the isaac-sim settings file - path_names = settings.split(",") - path_names = [path_name.strip().strip('"') for path_name in path_names] - path_names = [path_name for path_name in path_names if len(path_name) > 0] - - # change the path names to be relative to the Isaac Lab directory - rel_path = os.path.relpath(ISAACSIM_DIR, ISAACLAB_DIR) - path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names] - else: - path_names = [] - - # add the generated project's source directory - path_names.append('"${workspaceFolder}/src"') - - # combine them into a single string - path_names = ",\n\t\t".expandtabs(4).join(path_names) - # deal with the path separator being different on Windows and Unix - path_names = path_names.replace("\\", "/") - - # replace the path names in the Isaac Lab settings file with the path names parsed - isaaclab_settings = re.sub( - r"\"python.analysis.extraPaths\": \[.*?\]", - '"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4), - isaaclab_settings, - flags=re.DOTALL, - ) - # return the Isaac Lab settings string - return isaaclab_settings - - -def overwrite_default_python_interpreter(isaaclab_settings: str) -> str: - """Overwrite the default python interpreter in the Isaac Lab settings file. - - The default python interpreter is replaced with the path to the python interpreter used by the - isaac-sim project. This is necessary because the default python interpreter is the one shipped with - isaac-sim. - - Args: - isaaclab_settings: The settings string to use as template. - - Returns: - The settings string with overwritten default python interpreter. - """ - # read executable name - python_exe = sys.executable.replace("\\", "/") - - # We make an exception for replacing the default interpreter if the - # path (/kit/python/bin/python3) indicates that we are using a local/container - # installation of IsaacSim. We will preserve the calling script as the default, python.sh. - # We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH - # (among other envars) that we need for all of our dependencies to be accessible. - if "kit/python/bin/python3" in python_exe: - return isaaclab_settings - # replace the default python interpreter in the Isaac Lab settings file with the path to the - # python interpreter in the Isaac Lab directory - isaaclab_settings = re.sub( - r"\"python.defaultInterpreterPath\": \".*?\"", - f'"python.defaultInterpreterPath": "{python_exe}"', - isaaclab_settings, - flags=re.DOTALL, - ) - # return the Isaac Lab settings file - return isaaclab_settings - - -def main(): - # Isaac Lab template settings - isaaclab_vscode_template_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "settings.template.json") - # make sure the Isaac Lab template settings file exists - if not os.path.exists(isaaclab_vscode_template_filename): - raise FileNotFoundError( - f"Could not find the Isaac Lab template settings file: {isaaclab_vscode_template_filename}" - ) - # read the Isaac Lab template settings file - with open(isaaclab_vscode_template_filename) as f: - isaaclab_template_settings = f.read() - - # overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names - isaaclab_settings = overwrite_python_analysis_extra_paths(isaaclab_template_settings) - # overwrite the default python interpreter in the Isaac Lab settings file with the path to the - # python interpreter used to call this script - isaaclab_settings = overwrite_default_python_interpreter(isaaclab_settings) - - # add template notice to the top of the file - header_message = ( - "// This file is a template and is automatically generated by the setup_vscode.py script.\n" - "// Do not edit this file directly.\n" - "// \n" - f"// Generated from: {isaaclab_vscode_template_filename}\n" - ) - isaaclab_settings = header_message + isaaclab_settings - - # write the Isaac Lab settings file - isaaclab_vscode_filename = os.path.join(ISAACLAB_DIR, ".vscode", "settings.json") - with open(isaaclab_vscode_filename, "w") as f: - f.write(isaaclab_settings) - - # copy the launch.json file if it doesn't exist - isaaclab_vscode_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "launch.json") - isaaclab_vscode_template_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "launch.template.json") - if not os.path.exists(isaaclab_vscode_launch_filename): - # read template launch settings - with open(isaaclab_vscode_template_launch_filename) as f: - isaaclab_template_launch_settings = f.read() - # add header - header_message = header_message.replace( - isaaclab_vscode_template_filename, isaaclab_vscode_template_launch_filename - ) - isaaclab_launch_settings = header_message + isaaclab_template_launch_settings - # write the Isaac Lab launch settings file - with open(isaaclab_vscode_launch_filename, "w") as f: - f.write(isaaclab_launch_settings) - - -if __name__ == "__main__": - main() diff --git a/tools/template/templates/external/README.md b/tools/template/templates/external/README.md index 01d0c188b59b..44ec3f479c59 100644 --- a/tools/template/templates/external/README.md +++ b/tools/template/templates/external/README.md @@ -75,12 +75,39 @@ The test helpers under `source/isaaclab_tasks/test` in the Isaac Lab repository `isaaclab_tasks` package. Keep test fixtures in this project and use public Isaac Lab APIs. If you copy `env_test_utils.py`, it becomes vendored code whose upstream changes you must track. -To configure VS Code, run the `setup_python_env` task or invoke its command directly: +To configure VS Code or Cursor, run the `setup_python_env` task or invoke its command directly: ```bash uv run python .vscode/tools/setup_vscode.py ``` +The setup command selects the active interpreter and generates a git-ignored `pyrightconfig.json`. The generated +configuration inherits the project's checked-in Pyright settings and adds the Isaac Sim extensions, project `src` root, +and any Isaac Lab packages discovered in the active Python environment. This supports both Pylance in VS Code and +basedpyright in Cursor. + +In VS Code, use Pylance and select the interpreter that ran the setup command. In Cursor, install the +[basedpyright extension](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) instead of +Pylance, select the same interpreter, and reload the window. Both language servers read `pyrightconfig.json`. + +When using the `isaacsim` extra, include it while generating the editor configuration so the command can discover the +Isaac Sim installation: + +```bash +uv run --extra isaacsim python .vscode/tools/setup_vscode.py +``` + +For an Isaac Sim binaries installation that is not available in the project environment, run the setup with its Python +launcher instead: + +```bash +# Linux +/python.sh .vscode/tools/setup_vscode.py --isaac_path + +# Windows +\python.bat .vscode\tools\setup_vscode.py --isaac_path +``` + {% if include_ui_extension %} ## Isaac Sim UI extension @@ -91,5 +118,6 @@ Add the project root to the Isaac Sim Extension Manager search paths, refresh, a {% endif %} ## Troubleshooting -If Pylance cannot resolve simulator modules, run the VS Code setup command above and reload the window. If indexing uses -too much memory, remove unused simulator extension paths from `.vscode/settings.json`. +If Pylance or basedpyright cannot resolve modules, confirm that the selected interpreter matches the one used to run the +setup command, then reload the editor window. To add a missing extension or reduce indexing memory, edit the `extraPaths` +array in the root `pyrightconfig.json`; remove simulator extension directories that the project does not use. diff --git a/tools/template/templates/external/pyproject.toml b/tools/template/templates/external/pyproject.toml index ae1b0ebb9d74..ff5a5acaa150 100644 --- a/tools/template/templates/external/pyproject.toml +++ b/tools/template/templates/external/pyproject.toml @@ -73,6 +73,11 @@ isaaclab = ["isaaclab", "isaaclab_newton", "isaaclab_ov", "isaaclab_physx"] [tool.ruff.format] docstring-code-format = true +[tool.pyright] +include = ["src", "scripts", "tests"] +exclude = ["**/__pycache__", "**/logs", ".git", ".venv", ".vscode"] +typeCheckingMode = "basic" + [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra"