vscode-common-python-lsp 0.2.1__tar.gz → 0.4.0__tar.gz

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.
Files changed (37) hide show
  1. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/PKG-INFO +1 -1
  2. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/pyproject.toml +1 -1
  3. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_process_runner.py +53 -1
  4. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_server.py +72 -0
  5. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/__init__.py +2 -1
  6. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/process_runner.py +19 -0
  7. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/server.py +29 -7
  8. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp.egg-info/PKG-INFO +1 -1
  9. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/README.md +0 -0
  10. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/setup.cfg +0 -0
  11. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_code_actions.py +0 -0
  12. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_context.py +0 -0
  13. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_debug.py +0 -0
  14. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_diagnostics.py +0 -0
  15. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_formatting.py +0 -0
  16. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_jsonrpc.py +0 -0
  17. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_linting.py +0 -0
  18. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_notebook.py +0 -0
  19. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_package.py +0 -0
  20. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_paths.py +0 -0
  21. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_runner.py +0 -0
  22. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/tests/test_version.py +0 -0
  23. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/code_actions.py +0 -0
  24. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/context.py +0 -0
  25. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/debug.py +0 -0
  26. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/diagnostics.py +0 -0
  27. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/formatting.py +0 -0
  28. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/jsonrpc.py +0 -0
  29. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/linting.py +0 -0
  30. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/notebook.py +0 -0
  31. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/paths.py +0 -0
  32. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/runner.py +0 -0
  33. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp/version.py +0 -0
  34. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp.egg-info/SOURCES.txt +0 -0
  35. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp.egg-info/dependency_links.txt +0 -0
  36. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp.egg-info/requires.txt +0 -0
  37. {vscode_common_python_lsp-0.2.1 → vscode_common_python_lsp-0.4.0}/vscode_common_python_lsp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vscode-common-python-lsp
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Shared Python utilities for VS Code Python tool extensions
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vscode-common-python-lsp"
7
- version = "0.2.1"
7
+ version = "0.4.0"
8
8
  description = "Shared Python utilities for VS Code Python tool extensions"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -8,7 +8,11 @@ import unittest
8
8
  from dataclasses import dataclass
9
9
  from unittest.mock import MagicMock
10
10
 
11
- from vscode_common_python_lsp.process_runner import run_message_loop, update_sys_path
11
+ from vscode_common_python_lsp.process_runner import (
12
+ run_message_loop,
13
+ update_environ_path,
14
+ update_sys_path,
15
+ )
12
16
 
13
17
 
14
18
  class TestUpdateSysPath(unittest.TestCase):
@@ -60,6 +64,54 @@ class TestUpdateSysPath(unittest.TestCase):
60
64
  sys.path[:] = original
61
65
 
62
66
 
67
+ class TestUpdateEnvironPath(unittest.TestCase):
68
+ """Tests for update_environ_path."""
69
+
70
+ def test_adds_scripts_to_path(self):
71
+ import sysconfig
72
+
73
+ scripts = sysconfig.get_path("scripts")
74
+ if not scripts:
75
+ self.skipTest("sysconfig does not report scripts path")
76
+
77
+ # Remove scripts from PATH if present
78
+ original_env = os.environ.copy()
79
+ path_var = "PATH" if "PATH" in os.environ else "Path"
80
+ paths = os.environ.get(path_var, "").split(os.pathsep)
81
+ paths = [p for p in paths if p != scripts]
82
+ os.environ[path_var] = os.pathsep.join(paths)
83
+
84
+ try:
85
+ update_environ_path()
86
+ new_paths = os.environ[path_var].split(os.pathsep)
87
+ assert scripts in new_paths
88
+ assert new_paths[0] == scripts
89
+ finally:
90
+ os.environ.clear()
91
+ os.environ.update(original_env)
92
+
93
+ def test_does_not_duplicate(self):
94
+ import sysconfig
95
+
96
+ scripts = sysconfig.get_path("scripts")
97
+ if not scripts:
98
+ self.skipTest("sysconfig does not report scripts path")
99
+
100
+ original_env = os.environ.copy()
101
+ path_var = "PATH" if "PATH" in os.environ else "Path"
102
+ # Ensure scripts is already in PATH
103
+ os.environ[path_var] = scripts + os.pathsep + os.environ.get(path_var, "")
104
+
105
+ try:
106
+ count_before = os.environ[path_var].split(os.pathsep).count(scripts)
107
+ update_environ_path()
108
+ count_after = os.environ[path_var].split(os.pathsep).count(scripts)
109
+ assert count_after == count_before
110
+ finally:
111
+ os.environ.clear()
112
+ os.environ.update(original_env)
113
+
114
+
63
115
  @dataclass
