vscode-common-python-lsp 0.5.0__tar.gz → 0.5.2__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.5.0 → vscode_common_python_lsp-0.5.2}/PKG-INFO +1 -1
  2. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/pyproject.toml +1 -1
  3. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_paths.py +151 -0
  4. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/__init__.py +2 -0
  5. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/paths.py +133 -1
  6. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp.egg-info/PKG-INFO +1 -1
  7. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/README.md +0 -0
  8. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/setup.cfg +0 -0
  9. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_code_actions.py +0 -0
  10. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_context.py +0 -0
  11. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_debug.py +0 -0
  12. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_diagnostics.py +0 -0
  13. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_formatting.py +0 -0
  14. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_jsonrpc.py +0 -0
  15. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_linting.py +0 -0
  16. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_notebook.py +0 -0
  17. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_package.py +0 -0
  18. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_process_runner.py +0 -0
  19. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_runner.py +0 -0
  20. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_server.py +0 -0
  21. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/tests/test_version.py +0 -0
  22. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/code_actions.py +0 -0
  23. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/context.py +0 -0
  24. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/debug.py +0 -0
  25. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/diagnostics.py +0 -0
  26. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/formatting.py +0 -0
  27. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/jsonrpc.py +0 -0
  28. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/linting.py +0 -0
  29. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/notebook.py +0 -0
  30. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/process_runner.py +0 -0
  31. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/runner.py +0 -0
  32. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/server.py +0 -0
  33. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp/version.py +0 -0
  34. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp.egg-info/SOURCES.txt +0 -0
  35. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp.egg-info/dependency_links.txt +0 -0
  36. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/vscode_common_python_lsp.egg-info/requires.txt +0 -0
  37. {vscode_common_python_lsp-0.5.0 → vscode_common_python_lsp-0.5.2}/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.5.0
3
+ Version: 0.5.2
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.5.0"
7
+ version = "0.5.2"
8
8
  description = "Shared Python utilities for VS Code Python tool extensions"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -3,6 +3,7 @@
3
3
  """Tests for paths module."""
4
4
 
5
5
  import os
6
+ import pathlib
6
7
  import site
7
8
  import sys
8
9
  import sysconfig
@@ -20,6 +21,7 @@ from vscode_common_python_lsp.paths import (
20
21
  is_match,
21
22
  is_same_path,
22
23
  normalize_path,
24
+ sanitize_path_for_name_max,
23
25
  )
24
26
 
25
27
 
@@ -266,3 +268,152 @@ class TestIsMatch:
266
268
  def test_with_workspace_root(self, tmp_path):
267
269
  fp = str(tmp_path / "src" / "test.py")
268
270
  assert is_match(["src/*.py"], fp, str(tmp_path))
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # sanitize_path_for_name_max tests
275
+ # ---------------------------------------------------------------------------
276
+
277
+ # A realistic dev-container/tunnel netloc component (>255 chars).
278
+ _LONG_NETLOC = (
279
+ "dev-container+7b22686f737450617468223a222f686f6d652f646f6e6e79"
280
+ "2f50726f6a656374732f776f72647365617263682d637370222c226c6f63"
281
+ "616c446f636b6572223a66616c73652c22636f6e66696746696c65223a7b"
282
+ "22246d6964223a312c2270617468223a222f686f6d652f646f6e6e792f50"
283
+ "726f6a656374732f776f72647365617263682d6373702f2e646576636f6e"
284
+ "7461696e65722f646576636f6e7461696e65722e6a736f6e222c22736368"
285
+ "656d65223a227673636f64652d66696c65486f7374227d7d"
286
+ )
287
+
288
+
289
+ class TestSanitizePathForNameMax:
290
+ """Tests for sanitize_path_for_name_max() — path sanitisation."""
291
+
292
+ def test_short_path_unchanged(self):
293
+ """Normal paths should pass through untouched."""
294
+ p = os.path.join(os.sep, "home", "user", "project", "src", "main.py")
295
+ assert sanitize_path_for_name_max(p) == p
296
+
297
+ def test_overlong_component_detected(self):
298
+ """The fixture netloc must actually exceed NAME_MAX."""
299
+ assert len(_LONG_NETLOC.encode()) > 255
300
+
301
+ def test_overlong_without_workspace(self):
302
+ """Overlong component is replaced with '_', basename preserved."""
303
+ p = os.path.join(os.sep, _LONG_NETLOC, "workspace", "src", "main.py")
304
+ result = sanitize_path_for_name_max(p)
305
+ for part in pathlib.PurePath(result).parts:
306
+ assert len(part.encode()) <= 255
307
+ assert result.endswith("main.py")
308
+
309
+ def test_overlong_with_workspace(self):
310
+ """With a workspace, result preserves sub-path below overlong component."""
311
+ p = os.path.join(os.sep, _LONG_NETLOC, "workspace", "src", "main.py")
312
+ workspace = os.path.join(os.sep, "workspace")
313
+ result = sanitize_path_for_name_max(p, workspace=workspace)
314
+ assert result == os.path.join(workspace, "workspace", "src", "main.py")
315
+
316
+ def test_overlong_with_workspace_preserves_subpath(self):
317
+ """Sub-path below overlong component is preserved when re-rooting."""
318
+ p = os.path.join(os.sep, _LONG_NETLOC, "deep", "nested", "path", "app.py")
319
+ workspace = os.path.join(os.sep, "home", "user", "project")
320
+ result = sanitize_path_for_name_max(p, workspace=workspace)
321
+ assert result == os.path.join(workspace, "deep", "nested", "path", "app.py")
322
+
323
+ def test_multiple_overlong_components(self):
324
+ """Multiple overlong components are all sanitised."""
325
+ long1 = "a" * 300
326
+ long2 = "b" * 400
327
+ p = os.path.join(os.sep, long1, long2, "file.py")
328
+ result = sanitize_path_for_name_max(p)
329
+ for part in pathlib.PurePath(result).parts:
330
+ assert len(part.encode()) <= 255
331
+
332
+ def test_empty_workspace_falls_back(self):
333
+ """Empty workspace string triggers component-replacement fallback."""
334
+ p = os.path.join(os.sep, _LONG_NETLOC, "src", "file.py")
335
+ result = sanitize_path_for_name_max(p, workspace="")
336
+ assert result.endswith("file.py")
337
+ for part in pathlib.PurePath(result).parts:
338
+ assert len(part.encode()) <= 255
339
+
340
+ def test_unicode_path_component(self):
341
+ """Multi-byte UTF-8 components exceeding 255 bytes are sanitised."""
342
+ # Each emoji is 4 bytes in UTF-8; 64 emojis = 256 bytes > 255
343
+ long_unicode = "\U0001f600" * 64
344
+ p = os.path.join(os.sep, long_unicode, "file.py")
345
+ result = sanitize_path_for_name_max(p)
346
+ for part in pathlib.PurePath(result).parts:
347
+ assert len(part.encode("utf-8")) <= 255
348
+
349
+ def test_overlong_basename_without_workspace(self):
350
+ """Overlong basename without workspace is sanitized, preserving suffix."""
351
+ long_name = "x" * 300 + ".py"
352
+ p = os.path.join(os.sep, "dev", long_name)
353
+ result = sanitize_path_for_name_max(p)
354
+ assert pathlib.PurePath(result).name == "_.py"
355
+ for part in pathlib.PurePath(result).parts:
356
+ assert len(part.encode()) <= 255
357
+
358
+ def test_overlong_basename_with_workspace(self):
359
+ """Overlong basename with workspace is sanitized, preserving suffix."""
360
+ long_name = "a" * 300 + ".py"
361
+ p = os.path.join(os.sep, _LONG_NETLOC, "src", long_name)
362
+ workspace = os.path.join(os.sep, "workspace")
363
+ result = sanitize_path_for_name_max(p, workspace=workspace)
364
+ assert pathlib.PurePath(result).name == "_.py"
365
+ for part in pathlib.PurePath(result).parts:
366
+ assert len(part.encode()) <= 255
367
+
368
+ def test_overlong_basename_replaced_preserving_suffix(self):
369
+ """If basename itself is too long, it gets replaced preserving suffix."""
370
+ long_name = "x" * 300 + ".py"
371
+ p = os.path.join(os.sep, "workspace", "src", long_name)
372
+ workspace = os.path.join(os.sep, "workspace")
373
+ result = sanitize_path_for_name_max(p, workspace=workspace)
374
+ # Should replace with "_.py" not reroute under workspace
375
+ assert pathlib.PurePath(result).name == "_.py"
376
+
377
+ def test_workspace_preserves_subpath(self):
378
+ """Intermediate directories below overlong component are preserved."""
379
+ p = os.path.join(os.sep, _LONG_NETLOC, "src", "pkg", "main.py")
380
+ workspace = os.path.join(os.sep, "ws")
381
+ result = sanitize_path_for_name_max(p, workspace=workspace)
382
+ assert result == os.path.join(os.sep, "ws", "src", "pkg", "main.py")
383
+
384
+ def test_workspace_with_overlong_basename_in_tail(self):
385
+ """Workspace branch sanitizes an overlong basename in the tail."""
386
+ long_name = "a" * 300 + ".py"
387
+ p = os.path.join(os.sep, _LONG_NETLOC, "workspace", long_name)
388
+ workspace = os.path.join(os.sep, "ws")
389
+ result = sanitize_path_for_name_max(p, workspace=workspace)
390
+ assert pathlib.PurePath(result).name == "_.py"
391
+ assert result.startswith(workspace)
392
+
393
+ def test_windows_limit_kind(self):
394
+ """Windows mode uses character count, not byte count."""
395
+ # 200 emoji chars = 200 characters but 800 UTF-8 bytes
396
+ # Should NOT exceed on windows (200 < 255) but WOULD exceed on posix (800 > 255)
397
+ component = "\U0001f600" * 200
398
+ p = os.path.join(os.sep, component, "file.py")
399
+ result_win = sanitize_path_for_name_max(p, limit_kind="windows")
400
+ result_posix = sanitize_path_for_name_max(p, limit_kind="posix")
401
+ # Windows: unchanged (200 chars < 255)
402
+ assert result_win == p
403
+ # POSIX: sanitized (800 bytes > 255)
404
+ assert result_posix != p
405
+ assert result_posix.endswith("file.py")
406
+
407
+ def test_root_components_ignored(self):
408
+ """Root/anchor parts like '/' are not flagged as overlong."""
409
+ p = os.path.join(os.sep, "normal", "path.py")
410
+ assert sanitize_path_for_name_max(p) == p
411
+
412
+ def test_unicode_posix_ascii_fast_path(self):
413
+ """ASCII-only strings that are short skip the byte-counting loop."""
414
+ p = os.path.join(os.sep, "a" * 255, "file.py")
415
+ # Exactly 255 is fine
416
+ assert sanitize_path_for_name_max(p) == p
417
+ # 256 exceeds
418
+ p2 = os.path.join(os.sep, "a" * 256, "file.py")
419
+ assert sanitize_path_for_name_max(p2) != p2
@@ -58,6 +58,7 @@ from .paths import (
58
58
  is_same_path,
59
59
  normalize_path,
60
60
  reset_caches,
61
+ sanitize_path_for_name_max,
61
62
  )
62
63
  from .process_runner import (
63
64
  resolve_bundle_path,
@@ -84,6 +85,7 @@ __all__ = [
84
85
  "PythonFileKind",
85
86
  "is_match",
86
87
  "reset_caches",
88
+ "sanitize_path_for_name_max",
87
89
  # context
88
90
  "substitute_attr",
89
91
  "redirect_io",
@@ -13,7 +13,7 @@ import sys
13
13
  import sysconfig
14
14
  import threading
15
15
  from enum import Enum
16
- from typing import Any
16
+ from typing import Any, Literal
17
17
 
18
18
  # Save the working directory used when loading this module
19
19
  SERVER_CWD = os.getcwd()
@@ -239,6 +239,138 @@ def normalize_path(file_path: str, resolve_symlinks: bool = True) -> str:
239
239
  return str(path)
240
240
 
241
241
 
242
+ # Conservative maximum length of a single path component. POSIX NAME_MAX is
243
+ # 255 *bytes* (ext4, APFS, …). NTFS actually limits to 255 UTF-16 code units,
244
+ # so this byte-oriented heuristic is stricter than necessary on Windows — but
245
+ # that is safe and keeps the logic simple across platforms.
246
+ _NAME_MAX = 255
247
+
248
+ PathComponentLimitKind = Literal["windows", "posix"]
249
+
250
+
251
+ def _utf8_len_exceeds(s: str, limit: int) -> bool:
252
+ """Check if UTF-8 byte length of s exceeds limit, without allocating."""
253
+ total = 0
254
+
255
+ for ch in s:
256
+ code = ord(ch)
257
+
258
+ if code < 0x80:
259
+ total += 1
260
+ elif code < 0x800:
261
+ total += 2
262
+ elif code < 0x10000:
263
+ total += 3
264
+ else:
265
+ total += 4
266
+
267
+ if total > limit:
268
+ return True
269
+
270
+ return False
271
+
272
+
273
+ def _component_exceeds_name_max(
274
+ component: str,
275
+ *,
276
+ limit_kind: PathComponentLimitKind,
277
+ ) -> bool:
278
+ # Ignore root/anchor-ish path parts.
279
+ if component in ("", "/", "\\") or component.endswith(":"):
280
+ return False
281
+
282
+ if limit_kind == "windows":
283
+ # Windows component limits are character-oriented for normal paths.
284
+ return len(component) > _NAME_MAX
285
+
286
+ # POSIX NAME_MAX is byte-oriented.
287
+ if len(component) > _NAME_MAX:
288
+ return True
289
+
290
+ if component.isascii():
291
+ return False
292
+
293
+ return _utf8_len_exceeds(component, _NAME_MAX)
294
+
295
+
296
+ def sanitize_path_for_name_max(
297
+ fs_path: str,
298
+ workspace: str | None = None,
299
+ *,
300
+ limit_kind: PathComponentLimitKind = "posix",
301
+ ) -> str:
302
+ """Return *fs_path* with any overlong path components shortened.
303
+
304
+ Dev-container / tunnel URIs can embed a ``netloc`` component that far
305
+ exceeds the conservative 255-byte ``NAME_MAX`` heuristic used here
306
+ (ext4 and APFS use 255 bytes; NTFS uses 255 UTF-16 code units, so
307
+ this byte-oriented check is stricter than necessary on Windows). When that
308
+ value is used as ``--stdin-filename`` or similar, the downstream tool
309
+ raises ``OSError: [Errno 36] File name too long``.
310
+
311
+ This helper replaces every overlong path component with ``_`` plus the
312
+ original file extension (e.g. ``_.py``), preserving the suffix that
313
+ downstream tools use for file-type dispatch.
314
+
315
+ If *workspace* is supplied and the overlong component is *not* the
316
+ basename, the path is re-rooted under the workspace while preserving
317
+ the full sub-path below the overlong component. For example::
318
+
319
+ sanitize_path_for_name_max(
320
+ "/dev-container+<long>/workspace/src/pkg/main.py",
321
+ workspace="/workspace",
322
+ )
323
+ # → "/workspace/workspace/src/pkg/main.py"
324
+
325
+ If *fs_path* contains no overlong component, it is returned unchanged.
326
+
327
+ Parameters
328
+ ----------
329
+ fs_path:
330
+ Filesystem path derived from a document URI (e.g. via
331
+ ``pygls.uris.to_fs_path`` or VS Code's ``Uri.fsPath``).
332
+ workspace:
333
+ Optional workspace root path. When provided and the overlong
334
+ component is not the basename, the result is
335
+ ``<workspace>/<sub-path>`` — the portion of the path below
336
+ the overlong component is preserved so that downstream tools
337
+ can still resolve config files and derive module names.
338
+ limit_kind:
339
+ Whether to apply Windows (character-count) or POSIX (byte-count)
340
+ limits. Defaults to ``"posix"``. Note: the POSIX byte-count
341
+ heuristic is conservative on NTFS, which actually limits to 255
342
+ UTF-16 code units.
343
+ """
344
+ path = pathlib.PurePath(fs_path)
345
+ parts = path.parts
346
+
347
+ safe_parts: list[str] | None = None
348
+
349
+ for i, part in enumerate(parts):
350
+ if not _component_exceeds_name_max(part, limit_kind=limit_kind):
351
+ continue
352
+
353
+ if workspace and i != len(parts) - 1:
354
+ # Preserve the sub-path below the overlong component so that
355
+ # intermediate directories (used by tools to locate config files
356
+ # and derive module names) are retained.
357
+ tail_parts = list(parts[i + 1 :])
358
+ for j, tp in enumerate(tail_parts):
359
+ if _component_exceeds_name_max(tp, limit_kind=limit_kind):
360
+ tail_parts[j] = "_" + pathlib.PurePath(tp).suffix
361
+ return str(pathlib.PurePath(workspace, *tail_parts))
362
+
363
+ if safe_parts is None:
364
+ safe_parts = list(parts)
365
+
366
+ safe_parts[i] = "_" + pathlib.PurePath(part).suffix
367
+
368
+ if safe_parts is None:
369
+ return fs_path
370
+
371
+ return str(pathlib.PurePath(*safe_parts))
372
+
373
+
242
374
  def is_current_interpreter(executable: str) -> bool:
243
375
  """Returns true if the executable path is same as the current interpreter."""
244
376
  return is_same_path(executable, sys.executable)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vscode-common-python-lsp
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Shared Python utilities for VS Code Python tool extensions
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT