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,487 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+ """Shared server infrastructure for Python tool extensions."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import importlib.metadata
8
+ import json
9
+ import os
10
+ import pathlib
11
+ import sys
12
+ import traceback
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Literal
16
+
17
+ import lsprotocol.types as lsp
18
+ from pygls import uris
19
+ from pygls.lsp.server import LanguageServer
20
+ from pygls.workspace import TextDocument
21
+
22
+ from . import jsonrpc
23
+ from .context import substitute_attr
24
+ from .paths import normalize_path
25
+ from .runner import RunResult, run_module, run_path
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Configuration
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @dataclass
33
+ class ToolServerConfig:
34
+ """Configuration for a Python tool extension server.
35
+
36
+ Parameters
37
+ ----------
38
+ tool_module:
39
+ Python module name of the tool (e.g. ``"black"``, ``"flake8"``).
40
+ tool_display:
41
+ Human-readable display name (e.g. ``"Black Formatter"``).
42
+ tool_args:
43
+ Default CLI arguments always passed to the tool.
44
+ min_version:
45
+ Minimum supported version string.
46
+ runner_script:
47
+ Path to the bundled JSON-RPC runner script.
48
+ default_notification_level:
49
+ Default value for the ``showNotifications`` setting.
50
+ default_settings:
51
+ Tool-specific setting keys and their default values. These are
52
+ merged into the base defaults returned by
53
+ :meth:`ToolServer.get_global_defaults`.
54
+ """
55
+
56
+ tool_module: str
57
+ tool_display: str
58
+ tool_args: list[str] = field(default_factory=list)
59
+ min_version: str = ""
60
+ runner_script: str = ""
61
+ default_notification_level: Literal["off", "onError", "onWarning", "always"] = "off"
62
+ default_settings: dict[str, Any] = field(default_factory=dict)
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # ToolServer — thin state container + shared utilities
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ class ToolServer:
71
+ """Shared server infrastructure for Python tool extensions.
72
+
73
+ Wraps a pygls :class:`LanguageServer` and provides settings management,
74
+ tool execution, and logging — the functions that are 100% identical
75
+ across all five extension repos.
76
+
77
+ Each repo keeps its own ``lsp_server.py`` that creates a
78
+ ``ToolServer``, registers LSP handlers on :attr:`server`, and calls
79
+ the shared methods as needed.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ config: ToolServerConfig,
85
+ *,
86
+ server: LanguageServer | None = None,
87
+ ):
88
+ self.config = config
89
+ self.workspace_settings: dict[str, Any] = {}
90
+ self.global_settings: dict[str, Any] = {}
91
+ if server is None:
92
+ try:
93
+ _pkg_version = importlib.metadata.version("vscode-common-python-lsp")
94
+ except importlib.metadata.PackageNotFoundError:
95
+ _pkg_version = "0.0.0-dev"
96
+ server = LanguageServer(
97
+ name=f"{config.tool_module}-server",
98
+ version=f"v{_pkg_version}",
99
+ max_workers=5,
100
+ )
101
+ self.server = server
102
+
103
+ # -----------------------------------------------------------------
104
+ # Settings management
105
+ # -----------------------------------------------------------------
106
+
107
+ def get_global_defaults(self) -> dict[str, Any]:
108
+ """Return merged base + tool-specific default settings."""
109
+ base: dict[str, Any] = {
110
+ "path": self.global_settings.get("path", []),
111
+ "interpreter": self.global_settings.get("interpreter", [sys.executable]),
112
+ "args": self.global_settings.get("args", []),
113
+ "importStrategy": self.global_settings.get("importStrategy", "useBundled"),
114
+ "showNotifications": self.global_settings.get(
115
+ "showNotifications", self.config.default_notification_level
116
+ ),
117
+ }
118
+ for key, default in self.config.default_settings.items():
119
+ base[key] = self.global_settings.get(key, default)
120
+ return base
121
+
122
+ def update_workspace_settings(self, settings: list[dict[str, Any]] | None) -> None:
123
+ """Populate :attr:`workspace_settings` from the client payload."""
124
+ if not settings:
125
+ key = normalize_path(os.getcwd())
126
+ self.workspace_settings[key] = {
127
+ "cwd": key,
128
+ "workspaceFS": key,
129
+ "workspace": uris.from_fs_path(key),
130
+ **self.get_global_defaults(),
131
+ }
132
+ return
133
+
134
+ for setting in settings:
135
+ key = normalize_path(uris.to_fs_path(setting["workspace"]))
136
+ self.workspace_settings[key] = {
137
+ **self.get_global_defaults(),
138
+ **setting,
139
+ "workspaceFS": key,
140
+ }
141
+
142
+ def get_settings_by_path(self, file_path: pathlib.Path) -> dict[str, Any]:
143
+ """Return workspace settings for the given file path."""
144
+ if not self.workspace_settings:
145
+ cwd = normalize_path(os.getcwd())
146
+ return {
147
+ "cwd": cwd,
148
+ "workspaceFS": cwd,
149
+ "workspace": uris.from_fs_path(cwd),
150
+ **self.get_global_defaults(),
151
+ }
152
+
153
+ workspaces = {s["workspaceFS"] for s in self.workspace_settings.values()}
154
+
155
+ while file_path != file_path.parent:
156
+ str_file_path = normalize_path(str(file_path))
157
+ if str_file_path in workspaces:
158
+ return self.workspace_settings[str_file_path]
159
+ file_path = file_path.parent
160
+
161
+ return list(self.workspace_settings.values())[0]
162
+
163
+ def get_document_key(self, document: TextDocument) -> str | None:
164
+ """Return the workspace key for the given document, or ``None``."""
165
+ if self.workspace_settings:
166
+ document_workspace = pathlib.Path(document.path)
167
+ workspaces = {s["workspaceFS"] for s in self.workspace_settings.values()}
168
+
169
+ while document_workspace != document_workspace.parent:
170
+ norm_path = normalize_path(str(document_workspace))
171
+ if norm_path in workspaces:
172
+ return norm_path
173
+ document_workspace = document_workspace.parent
174
+
175
+ return None
176
+
177
+ def get_settings_by_document(self, document: TextDocument | None) -> dict[str, Any]:
178
+ """Return workspace settings for the given document."""
179
+ if document is None or document.path is None:
180
+ if not self.workspace_settings:
181
+ cwd = normalize_path(os.getcwd())
182
+ return {
183
+ "cwd": cwd,
184
+ "workspaceFS": cwd,
185
+ "workspace": uris.from_fs_path(cwd),
186
+ **self.get_global_defaults(),
187
+ }
188
+ return list(self.workspace_settings.values())[0]
189
+
190
+ key = self.get_document_key(document)
191
+ if key is not None:
192
+ return self.workspace_settings[key]
193
+
194
+ key = normalize_path(str(pathlib.Path(document.path).parent))
195
+ return {
196
+ "cwd": key,
197
+ "workspaceFS": key,
198
+ "workspace": uris.from_fs_path(key),
199
+ **self.get_global_defaults(),
200
+ }
201
+
202
+ # -----------------------------------------------------------------
203
+ # CWD resolution
204
+ # -----------------------------------------------------------------
205
+
206
+ def get_cwd(
207
+ self,
208
+ settings: dict[str, Any],
209
+ document: TextDocument | None = None,
210
+ *,
211
+ document_path: str | None = None,
212
+ ) -> str:
213
+ """Resolve the working directory with VS Code variable substitution.
214
+
215
+ Parameters
216
+ ----------
217
+ settings:
218
+ Workspace settings dict (must contain ``workspaceFS``).
219
+ document:
220
+ The current text document, if available.
221
+ document_path:
222
+ Explicit path override. When provided, takes precedence over
223
+ ``document.path`` — useful for notebook cell handling where
224
+ the cell's URI differs from the notebook file path.
225
+ """
226
+ cwd = settings.get("cwd", settings["workspaceFS"])
227
+ workspace_fs = settings["workspaceFS"]
228
+
229
+ file_path = document_path or (document.path if document else None)
230
+
231
+ if file_path:
232
+ file_dir = os.path.dirname(file_path)
233
+ file_basename = os.path.basename(file_path)
234
+ file_stem, file_ext = os.path.splitext(file_basename)
235
+
236
+ try:
237
+ rel_file = os.path.relpath(file_path, workspace_fs)
238
+ except ValueError:
239
+ rel_file = file_path
240
+
241
+ try:
242
+ rel_dir = os.path.relpath(file_dir, workspace_fs)
243
+ except ValueError:
244
+ rel_dir = file_dir
245
+
246
+ substitutions = {
247
+ "${file}": file_path,
248
+ "${fileBasename}": file_basename,
249
+ "${fileBasenameNoExtension}": file_stem,
250
+ "${fileExtname}": file_ext,
251
+ "${fileDirname}": file_dir,
252
+ "${fileDirnameBasename}": os.path.basename(file_dir),
253
+ "${relativeFile}": rel_file,
254
+ "${relativeFileDirname}": rel_dir,
255
+ "${fileWorkspaceFolder}": workspace_fs,
256
+ }
257
+
258
+ for token, value in substitutions.items():
259
+ cwd = cwd.replace(token, value)
260
+ else:
261
+ # Without a document we cannot resolve file-related variables.
262
+ if "${file" in cwd or "${relativeFile" in cwd:
263
+ cwd = workspace_fs
264
+
265
+ return cwd
266
+
267
+ # -----------------------------------------------------------------
268
+ # Tool execution
269
+ # -----------------------------------------------------------------
270
+
271
+ def execute_tool(
272
+ self,
273
+ *,
274
+ argv: Sequence[str],
275
+ mode: Literal["path", "rpc", "module"],
276
+ settings: dict[str, Any],
277
+ use_stdin: bool = False,
278
+ cwd: str = "",
279
+ workspace: str = "",
280
+ source: str = "",
281
+ runner_script: str | None = None,
282
+ env: dict[str, str] | None = None,
283
+ timeout: float | None = None,
284
+ ) -> RunResult:
285
+ """Execute the tool in the specified mode.
286
+
287
+ Parameters
288
+ ----------
289
+ argv:
290
+ Full command-line argument list (caller is responsible for
291
+ building this — shared code does not impose arg ordering).
292
+ mode:
293
+ One of ``"path"``, ``"rpc"``, or ``"module"``.
294
+ settings:
295
+ Workspace settings dict (used for interpreter lookup in RPC).
296
+ use_stdin:
297
+ Whether to pipe source via stdin.
298
+ cwd:
299
+ Working directory for the tool process.
300
+ workspace:
301
+ Workspace key for RPC process management.
302
+ source:
303
+ Document source text (for stdin and RPC).
304
+ runner_script:
305
+ Override for :attr:`ToolServerConfig.runner_script`.
306
+ env:
307
+ Extra environment variables for path and RPC modes.
308
+ timeout:
309
+ Timeout in seconds for path and RPC modes (``None`` = no timeout).
310
+ """
311
+ runner = runner_script or self.config.runner_script
312
+
313
+ if mode == "path":
314
+ self.log_to_output(" ".join(argv))
315
+ self.log_to_output(f"CWD Server: {cwd}")
316
+ result = run_path(
317
+ argv=argv,
318
+ use_stdin=use_stdin,
319
+ cwd=cwd,
320
+ source=source,
321
+ env=env,
322
+ timeout=timeout,
323
+ )
324
+ if result.stderr:
325
+ self.log_to_output(result.stderr)
326
+
327
+ elif mode == "rpc":
328
+ if not workspace:
329
+ raise ValueError("workspace is required for RPC execution mode")
330
+ self.log_to_output(" ".join(settings["interpreter"] + ["-m"] + list(argv)))
331
+ self.log_to_output(f"CWD {self.config.tool_display}: {cwd}")
332
+ rpc_result = jsonrpc.run_over_json_rpc(
333
+ workspace=workspace,
334
+ interpreter=settings["interpreter"],
335
+ module=self.config.tool_module,
336
+ argv=argv,
337
+ use_stdin=use_stdin,
338
+ cwd=cwd,
339
+ runner_script=runner,
340
+ source=source,
341
+ env=env,
342
+ timeout=timeout,
343
+ )
344
+ result = self._rpc_to_run_result(rpc_result)
345
+
346
+ elif mode == "module":
347
+ self.log_to_output(" ".join([sys.executable, "-m"] + list(argv)))
348
+ self.log_to_output(f"CWD {self.config.tool_display}: {cwd}")
349
+ with substitute_attr(sys, "path", [""] + sys.path[:]):
350
+ try:
351
+ result = run_module(
352
+ module=self.config.tool_module,
353
+ argv=argv,
354
+ use_stdin=use_stdin,
355
+ cwd=cwd,
356
+ source=source,
357
+ )
358
+ except Exception:
359
+ self.log_error(traceback.format_exc(chain=True))
360
+ raise
361
+ if result.stderr:
362
+ self.log_to_output(result.stderr)
363
+
364
+ else:
365
+ raise ValueError(
366
+ f"Unknown execution mode: {mode!r}."
367
+ " Expected 'path', 'rpc', or 'module'."
368
+ )
369
+
370
+ return result
371
+
372
+ def _rpc_to_run_result(self, rpc_result: jsonrpc.RpcRunResult) -> RunResult:
373
+ """Convert an :class:`RpcRunResult` to a :class:`RunResult`, logging errors."""
374
+ error = ""
375
+ if rpc_result.exception:
376
+ self.log_error(rpc_result.exception)
377
+ error = rpc_result.exception
378
+ if rpc_result.stderr:
379
+ self.log_to_output(rpc_result.stderr)
380
+ error += "\n" + rpc_result.stderr
381
+ elif rpc_result.stderr:
382
+ self.log_to_output(rpc_result.stderr)
383
+ error = rpc_result.stderr
384
+ return RunResult(rpc_result.stdout, error)
385
+
386
+ # -----------------------------------------------------------------
387
+ # Logging
388
+ # -----------------------------------------------------------------
389
+
390
+ def log_to_output(
391
+ self,
392
+ message: str,
393
+ msg_type: lsp.MessageType = lsp.MessageType.Log,
394
+ ) -> None:
395
+ """Log a message to the Output channel."""
396
+ self.server.window_log_message(
397
+ lsp.LogMessageParams(type=msg_type, message=message)
398
+ )
399
+
400
+ def log_error(self, message: str) -> None:
401
+ """Log an error and optionally show a notification."""
402
+ self.server.window_log_message(
403
+ lsp.LogMessageParams(type=lsp.MessageType.Error, message=message)
404
+ )
405
+ if os.getenv("LS_SHOW_NOTIFICATION", "off") in [
406
+ "onError",
407
+ "onWarning",
408
+ "always",
409
+ ]:
410
+ self.server.window_show_message(
411
+ lsp.ShowMessageParams(type=lsp.MessageType.Error, message=message)
412
+ )
413
+
414
+ def log_warning(self, message: str) -> None:
415
+ """Log a warning and optionally show a notification."""
416
+ self.server.window_log_message(
417
+ lsp.LogMessageParams(type=lsp.MessageType.Warning, message=message)
418
+ )
419
+ if os.getenv("LS_SHOW_NOTIFICATION", "off") in [
420
+ "onWarning",
421
+ "always",
422
+ ]:
423
+ self.server.window_show_message(
424
+ lsp.ShowMessageParams(type=lsp.MessageType.Warning, message=message)
425
+ )
426
+
427
+ def log_always(self, message: str) -> None:
428
+ """Log an info message and show a notification only when ``always``."""
429
+ self.server.window_log_message(
430
+ lsp.LogMessageParams(type=lsp.MessageType.Info, message=message)
431
+ )
432
+ if os.getenv("LS_SHOW_NOTIFICATION", "off") == "always":
433
+ self.server.window_show_message(
434
+ lsp.ShowMessageParams(type=lsp.MessageType.Info, message=message)
435
+ )
436
+
437
+ # -----------------------------------------------------------------
438
+ # Lifecycle helpers
439
+ # -----------------------------------------------------------------
440
+
441
+ def apply_settings(self, params: lsp.InitializeParams) -> None:
442
+ """Apply global and workspace settings from an initialize request.
443
+
444
+ Call this from your ``@server.feature(lsp.INITIALIZE)`` handler.
445
+ Repos can add tool-specific logic before/after this call.
446
+ """
447
+ initialization_options = params.initialization_options or {}
448
+ self.global_settings.update(**initialization_options.get("globalSettings", {}))
449
+ settings = initialization_options.get("settings")
450
+ self.update_workspace_settings(settings)
451
+
452
+ def log_startup_info(self, settings: list[dict[str, Any]] | None = None) -> None:
453
+ """Log CWD, settings, and sys.path at server startup.
454
+
455
+ Call this from your ``@server.feature(lsp.INITIALIZE)`` handler
456
+ after :meth:`apply_settings`.
457
+ """
458
+ self.log_to_output(f"CWD Server: {os.getcwd()}")
459
+
460
+ if settings is not None:
461
+ self.log_to_output(
462
+ "Settings used to run Server:\r\n"
463
+ f"{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n"
464
+ )
465
+ self.log_to_output(
466
+ "Global settings:\r\n"
467
+ f"{json.dumps(self.global_settings, indent=4, ensure_ascii=False)}\r\n"
468
+ )
469
+
470
+ paths = "\r\n ".join(sys.path)
471
+ self.log_to_output(f"sys.path used to run Server:\r\n {paths}")
472
+
473
+ def handle_exit(self) -> None:
474
+ """Shut down JSON-RPC processes.
475
+
476
+ Call this from your ``@server.feature(lsp.EXIT)`` handler.
477
+ Repos with additional cleanup (e.g. mypy daemon) should perform
478
+ their own cleanup before or after this call.
479
+ """
480
+ jsonrpc.shutdown_json_rpc()
481
+
482
+ def handle_shutdown(self) -> None:
483
+ """Shut down JSON-RPC processes.
484
+
485
+ Call this from your ``@server.feature(lsp.SHUTDOWN)`` handler.
486
+ """
487
+ jsonrpc.shutdown_json_rpc()
@@ -0,0 +1,64 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+ """Tool version detection and validation."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import re
9
+ from collections.abc import Callable
10
+ from typing import TypeAlias
11
+
12
+ from packaging.version import InvalidVersion, parse
13
+
14
+
15
+ def extract_version(
16
+ stdout: str | None,
17
+ *,
18
+ parser: Callable[[str], str | None] | None = None,
19
+ ) -> str | None:
20
+ """Extract a version string from tool ``--version`` output.
21
+
22
+ Parameters
23
+ ----------
24
+ stdout:
25
+ The tool's stdout (typically from running ``<tool> --version``).
26
+ May be *None* or empty — returns *None* in that case.
27
+ parser:
28
+ Optional callable that receives the full stdout and returns a
29
+ version string. When *None* the first ``\\d+\\.\\d+`` match in the
30
+ first non-empty line is returned.
31
+ """
32
+ if not stdout or not stdout.strip():
33
+ return None
34
+
35
+ if parser is not None:
36
+ return parser(stdout)
37
+
38
+ first_line = next((line for line in stdout.splitlines() if line.strip()), "")
39
+ match = re.search(r"\d+\.\d+\S*", first_line)
40
+ return match.group(0) if match else None
41
+
42
+
43
+ def check_min_version(actual: str, minimum: str) -> bool:
44
+ """Return *True* when *actual* ≥ *minimum*.
45
+
46
+ Uses :func:`packaging.version.parse` for PEP 440 comparison.
47
+ """
48
+ try:
49
+ return parse(actual) >= parse(minimum)
50
+ except InvalidVersion:
51
+ logging.warning(
52
+ "Invalid version string: actual=%r, minimum=%r", actual, minimum
53
+ )
54
+ return False
55
+
56
+
57
+ VersionInfo: TypeAlias = tuple[int, int, int]
58
+ """(major, minor, micro) tuple stored per-workspace."""
59
+
60
+
61
+ def version_to_tuple(version_str: str) -> VersionInfo:
62
+ """Convert a version string to a ``(major, minor, micro)`` tuple."""
63
+ v = parse(version_str)
64
+ return (v.major, v.minor, v.micro)
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: vscode-common-python-lsp
3
+ Version: 0.1.0
4
+ Summary: Shared Python utilities for VS Code Python tool extensions
5
+ Author: Microsoft Corporation
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pygls>=1.1.0
10
+ Requires-Dist: lsprotocol>=2023.0.0
11
+ Requires-Dist: packaging>=22.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Requires-Dist: black; extra == "dev"
15
+ Requires-Dist: isort; extra == "dev"
16
+ Requires-Dist: flake8; extra == "dev"
17
+
18
+ # vscode-common-python-lsp (Python)
19
+
20
+ Shared Python utilities for VS Code Python tool extensions.
21
+
22
+ See the [main README](../README.md) for full documentation.
@@ -0,0 +1,18 @@
1
+ vscode_common_python_lsp/__init__.py,sha256=HL5WWYmwbK_oOomwqmkl7FsMDDew0HQmEJs6W4oTluI,3172
2
+ vscode_common_python_lsp/code_actions.py,sha256=m9MZyTnxx_cXSYXqfM7voDsOOWX-qfRlGm6273pYvcs,4039
3
+ vscode_common_python_lsp/context.py,sha256=Ik96BR3z9GuEUqQYsjXtlFv94tf2rzGoYpwmNVa_BFE,1663
4
+ vscode_common_python_lsp/debug.py,sha256=xulbn0ypUYhSwZbCt1wICU1cjAYE8a-T3YK8wqqJsLs,1435
5
+ vscode_common_python_lsp/diagnostics.py,sha256=zlI5B3HM427kzTn2PaOLv5y0Q4JE1HTVk-T4AN_rR1M,8506
6
+ vscode_common_python_lsp/formatting.py,sha256=0RZM4bwypudzTAQia66d5YDpEGdNhNgKIriE_jcSwjU,1697
7
+ vscode_common_python_lsp/jsonrpc.py,sha256=ElIetD2B50wtUj_cOPIcUJUawe_38C40wj4BmFtApgU,9630
8
+ vscode_common_python_lsp/linting.py,sha256=ulkQNJXTgnKUDXx1CsMctHj9SVz0dQ0dN7R8fE3b6vg,1915
9
+ vscode_common_python_lsp/notebook.py,sha256=bM2xh3APfZfemOf80hq0w9P9dmuM2rby1SUeZJPDTxM,7901
10
+ vscode_common_python_lsp/paths.py,sha256=u5wCMYsC5KfNFK2uuPuD41s4-Df3k5yi1A7xGUB99Ic,8180
11
+ vscode_common_python_lsp/process_runner.py,sha256=UtHwVqsaH32S7BzTn8cxVqPleKiL06eUx3u6mFzWbl0,2792
12
+ vscode_common_python_lsp/runner.py,sha256=95BzjHVbvRwzSdtgxlgwVxovC_4h9C-AXmx4U75VjAs,5635
13
+ vscode_common_python_lsp/server.py,sha256=V1seq8lFcAHAB3PPMyCn25ow6IqhGPfIlqu-SWYx2vI,18125
14
+ vscode_common_python_lsp/version.py,sha256=NKiIzZGt3viIbV1A6x5bYqGp5KJCf3h7FWRI-tSpf1Q,1895
15
+ vscode_common_python_lsp-0.1.0.dist-info/METADATA,sha256=Oq1bn9le17BgQ0IOEYpXLzqwNTjhKKZ8K6zhnW1qfWM,683
16
+ vscode_common_python_lsp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ vscode_common_python_lsp-0.1.0.dist-info/top_level.txt,sha256=pT1RWvivYspG8rgTXHhA-TY45SKHB6xTXlFPFrx3YFc,25
18
+ vscode_common_python_lsp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ vscode_common_python_lsp