vscode-common-python-lsp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+ """Path utilities for VS Code Python tool extensions."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import fnmatch
8
+ import functools
9
+ import os
10
+ import pathlib
11
+ import site
12
+ import sys
13
+ import sysconfig
14
+ import threading
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ # Save the working directory used when loading this module
19
+ SERVER_CWD = os.getcwd()
20
+ CWD_LOCK = threading.Lock()
21
+
22
+
23
+ class PythonFileKind(Enum):
24
+ """Classification of a Python file by its location in the environment."""
25
+
26
+ STDLIB = "stdlib"
27
+ USER_SITE = "user_site"
28
+ SYSTEM_SITE = "system_site"
29
+
30
+
31
+ def as_list(content: Any) -> list[Any]:
32
+ """Ensures we always get a list."""
33
+ if isinstance(content, (list, tuple)):
34
+ return list(content)
35
+ return [content]
36
+
37
+
38
+ def get_sys_config_paths() -> list[str]:
39
+ """Returns Python installation paths from sysconfig.get_paths().
40
+
41
+ Uses the broader filter (not in data/platdata/scripts) to match
42
+ black, isort, mypy, and pylint. Includes stdlib, platstdlib,
43
+ purelib, platlib, include, and platinclude.
44
+ """
45
+ return [
46
+ path
47
+ for group, path in sysconfig.get_paths().items()
48
+ if group not in ["data", "platdata", "scripts"]
49
+ ]
50
+
51
+
52
+ def get_extensions_dir() -> list[str]:
53
+ """Returns the VS Code extensions folder (under ~/.vscode or ~/.vscode-server).
54
+
55
+ The path is calculated relative to this file, because users can launch
56
+ VS Code with a custom extensions folder using the --extensions-dir argument.
57
+ """
58
+ path = pathlib.Path(__file__).parent.parent.parent.parent
59
+ # ^ bundled ^ extensions
60
+ # tool <extension>
61
+ if path.name == "extensions":
62
+ return [os.fspath(path)]
63
+ return []
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Path classification (lazy, cached)
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ @functools.lru_cache(maxsize=1)
72
+ def _get_stdlib_roots() -> frozenset[pathlib.Path]:
73
+ """Resolved stdlib and platstdlib paths plus VS Code extensions dir.
74
+
75
+ Uses only the strict stdlib/platstdlib sysconfig groups so that
76
+ purelib/platlib (which point to site-packages) are excluded.
77
+
78
+ The result is computed lazily on the first call, then cached for
79
+ the lifetime of the process (paths do not change at runtime).
80
+ """
81
+ roots: set[pathlib.Path] = set()
82
+ for group in ("stdlib", "platstdlib"):
83
+ p = sysconfig.get_path(group)
84
+ if p:
85
+ try:
86
+ roots.add(pathlib.Path(p).resolve())
87
+ except OSError:
88
+ pass
89
+ for p in get_extensions_dir():
90
+ try:
91
+ roots.add(pathlib.Path(p).resolve())
92
+ except OSError:
93
+ pass
94
+ return frozenset(roots)
95
+
96
+
97
+ @functools.lru_cache(maxsize=1)
98
+ def _get_user_site_root() -> pathlib.Path | None:
99
+ """Resolved user site-packages path, or None.
100
+
101
+ The result is computed lazily on the first call, then cached for
102
+ the lifetime of the process.
103
+ """
104
+ try:
105
+ raw = site.getusersitepackages()
106
+ except Exception:
107
+ return None
108
+ if raw:
109
+ try:
110
+ return pathlib.Path(raw).resolve()
111
+ except OSError:
112
+ pass
113
+ return None
114
+
115
+
116
+ @functools.lru_cache(maxsize=1)
117
+ def _get_system_site_roots() -> frozenset[pathlib.Path]:
118
+ """All system site-packages roots.
119
+
120
+ Includes purelib/platlib from sysconfig plus every entry from
121
+ ``site.getsitepackages()``. On Windows Store Python the latter
122
+ includes the base installation directory, which is a *parent* of
123
+ the stdlib root — the classifier handles this by checking stdlib
124
+ first.
125
+
126
+ The result is computed lazily on the first call, then cached for
127
+ the lifetime of the process.
128
+ """
129
+ roots: set[pathlib.Path] = set()
130
+ for group in ("purelib", "platlib"):
131
+ p = sysconfig.get_path(group)
132
+ if p:
133
+ try:
134
+ roots.add(pathlib.Path(p).resolve())
135
+ except OSError:
136
+ pass
137
+ try:
138
+ for p in as_list(site.getsitepackages()):
139
+ try:
140
+ roots.add(pathlib.Path(p).resolve())
141
+ except OSError:
142
+ pass
143
+ except Exception:
144
+ pass
145
+ return frozenset(roots)
146
+
147
+
148
+ def classify_python_file(file_path: str) -> PythonFileKind | None:
149
+ """Classify a file as stdlib, user site-packages, or system site-packages.
150
+
151
+ Returns None if the file does not belong to any known Python
152
+ installation path. Resolution order:
153
+
154
+ 1. User site-packages (most specific user path).
155
+ 2. Stdlib roots *excluding* ``site-packages``/``dist-packages``
156
+ subdirectories — checked before system site-packages because on
157
+ some platforms (Windows Store Python) ``site.getsitepackages()``
158
+ includes the base installation directory, which is a parent of
159
+ the stdlib root.
160
+ 3. System site-packages (purelib, platlib, broad base roots).
161
+ """
162
+ try:
163
+ resolved = pathlib.Path(file_path).resolve()
164
+ except OSError:
165
+ return None
166
+
167
+ # 1. User site-packages (most specific)
168
+ user_site = _get_user_site_root()
169
+ if user_site and resolved.is_relative_to(user_site):
170
+ return PythonFileKind.USER_SITE
171
+
172
+ parts = resolved.parts
173
+ has_site_packages = "site-packages" in parts or "dist-packages" in parts
174
+
175
+ # 2. Stdlib (only if the path does NOT traverse a site-packages dir)
176
+ if not has_site_packages:
177
+ for root in _get_stdlib_roots():
178
+ if resolved.is_relative_to(root):
179
+ return PythonFileKind.STDLIB
180
+
181
+ # 3. System site-packages (includes broad roots like the base dir)
182
+ for root in _get_system_site_roots():
183
+ if resolved.is_relative_to(root):
184
+ return PythonFileKind.SYSTEM_SITE
185
+
186
+ return None
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Path comparison and normalization
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ def is_same_path(
195
+ file_path1: str, file_path2: str, resolve_symlinks: bool = False
196
+ ) -> bool:
197
+ """Returns true if two paths are the same.
198
+
199
+ When *resolve_symlinks* is True, both paths are resolved through the
200
+ filesystem first so that symlinked paths compare equal (mirrors the
201
+ behaviour required by mypy). Falls back to lexical comparison on
202
+ OSError (e.g. the file does not exist yet).
203
+ """
204
+ p1, p2 = pathlib.Path(file_path1), pathlib.Path(file_path2)
205
+ if resolve_symlinks:
206
+ try:
207
+ return p1.resolve() == p2.resolve()
208
+ except OSError:
209
+ pass
210
+ return p1 == p2
211
+
212
+
213
+ def normalize_path(file_path: str, resolve_symlinks: bool = True) -> str:
214
+ """Returns normalized path."""
215
+ path = pathlib.Path(file_path)
216
+ if resolve_symlinks:
217
+ path = path.resolve()
218
+ return str(path)
219
+
220
+
221
+ def is_current_interpreter(executable: str) -> bool:
222
+ """Returns true if the executable path is same as the current interpreter."""
223
+ return is_same_path(executable, sys.executable)
224
+
225
+
226
+ def get_relative_path(file_path: str, workspace_root: str) -> str:
227
+ """Returns the file path relative to the workspace root.
228
+
229
+ Falls back to the original path if the workspace root is empty or
230
+ the paths are on different drives (Windows).
231
+ """
232
+ if not workspace_root:
233
+ return pathlib.Path(file_path).as_posix()
234
+ try:
235
+ return pathlib.Path(file_path).relative_to(workspace_root).as_posix()
236
+ except ValueError:
237
+ return pathlib.Path(file_path).as_posix()
238
+
239
+
240
+ def is_match(
241
+ patterns: list[str], file_path: str, workspace_root: str | None = None
242
+ ) -> bool:
243
+ """Returns true if the file matches one of the fnmatch patterns."""
244
+ if not patterns:
245
+ return False
246
+ relative_path = (
247
+ get_relative_path(file_path, workspace_root) if workspace_root else file_path
248
+ )
249
+ file_name = pathlib.Path(file_path).name
250
+ return any(
251
+ fnmatch.fnmatch(relative_path, pattern)
252
+ or (not pattern.startswith("/") and fnmatch.fnmatch(file_name, pattern))
253
+ for pattern in patterns
254
+ )
@@ -0,0 +1,94 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+ """Runner to use when running under a different interpreter."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import sys
9
+ import traceback
10
+ from collections.abc import Callable
11
+ from typing import TYPE_CHECKING
12
+
13
+ from .context import substitute_attr
14
+
15
+ if TYPE_CHECKING:
16
+ from .jsonrpc import JsonRpc
17
+
18
+
19
+ def update_sys_path(path_to_add: str, strategy: str) -> None:
20
+ """Add given path to ``sys.path``.
21
+
22
+ Parameters
23
+ ----------
24
+ path_to_add:
25
+ The directory to add.
26
+ strategy:
27
+ ``"useBundled"`` inserts at position 0 (highest priority);
28
+ any other value appends.
29
+ """
30
+ if path_to_add not in sys.path and os.path.isdir(path_to_add):
31
+ if strategy == "useBundled":
32
+ sys.path.insert(0, path_to_add)
33
+ else:
34
+ sys.path.append(path_to_add)
35
+
36
+
37
+ def run_message_loop(
38
+ rpc: JsonRpc,
39
+ run_fn: Callable[..., object],
40
+ result_cls: Callable[[str, str], object],
41
+ ) -> None:
42
+ """Process JSON-RPC messages in a loop until ``exit`` is received.
43
+
44
+ Parameters
45
+ ----------
46
+ rpc:
47
+ A ``JsonRpc`` instance connected to the parent process.
48
+ run_fn:
49
+ Callable with signature ``(module, argv, use_stdin, cwd, source=None)``
50
+ that returns a result object with ``.stdout`` and ``.stderr`` attrs.
51
+ result_cls:
52
+ Callable ``(stdout, stderr) -> result`` used to wrap exceptions.
53
+ """
54
+ exit_now = False
55
+ while not exit_now:
56
+ msg = rpc.receive_data()
57
+
58
+ method = msg["method"]
59
+ if method == "exit":
60
+ exit_now = True
61
+ continue
62
+
63
+ if method == "run":
64
+ is_exception = False
65
+ with substitute_attr(sys, "path", [""] + sys.path[:]):
66
+ try:
67
+ result = run_fn(
68
+ module=msg["module"],
69
+ argv=msg["argv"],
70
+ use_stdin=msg["useStdin"],
71
+ cwd=msg["cwd"],
72
+ source=msg.get("source"),
73
+ )
74
+ except Exception:
75
+ result = result_cls("", traceback.format_exc(chain=True))
76
+ is_exception = True
77
+
78
+ response: dict[str, object] = {
79
+ "id": msg["id"],
80
+ "error": result.stderr,
81
+ }
82
+ if is_exception:
83
+ response["exception"] = is_exception
84
+ elif result.stdout is not None:
85
+ response["result"] = result.stdout
86
+
87
+ rpc.send_data(response)
88
+ else:
89
+ rpc.send_data(
90
+ {
91
+ "id": msg.get("id", ""),
92
+ "error": f"Unknown method: {method}",
93
+ }
94
+ )
@@ -0,0 +1,174 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+ """Tool execution runners for VS Code Python tool extensions."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import contextlib
8
+ import io
9
+ import os
10
+ import runpy
11
+ import subprocess
12
+ import sys
13
+ from collections.abc import Callable, Iterator, Sequence
14
+ from dataclasses import dataclass
15
+
16
+ from .context import change_cwd, redirect_io, substitute_attr
17
+ from .paths import CWD_LOCK, is_same_path
18
+
19
+
20
+ @dataclass
21
+ class RunResult:
22
+ """Object to hold result from running tool."""
23
+
24
+ stdout: str
25
+ stderr: str
26
+ exit_code: int | str | None = None
27
+
28
+
29
+ class CustomIO(io.TextIOWrapper):
30
+ """Custom stream object to replace stdio."""
31
+
32
+ name = None
33
+
34
+ def __init__(self, name, encoding="utf-8", newline=None):
35
+ self._buffer = io.BytesIO()
36
+ super().__init__(self._buffer, encoding=encoding, newline=newline)
37
+ self.name = name
38
+
39
+ def close(self):
40
+ """Provide this close method which is used by some tools."""
41
+ # This is intentionally empty.
42
+ pass
43
+
44
+ def get_value(self) -> str:
45
+ """Returns value from the buffer as string."""
46
+ self.seek(0)
47
+ return self.read()
48
+
49
+
50
+ @contextlib.contextmanager
51
+ def _cwd_lock(cwd: str) -> Iterator[None]:
52
+ """Acquire CWD_LOCK and optionally change directory."""
53
+ with CWD_LOCK:
54
+ if is_same_path(os.getcwd(), cwd):
55
+ yield
56
+ else:
57
+ with change_cwd(cwd):
58
+ yield
59
+
60
+
61
+ def run_module(
62
+ module: str,
63
+ argv: Sequence[str],
64
+ use_stdin: bool,
65
+ cwd: str,
66
+ source: str | None = None,
67
+ ) -> RunResult:
68
+ """Runs a Python module via runpy (e.g. black, flake8).
69
+
70
+ Captures stdout, stderr, and SystemExit exit codes.
71
+ """
72
+ with _cwd_lock(cwd):
73
+ str_output = CustomIO("<stdout>", encoding="utf-8")
74
+ str_error = CustomIO("<stderr>", encoding="utf-8")
75
+
76
+ exit_code = None
77
+ try:
78
+ with substitute_attr(sys, "argv", list(argv)):
79
+ with redirect_io("stdout", str_output):
80
+ with redirect_io("stderr", str_error):
81
+ if use_stdin:
82
+ str_input = CustomIO(
83
+ "<stdin>", encoding="utf-8", newline="\n"
84
+ )
85
+ with redirect_io("stdin", str_input):
86
+ if source is not None:
87
+ str_input.write(source)
88
+ str_input.seek(0)
89
+ runpy.run_module(module, run_name="__main__")
90
+ else:
91
+ runpy.run_module(module, run_name="__main__")
92
+ except SystemExit as e:
93
+ exit_code = e.code
94
+
95
+ return RunResult(str_output.get_value(), str_error.get_value(), exit_code)
96
+
97
+
98
+ def run_path(
99
+ argv: Sequence[str],
100
+ use_stdin: bool,
101
+ cwd: str,
102
+ source: str | None = None,
103
+ env: dict[str, str] | None = None,
104
+ timeout: float | None = None,
105
+ ) -> RunResult:
106
+ """Runs tool as a subprocess via executable path."""
107
+ if use_stdin:
108
+ with subprocess.Popen(
109
+ argv,
110
+ encoding="utf-8",
111
+ stdout=subprocess.PIPE,
112
+ stderr=subprocess.PIPE,
113
+ stdin=subprocess.PIPE,
114
+ cwd=cwd,
115
+ env=env,
116
+ ) as process:
117
+ try:
118
+ stdout, stderr = process.communicate(input=source, timeout=timeout)
119
+ except subprocess.TimeoutExpired:
120
+ process.kill()
121
+ stdout, stderr = process.communicate()
122
+ return RunResult(stdout, stderr, process.returncode)
123
+ else:
124
+ try:
125
+ result = subprocess.run(
126
+ argv,
127
+ encoding="utf-8",
128
+ stdout=subprocess.PIPE,
129
+ stderr=subprocess.PIPE,
130
+ check=False,
131
+ cwd=cwd,
132
+ env=env,
133
+ timeout=timeout,
134
+ )
135
+ except subprocess.TimeoutExpired as e:
136
+ return RunResult(e.stdout or "", e.stderr or "", None)
137
+ return RunResult(result.stdout, result.stderr, result.returncode)
138
+
139
+
140
+ def run_api(
141
+ callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None],
142
+ argv: Sequence[str],
143
+ use_stdin: bool,
144
+ cwd: str,
145
+ source: str | None = None,
146
+ ) -> RunResult:
147
+ """Runs tool via API callback (importable tools).
148
+
149
+ Captures stdout, stderr, and SystemExit exit codes.
150
+ """
151
+ with _cwd_lock(cwd):
152
+ str_output = CustomIO("<stdout>", encoding="utf-8")
153
+ str_error = CustomIO("<stderr>", encoding="utf-8")
154
+
155
+ exit_code = None
156
+ try:
157
+ with substitute_attr(sys, "argv", list(argv)):
158
+ with redirect_io("stdout", str_output):
159
+ with redirect_io("stderr", str_error):
160
+ if use_stdin:
161
+ str_input = CustomIO(
162
+ "<stdin>", encoding="utf-8", newline="\n"
163
+ )
164
+ with redirect_io("stdin", str_input):
165
+ if source is not None:
166
+ str_input.write(source)
167
+ str_input.seek(0)
168
+ callback(argv, str_output, str_error, str_input)
169
+ else:
170
+ callback(argv, str_output, str_error, None)
171
+ except SystemExit as e:
172
+ exit_code = e.code
173
+
174
+ return RunResult(str_output.get_value(), str_error.get_value(), exit_code)