jusi-shell 0.2.0__tar.gz → 0.2.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.
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ dist/
4
+ *.egg-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jusi-shell
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Shell plugin for Jusi
5
5
  Author: Jusi contributors
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jusi-shell"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Shell plugin for Jusi"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,5 +1,5 @@
1
1
  """Jusi 1.0 interactive shell plugin."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -5,6 +5,13 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
 
8
+ MAX_COMPLETION_ITEMS = 500
9
+
10
+
11
+ class CompletionLimitExceeded(Exception):
12
+ """The completion would exceed Jusi's protocol item limit."""
13
+
14
+
8
15
  def _active_word(prefix: str) -> tuple[str, int]:
9
16
  line_start = prefix.rfind("\n") + 1
10
17
  line = prefix[line_start:]
@@ -39,6 +46,8 @@ def complete_paths(payload: dict[str, Any], cwd: str) -> dict[str, list[dict[str
39
46
  for entry in entries:
40
47
  if not entry.name.startswith(name_prefix):
41
48
  continue
49
+ if len(items) == MAX_COMPLETION_ITEMS:
50
+ raise CompletionLimitExceeded
42
51
  text = typed_parent + entry.name
43
52
  if entry.is_dir():
44
53
  text += "/"
@@ -11,7 +11,7 @@ from typing import Any
11
11
 
12
12
  from jusi.plugin_api import OperationRejected, WorkerResult, copy_text, open_text, show_diff, terminal_surface
13
13
 
14
- from .completion import complete_paths
14
+ from .completion import CompletionLimitExceeded, MAX_COMPLETION_ITEMS, complete_paths
15
15
  from .control import exchange
16
16
 
17
17
 
@@ -40,7 +40,15 @@ class ShellWorker:
40
40
  if operation == "complete":
41
41
  status = self._request({"type": "status"})
42
42
  cwd = str(status.get("cwd", "")).strip() or self.cwd
43
- return WorkerResult(complete_paths(payload, cwd))
43
+ try:
44
+ completion = complete_paths(payload, cwd)
45
+ except CompletionLimitExceeded:
46
+ raise OperationRejected(
47
+ f"Path completion has more than {MAX_COMPLETION_ITEMS} matches; "
48
+ "type more characters to narrow it down",
49
+ reason="invalid_request",
50
+ ) from None
51
+ return WorkerResult(completion)
44
52
  if operation == "editor_action":
45
53
  return self._editor_action(payload)
46
54
  raise OperationRejected(f"Unsupported shell operation: {operation}", reason="unsupported")
@@ -4,6 +4,7 @@ import json
4
4
  import os
5
5
  from pathlib import Path
6
6
  import pty
7
+ import re
7
8
  import select
8
9
  import subprocess
9
10
  import sys
@@ -24,6 +25,15 @@ from jusi_shell.kernel import configure_jusi_runtime_v1, jusi_kernel_adapter_v1,
24
25
  from jusi_shell.worker import ShellWorker, _followup_body
25
26
 
26
27
 
28
+ class VersionTest(unittest.TestCase):
29
+ def test_runtime_version_matches_project_metadata(self) -> None:
30
+ pyproject = Path(__file__).parents[1] / "pyproject.toml"
31
+ match = re.search(r'^version = "([^"]+)"$', pyproject.read_text(), re.MULTILINE)
32
+
33
+ self.assertIsNotNone(match)
34
+ self.assertEqual(match.group(1), __version__)
35
+
36
+
27
37
  class FakeIPython:
28
38
  def __init__(self) -> None:
29
39
  self.magics_manager = type("MagicsManager", (), {"magics": {"cell": {}}})()
@@ -137,6 +147,26 @@ class WorkerTest(unittest.TestCase):
137
147
  worker.handle("execute", {"cwd": "/definitely/not/a/jusi-shell-directory"})
138
148
  self.assertIsNone(worker.runtime_directory)
139
149
 
150
+ def test_too_many_completions_are_recoverable(self) -> None:
151
+ from jusi.plugin_api import OperationRejected
152
+
153
+ worker = ShellWorker(object())
154
+ with tempfile.TemporaryDirectory() as crowded, tempfile.TemporaryDirectory() as narrow:
155
+ for index in range(501):
156
+ Path(crowded, f"item-{index:03}").touch()
157
+ Path(narrow, "item-only").touch()
158
+
159
+ with patch.object(worker, "_request", side_effect=({"cwd": crowded}, {"cwd": narrow})):
160
+ with self.assertRaisesRegex(OperationRejected, "more than 500 matches") as raised:
161
+ worker.handle("complete", {"prefix": "cat item-", "cursor_pos": len("cat item-")})
162
+ self.assertEqual("invalid_request", raised.exception.reason)
163
+
164
+ result = worker.handle(
165
+ "complete", {"prefix": "cat item-", "cursor_pos": len("cat item-")}
166
+ )
167
+
168
+ self.assertEqual(["item-only"], [item["text"] for item in result.result["items"]])
169
+
140
170
  def test_editor_actions_export_content_not_paths(self) -> None:
141
171
  worker = ShellWorker(object())
142
172
  copied = worker.handle("editor_action", {"action": "copy", "selection": {"text": "/target/file"}})
File without changes
File without changes