Skip to content

Commit

Permalink
feat: add "blender" and "gdb" dev_environments (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
rwols authored Aug 23, 2024
1 parent db0b79a commit 8789f85
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 16 deletions.
12 changes: 12 additions & 0 deletions LSP-pyright.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,19 @@
// so ST package dependecies can be resolved by the LSP server.
// - "sublime_text_33": Similar to "sublime_text" but Python 3.3 forced.
// - "sublime_text_38": Similar to "sublime_text" but Python 3.8 forced.
// - "blender": Suitable for people who are developing Blender add-ons. `sys.path` from Blender's embedded
// Python interpreter will be added into "python.analysis.extraPaths". Note that this requires
// invoking Blender, headless, to query the additional Python paths. The setting
// "pyright.dev_environment_blender_binary" controls which executable to call to invoke Blender.
// - "gdb": Suitable for people who are developing GDB automation scripts. `sys.path` from GDB's embedded
// Python interpreter will be added into "python.analysis.extraPaths". Note that this requires invoking
// GDB, in batch mode, to query the additional Python paths. The setting
// "pyright.dev_environment_gdb_binary" controls which exectuable to call to invoke GDB.
"pyright.dev_environment": "",
// When the predefined setup is "blender", invoke this binary to query the additional search paths.
"pyright.dev_environment.blender.binary": "blender",
// When the predefined setup is "gdb", invoke this binary to query the additional search paths.
"pyright.dev_environment.gdb.binary": "gdb",
// Offer auto-import completions.
"python.analysis.autoImportCompletions": true,
// Automatically add common search paths like 'src'?
Expand Down
90 changes: 79 additions & 11 deletions plugin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import re
import shutil
import sys
import tempfile
import weakref
from dataclasses import dataclass
from pathlib import Path
from typing import Any, cast
from typing import Any, Callable, cast

import jmespath
import sublime
Expand All @@ -19,8 +20,9 @@
from sublime_lib import ResourcePath

from .constants import PACKAGE_NAME
from .log import log_info, log_warning
from .log import log_error, log_info, log_warning
from .template import load_string_template
from .utils import run_shell_command
from .virtual_env.helpers import find_venv_by_finder_names, find_venv_by_python_executable
from .virtual_env.venv_finder import BaseVenvInfo, get_finder_name_mapping

Expand Down Expand Up @@ -89,16 +91,22 @@ def on_settings_changed(self, settings: DottedDict) -> None:
super().on_settings_changed(settings)

dev_environment = settings.get("pyright.dev_environment")
extraPaths: list[str] = settings.get("python.analysis.extraPaths") or []
extra_paths: list[str] = settings.get("python.analysis.extraPaths") or []

if dev_environment in {"sublime_text", "sublime_text_33", "sublime_text_38"}:
py_ver = self.detect_st_py_ver(dev_environment)
# add package dependencies into "python.analysis.extraPaths"
extraPaths.extend(self.find_package_dependency_dirs(py_ver))

settings.set("python.analysis.extraPaths", extraPaths)

self.update_status_bar_text()
try:
if dev_environment.startswith("sublime_text"):
py_ver = self.detect_st_py_ver(dev_environment)
# add package dependencies into "python.analysis.extraPaths"
extra_paths.extend(self.find_package_dependency_dirs(py_ver))
elif dev_environment == "blender":
extra_paths.extend(self.find_blender_paths(settings))
elif dev_environment == "gdb":
extra_paths.extend(self.find_gdb_paths(settings))
settings.set("python.analysis.extraPaths", extra_paths)
except Exception as ex:
log_error(f"failed to update extra paths for dev environment {dev_environment}: {ex}")
finally:
self.update_status_bar_text()

@classmethod
def on_pre_start(
Expand Down Expand Up @@ -254,6 +262,66 @@ def find_package_dependency_dirs(self, py_ver: tuple[int, int] = (3, 3)) -> list

return list(filter(os.path.isdir, dep_dirs))

@classmethod
def _print_print_sys_paths(cls, sink: Callable[[str], None]) -> None:
sink("import sys")
sink("import json")
sink('json.dump({"executable": sys.executable, "paths": sys.path}, sys.stdout)')

@classmethod
def _get_dev_environment_binary(cls, settings: DottedDict, name: str) -> str:
return settings.get(f"settings.dev_environment.{name}.binary") or name

@classmethod
def _check_json_is_dict(cls, name: str, output_dict: Any) -> dict[str, Any]:
if not isinstance(output_dict, dict):
raise RuntimeError(f"unexpected output when calling {name}; expected JSON dict")
return output_dict

@classmethod
def find_blender_paths(cls, settings: DottedDict) -> list[str]:
filename = "print_sys_path.py"
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, filename)
with open(filepath, "w") as fp:

def out(line: str) -> None:
print(line, file=fp)

cls._print_print_sys_paths(out)
out("exit(0)")
args = (cls._get_dev_environment_binary(settings, "blender"), "--background", "--python", filepath)
result = run_shell_command(args, shell=False)
if result is None or result[2] != 0:
raise RuntimeError("failed to run command")
# Blender prints a bunch of general information to stdout before printing the output of the python
# script. We want to ignore that initial information. We do that by finding the start of the JSON
# dict. This is a bit hacky and there must be a better way.
index = result[0].find('\n{"')
if index == -1:
raise RuntimeError("unexpected output when calling blender")
return cls._check_json_is_dict("blender", json.loads(result[0][index:].strip()))["paths"]

@classmethod
def find_gdb_paths(cls, settings: DottedDict) -> list[str]:
filename = "print_sys_path.commands"
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, filename)
with open(filepath, "w") as fp:

def out(line: str) -> None:
print(line, file=fp)

out("python")
cls._print_print_sys_paths(out)
out("end")
out("exit")
args = (cls._get_dev_environment_binary(settings, "gdb"), "--batch", "--command", filepath)
result = run_shell_command(args, shell=False)
if result is None or result[2] != 0:
raise RuntimeError("failed to run command")
return cls._check_json_is_dict("gdb", json.loads(result[0].strip()))["paths"]

@classmethod
def parse_server_version(cls) -> str:
lock_file_content = sublime.load_resource(f"Packages/{PACKAGE_NAME}/language-server/package-lock.json")
Expand Down
8 changes: 5 additions & 3 deletions plugin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
from collections.abc import Generator, Iterable
from pathlib import Path
from typing import Any, TypeVar
from typing import Any, Sequence, TypeVar

from .log import log_error

Expand Down Expand Up @@ -60,12 +60,14 @@ def get_default_startupinfo() -> Any:
return None


def run_shell_command(command: str, *, cwd: str | Path | None = None) -> tuple[str, str, int] | None:
def run_shell_command(
command: str | Sequence[str], *, cwd: str | Path | None = None, shell: bool = True
) -> tuple[str, str, int] | None:
try:
proc = subprocess.Popen(
command,
cwd=cwd,
shell=True,
shell=shell,
startupinfo=get_default_startupinfo(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
Expand Down
18 changes: 16 additions & 2 deletions sublime-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,29 @@
"",
"sublime_text",
"sublime_text_33",
"sublime_text_38"
"sublime_text_38",
"blender",
"gdb"
],
"markdownEnumDescriptions": [
"No modifications applied.",
"Suitable for people who are developing ST python plugins. The Python version which the developed plugin runs on will be used. - `sys.path` from the plugin_host will be added into \"python.analysis.extraPaths\" so that ST package dependencies can be resolved by the LSP server.",
"Similar to \"sublime_text\" but Python 3.3 forced.",
"Similar to \"sublime_text\" but Python 3.8 forced."
"Similar to \"sublime_text\" but Python 3.8 forced.",
"Suitable for people who are developing Blender add-ons. `sys.path` from Blender's embedded Python interpreter will be added into \"python.analysis.extraPaths\". Note that this requires invoking Blender, headless, to query the additional Python paths. The setting \"pyright.dev_environment_blender_binary\" controls which executable to call to invoke Blender.",
"Suitable for people who are developing GDB automation scripts. `sys.path` from GDB's embedded Python interpreter will be added into \"python.analysis.extraPaths\". Note that this requires invoking GDB, in batch mode, to query the additional Python paths. The setting \"pyright.dev_environment_gdb_binary\" controls which exectuable to call to invoke GDB."
]
},
"pyright.dev_environment.blender.binary": {
"default": "blender",
"description": "When the predefined setup is \"blender\", invoke this binary to query the additional search paths.",
"type": "string"
},
"pyright.dev_environment.gdb.binary": {
"default": "gdb",
"description": "When the predefined setup is \"gdb\", invoke this binary to query the additional search paths.",
"type": "string"
},
"pyright.disableLanguageServices": {
"default": false,
"description": "Disables type completion, definitions, and references.",
Expand Down

0 comments on commit 8789f85

Please sign in to comment.