64
116
  class _MockResult:
65
117
  """Minimal result object for testing."""
@@ -51,6 +51,7 @@ class TestToolServerConfig:
51
51
  assert cfg.tool_args == []
52
52
  assert cfg.min_version == ""
53
53
  assert cfg.runner_script == ""
54
+ assert cfg.resolve_symlinks is False
54
55
  assert cfg.default_notification_level == "off"
55
56
  assert cfg.default_settings == {}
56
57
 
@@ -330,6 +331,77 @@ class TestGetDocumentKey:
330
331
  assert ts.get_document_key(doc) is None
331
332
 
332
333
 
334
+ class TestResolveSymlinks:
335
+ """Tests for the resolve_symlinks config propagation to normalize_path."""
336
+
337
+ def test_default_config_has_resolve_symlinks_false(self):
338
+ cfg = ToolServerConfig(tool_module="mod", tool_display="Mod")
339
+ assert cfg.resolve_symlinks is False
340
+
341
+ @patch("vscode_common_python_lsp.server.normalize_path")
342
+ @patch("vscode_common_python_lsp.server.uris")
343
+ def test_update_workspace_settings_passes_resolve_symlinks_false(
344
+ self, mock_uris, mock_normalize
345
+ ):
346
+ mock_uris.from_fs_path.return_value = "file:///cwd"
347
+ mock_normalize.return_value = "/normalized/cwd"
348
+ ts = _make_server()
349
+ ts.update_workspace_settings(None)
350
+ mock_normalize.assert_called_with(os.getcwd(), resolve_symlinks=False)
351
+
352
+ @patch("vscode_common_python_lsp.server.normalize_path")
353
+ @patch("vscode_common_python_lsp.server.uris")
354
+ def test_update_workspace_settings_passes_resolve_symlinks_true(
355
+ self, mock_uris, mock_normalize
356
+ ):
357
+ mock_uris.from_fs_path.return_value = "file:///cwd"
358
+ mock_normalize.return_value = "/normalized/cwd"
359
+ cfg = ToolServerConfig(
360
+ tool_module="mod", tool_display="Mod", resolve_symlinks=True
361
+ )
362
+ ts = _make_server(cfg)
363
+ ts.update_workspace_settings(None)
364
+ mock_normalize.assert_called_with(os.getcwd(), resolve_symlinks=True)
365
+
366
+ @patch("vscode_common_python_lsp.server.normalize_path")
367
+ @patch("vscode_common_python_lsp.server.uris")
368
+ def test_update_workspace_settings_list_passes_resolve_symlinks(
369
+ self, mock_uris, mock_normalize
370
+ ):
371
+ mock_uris.to_fs_path.side_effect = lambda u: u.replace("file://", "")
372
+ mock_normalize.return_value = "/ws1"
373
+ ts = _make_server()
374
+ ts.update_workspace_settings([{"workspace": "file:///ws1"}])
375
+ mock_normalize.assert_called_with("/ws1", resolve_symlinks=False)
376
+
377
+ @patch("vscode_common_python_lsp.server.normalize_path")
378
+ @patch("vscode_common_python_lsp.server.uris")
379
+ def test_get_settings_by_document_passes_resolve_symlinks(
380
+ self, mock_uris, mock_normalize
381
+ ):
382
+ mock_uris.from_fs_path.return_value = "file:///parent"
383
+ mock_normalize.return_value = "/parent"
384
+ ts = _make_server()
385
+ doc = _make_document("/parent/file.py")
386
+ # No workspace settings, document outside all → fallback path
387
+ ts.get_settings_by_document(doc)
388
+ mock_normalize.assert_called_with(
389
+ str(pathlib.Path("/parent/file.py").parent),
390
+ resolve_symlinks=False,
391
+ )
392
+
393
+ @patch("vscode_common_python_lsp.server.normalize_path")
394
+ @patch("vscode_common_python_lsp.server.uris")
395
+ def test_get_settings_by_path_empty_passes_resolve_symlinks(
396
+ self, mock_uris, mock_normalize
397
+ ):
398
+ mock_uris.from_fs_path.return_value = "file:///cwd"
399
+ mock_normalize.return_value = "/normalized/cwd"
400
+ ts = _make_server()
401
+ ts.get_settings_by_path(pathlib.Path("/some/file.py"))
402
+ mock_normalize.assert_called_with(os.getcwd(), resolve_symlinks=False)
403
+
404
+
333
405
  # ---------------------------------------------------------------------------
334
406
  # CWD resolution
335
407
  # ---------------------------------------------------------------------------
@@ -59,7 +59,7 @@ from .paths import (
59
59
  normalize_path,
60
60
  reset_caches,
61
61
  )
62
- from .process_runner import run_message_loop, update_sys_path
62
+ from .process_runner import run_message_loop, update_environ_path, update_sys_path
63
63
  from .runner import CustomIO, RunResult, run_api, run_module, run_path
64
64
  from .server import ToolServer, ToolServerConfig
65
65
  from .version import VersionInfo, check_min_version, extract_version, version_to_tuple
@@ -98,6 +98,7 @@ __all__ = [
98
98
  "shutdown_json_rpc",
99
99
  # process_runner
100
100
  "update_sys_path",
101
+ "update_environ_path",
101
102
  "run_message_loop",
102
103
  # debug
103
104
  "setup_debugpy",
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import os
8
8
  import sys
9
+ import sysconfig
9
10
  import traceback
10
11
  from collections.abc import Callable
11
12
  from typing import TYPE_CHECKING
@@ -34,6 +35,24 @@ def update_sys_path(path_to_add: str, strategy: str) -> None:
34
35
  sys.path.append(path_to_add)
35
36
 
36
37
 
38
+ def update_environ_path() -> None:
39
+ """Update PATH environment variable with the ``scripts`` directory.
40
+
41
+ Ensures tool executables installed in the virtual environment's scripts
42
+ directory (``Scripts`` on Windows, ``bin`` on Unix) are discoverable.
43
+ """
44
+ scripts = sysconfig.get_path("scripts")
45
+ if not scripts:
46
+ return
47
+ for var_name in ("Path", "PATH"):
48
+ if var_name in os.environ:
49
+ paths = os.environ[var_name].split(os.pathsep)
50
+ if scripts not in paths:
51
+ paths.insert(0, scripts)
52
+ os.environ[var_name] = os.pathsep.join(paths)
53
+ break
54
+
55
+
37
56
  def run_message_loop(
38
57
  rpc: JsonRpc,
39
58
  run_fn: Callable[..., object],
@@ -45,6 +45,10 @@ class ToolServerConfig:
45
45
  Minimum supported version string.
46
46
  runner_script:
47
47
  Path to the bundled JSON-RPC runner script.
48
+ resolve_symlinks:
49
+ Whether to resolve symlinks when normalizing workspace paths.
50
+ All current extension repos use ``False`` to keep workspace
51
+ keys relative to the (possibly symlinked) workspace root.
48
52
  default_notification_level:
49
53
  Default value for the ``showNotifications`` setting.
50
54
  default_settings:
@@ -58,6 +62,7 @@ class ToolServerConfig:
58
62
  tool_args: list[str] = field(default_factory=list)
59
63
  min_version: str = ""
60
64
  runner_script: str = ""
65
+ resolve_symlinks: bool = False
61
66
  default_notification_level: Literal["off", "onError", "onWarning", "always"] = "off"
62
67
  default_settings: dict[str, Any] = field(default_factory=dict)
63
68
 
@@ -122,7 +127,9 @@ class ToolServer:
122
127
  def update_workspace_settings(self, settings: list[dict[str, Any]] | None) -> None:
123
128
  """Populate :attr:`workspace_settings` from the client payload."""
124
129
  if not settings:
125
- key = normalize_path(os.getcwd())
130
+ key = normalize_path(
131
+ os.getcwd(), resolve_symlinks=self.config.resolve_symlinks
132
+ )
126
133
  self.workspace_settings[key] = {
127
134
  "cwd": key,
128
135
  "workspaceFS": key,
@@ -132,7 +139,10 @@ class ToolServer:
132
139
  return
133
140
 
134
141
  for setting in settings:
135
- key = normalize_path(uris.to_fs_path(setting["workspace"]))
142
+ key = normalize_path(
143
+ uris.to_fs_path(setting["workspace"]),
144
+ resolve_symlinks=self.config.resolve_symlinks,
145
+ )
136
146
  self.workspace_settings[key] = {
137
147
  **self.get_global_defaults(),
138
148
  **setting,
@@ -142,7 +152,9 @@ class ToolServer:
142
152
  def get_settings_by_path(self, file_path: pathlib.Path) -> dict[str, Any]:
143
153
  """Return workspace settings for the given file path."""
144
154
  if not self.workspace_settings:
145
- cwd = normalize_path(os.getcwd())
155
+ cwd = normalize_path(
156
+ os.getcwd(), resolve_symlinks=self.config.resolve_symlinks
157
+ )
146
158
  return {
147
159
  "cwd": cwd,
148
160
  "workspaceFS": cwd,
@@ -153,7 +165,9 @@ class ToolServer:
153
165
  workspaces = {s["workspaceFS"] for s in self.workspace_settings.values()}
154
166
 
155
167
  while file_path != file_path.parent:
156
- str_file_path = normalize_path(str(file_path))
168
+ str_file_path = normalize_path(
169
+ str(file_path), resolve_symlinks=self.config.resolve_symlinks
170
+ )
157
171
  if str_file_path in workspaces:
158
172
  return self.workspace_settings[str_file_path]
159
173
  file_path = file_path.parent
@@ -167,7 +181,10 @@ class ToolServer:
167
181
  workspaces = {s["workspaceFS"] for s in self.workspace_settings.values()}
168
182
 
169
183
  while document_workspace != document_workspace.parent:
170
- norm_path = normalize_path(str(document_workspace))
184
+ norm_path = normalize_path(
185
+ str(document_workspace),
186
+ resolve_symlinks=self.config.resolve_symlinks,
187
+ )
171
188
  if norm_path in workspaces:
172
189
  return norm_path
173
190
  document_workspace = document_workspace.parent
@@ -178,7 +195,9 @@ class ToolServer:
178
195
  """Return workspace settings for the given document."""
179
196
  if document is None or document.path is None:
180
197
  if not self.workspace_settings:
181
- cwd = normalize_path(os.getcwd())
198
+ cwd = normalize_path(
199
+ os.getcwd(), resolve_symlinks=self.config.resolve_symlinks
200
+ )
182
201
  return {
183
202
  "cwd": cwd,
184
203
  "workspaceFS": cwd,
@@ -191,7 +210,10 @@ class ToolServer:
191
210
  if key is not None:
192
211
  return self.workspace_settings[key]
193
212
 
194
- key = normalize_path(str(pathlib.Path(document.path).parent))
213
+ key = normalize_path(
214
+ str(pathlib.Path(document.path).parent),
215
+ resolve_symlinks=self.config.resolve_symlinks,
216
+ )
195
217
  return {
196
218
  "cwd": key,
197
219
  "workspaceFS": key,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vscode-common-python-lsp
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Shared Python utilities for VS Code Python tool extensions
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT