Skip to content

Commit 73b945b

Browse files
authored
Update rope to 1.11.0 for multi-threading capabilities (#498)
1 parent 7347737 commit 73b945b

File tree

4 files changed

+108
-103
lines changed

4 files changed

+108
-103
lines changed

pylsp/plugins/rope_autoimport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def pylsp_code_actions(
295295
word = get_name_or_module(document, diagnostic)
296296
log.debug(f"autoimport: searching for word: {word}")
297297
rope_config = config.settings(document_path=document.path).get("rope", {})
298-
autoimport = workspace._rope_autoimport(rope_config, feature="code_actions")
298+
autoimport = workspace._rope_autoimport(rope_config)
299299
suggestions = list(autoimport.search_full(word))
300300
log.debug("autoimport: suggestions: %s", suggestions)
301301
results = list(

pylsp/workspace.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
import uuid
1010
import functools
11-
from typing import Literal, Optional, Generator, Callable, List
11+
from typing import Optional, Generator, Callable, List
1212
from threading import RLock
1313

1414
import jedi
@@ -58,30 +58,20 @@ def __init__(self, root_uri, endpoint, config=None):
5858
# Whilst incubating, keep rope private
5959
self.__rope = None
6060
self.__rope_config = None
61-
62-
# We have a sperate AutoImport object for each feature to avoid sqlite errors
63-
# from accessing the same database from multiple threads.
64-
# An upstream fix discussion is here: https://github.com/python-rope/rope/issues/713
65-
self.__rope_autoimport = (
66-
{}
67-
) # Type: Dict[Literal["completions", "code_actions"], rope.contrib.autoimport.sqlite.AutoImport]
61+
self.__rope_autoimport = None
6862

6963
def _rope_autoimport(
7064
self,
7165
rope_config: Optional,
7266
memory: bool = False,
73-
feature: Literal["completions", "code_actions"] = "completions",
7467
):
7568
# pylint: disable=import-outside-toplevel
7669
from rope.contrib.autoimport.sqlite import AutoImport
7770

78-
if feature not in ["completions", "code_actions"]:
79-
raise ValueError(f"Unknown feature {feature}")
80-
81-
if self.__rope_autoimport.get(feature, None) is None:
71+
if self.__rope_autoimport is None:
8272
project = self._rope_project_builder(rope_config)
83-
self.__rope_autoimport[feature] = AutoImport(project, memory=memory)
84-
return self.__rope_autoimport[feature]
73+
self.__rope_autoimport = AutoImport(project, memory=memory)
74+
return self.__rope_autoimport
8575

8676
def _rope_project_builder(self, rope_config):
8777
# pylint: disable=import-outside-toplevel
@@ -388,8 +378,8 @@ def _create_cell_document(
388378
)
389379

390380
def close(self):
391-
for _, autoimport in self.__rope_autoimport.items():
392-
autoimport.close()
381+
if self.__rope_autoimport:
382+
self.__rope_autoimport.close()
393383

394384

395385
class Document:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ all = [
3434
"pydocstyle>=6.3.0,<6.4.0",
3535
"pyflakes>=3.1.0,<3.2.0",
3636
"pylint>=2.5.0,<3.1",
37-
"rope>1.2.0",
37+
"rope>=1.11.0",
3838
"yapf>=0.33.0",
3939
"whatthepatch>=1.0.2,<2.0.0"
4040
]
@@ -45,7 +45,7 @@ pycodestyle = ["pycodestyle>=2.11.0,<2.12.0"]
4545
pydocstyle = ["pydocstyle>=6.3.0,<6.4.0"]
4646
pyflakes = ["pyflakes>=3.1.0,<3.2.0"]
4747
pylint = ["pylint>=2.5.0,<3.1"]
48-
rope = ["rope>1.2.0"]
48+
rope = ["rope>=1.11.0"]
4949
yapf = ["yapf>=0.33.0", "whatthepatch>=1.0.2,<2.0.0"]
5050
websockets = ["websockets>=10.3"]
5151
test = [

test/plugins/test_autoimport.py

Lines changed: 98 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# Copyright 2022- Python Language Server Contributors.
22

33
from typing import Any, Dict, List
4-
from unittest.mock import Mock
4+
from unittest.mock import Mock, patch
5+
6+
from test.test_notebook_document import wait_for_condition
7+
from test.test_utils import send_initialize_request, send_notebook_did_open
58

69
import jedi
710
import parso
811
import pytest
912

10-
from pylsp import lsp, uris
13+
from pylsp import IS_WIN, lsp, uris
1114
from pylsp.config.config import Config
1215
from pylsp.plugins.rope_autoimport import (
1316
_get_score,
@@ -25,13 +28,18 @@
2528
DOC_URI = uris.from_fs_path(__file__)
2629

2730

28-
def contains_autoimport(suggestion: Dict[str, Any], module: str) -> bool:
29-
"""Checks if `suggestion` contains an autoimport for `module`."""
31+
def contains_autoimport_completion(suggestion: Dict[str, Any], module: str) -> bool:
32+
"""Checks if `suggestion` contains an autoimport completion for `module`."""
3033
return suggestion.get("label", "") == module and "import" in suggestion.get(
3134
"detail", ""
3235
)
3336

3437

38+
def contains_autoimport_quickfix(suggestion: Dict[str, Any], module: str) -> bool:
39+
"""Checks if `suggestion` contains an autoimport quick fix for `module`."""
40+
return suggestion.get("title", "") == f"import {module}"
41+
42+
3543
@pytest.fixture(scope="session")
3644
def autoimport_workspace(tmp_path_factory) -> Workspace:
3745
"Special autoimport workspace. Persists across sessions to make in-memory sqlite3 database fast."
@@ -248,82 +256,89 @@ def test_autoimport_code_actions_get_correct_module_name(autoimport_workspace, m
248256
assert module_name == "os"
249257

250258

251-
# rope autoimport launches a sqlite database which checks from which thread it is called.
252-
# This makes the test below fail because we access the db from a different thread.
253-
# See https://stackoverflow.com/questions/48218065/objects-created-in-a-thread-can-only-be-used-in-that-same-thread
254-
# @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows")
255-
# def test_autoimport_completions_for_notebook_document(
256-
# client_server_pair,
257-
# ):
258-
# client, server = client_server_pair
259-
# send_initialize_request(client)
260-
261-
# with patch.object(server._endpoint, "notify") as mock_notify:
262-
# # Expectations:
263-
# # 1. We receive an autoimport suggestion for "os" in the first cell because
264-
# # os is imported after that.
265-
# # 2. We don't receive an autoimport suggestion for "os" in the second cell because it's
266-
# # already imported in the second cell.
267-
# # 3. We don't receive an autoimport suggestion for "os" in the third cell because it's
268-
# # already imported in the second cell.
269-
# # 4. We receive an autoimport suggestion for "sys" because it's not already imported
270-
# send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"])
271-
# wait_for_condition(lambda: mock_notify.call_count >= 3)
272-
273-
# server.m_workspace__did_change_configuration(
274-
# settings={
275-
# "pylsp": {
276-
# "plugins": {
277-
# "rope_autoimport": {
278-
# "memory": True,
279-
# "completions": {"enabled": True},
280-
# },
281-
# }
282-
# }
283-
# }
284-
# )
285-
# rope_autoimport_settings = server.workspace._config.plugin_settings(
286-
# "rope_autoimport"
287-
# )
288-
# assert rope_autoimport_settings.get("completions", {}).get("enabled", False) is True
289-
# assert rope_autoimport_settings.get("memory", False) is True
290-
291-
# # 1.
292-
# suggestions = server.completions("cell_1_uri", {"line": 0, "character": 2}).get(
293-
# "items"
294-
# )
295-
# assert any(
296-
# suggestion
297-
# for suggestion in suggestions
298-
# if contains_autoimport(suggestion, "os")
299-
# )
300-
301-
# # 2.
302-
# suggestions = server.completions("cell_2_uri", {"line": 1, "character": 2}).get(
303-
# "items"
304-
# )
305-
# assert not any(
306-
# suggestion
307-
# for suggestion in suggestions
308-
# if contains_autoimport(suggestion, "os")
309-
# )
310-
311-
# # 3.
312-
# suggestions = server.completions("cell_3_uri", {"line": 0, "character": 2}).get(
313-
# "items"
314-
# )
315-
# assert not any(
316-
# suggestion
317-
# for suggestion in suggestions
318-
# if contains_autoimport(suggestion, "os")
319-
# )
320-
321-
# # 4.
322-
# suggestions = server.completions("cell_4_uri", {"line": 0, "character": 3}).get(
323-
# "items"
324-
# )
325-
# assert any(
326-
# suggestion
327-
# for suggestion in suggestions
328-
# if contains_autoimport(suggestion, "sys")
329-
# )
259+
def make_context(module_name, line, character_start, character_end):
260+
return {
261+
"diagnostics": [
262+
{
263+
"message": f"undefined name '{module_name}'",
264+
"range": {
265+
"start": {"line": line, "character": character_start},
266+
"end": {"line": line, "character": character_end},
267+
},
268+
}
269+
]
270+
}
271+
272+
273+
def position(line, character):
274+
return {"line": line, "character": character}
275+
276+
277+
@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows")
278+
def test_autoimport_code_actions_and_completions_for_notebook_document(
279+
client_server_pair,
280+
):
281+
client, server = client_server_pair
282+
send_initialize_request(
283+
client,
284+
{
285+
"pylsp": {
286+
"plugins": {
287+
"rope_autoimport": {
288+
"memory": True,
289+
"enabled": True,
290+
"completions": {"enabled": True},
291+
},
292+
}
293+
}
294+
},
295+
)
296+
297+
with patch.object(server._endpoint, "notify") as mock_notify:
298+
# Expectations:
299+
# 1. We receive an autoimport suggestion for "os" in the first cell because
300+
# os is imported after that.
301+
# 2. We don't receive an autoimport suggestion for "os" in the second cell because it's
302+
# already imported in the second cell.
303+
# 3. We don't receive an autoimport suggestion for "os" in the third cell because it's
304+
# already imported in the second cell.
305+
# 4. We receive an autoimport suggestion for "sys" because it's not already imported.
306+
# 5. If diagnostics doesn't contain "undefined name ...", we send empty quick fix suggestions.
307+
send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"])
308+
wait_for_condition(lambda: mock_notify.call_count >= 3)
309+
310+
rope_autoimport_settings = server.workspace._config.plugin_settings(
311+
"rope_autoimport"
312+
)
313+
assert rope_autoimport_settings.get("completions", {}).get("enabled", False) is True
314+
assert rope_autoimport_settings.get("memory", False) is True
315+
316+
# 1.
317+
quick_fixes = server.code_actions("cell_1_uri", {}, make_context("os", 0, 0, 2))
318+
assert any(s for s in quick_fixes if contains_autoimport_quickfix(s, "os"))
319+
320+
completions = server.completions("cell_1_uri", position(0, 2)).get("items")
321+
assert any(s for s in completions if contains_autoimport_completion(s, "os"))
322+
323+
# 2.
324+
# We don't test code actions here as in this case, there would be no code actions sent bc
325+
# there wouldn't be a diagnostics message.
326+
completions = server.completions("cell_2_uri", position(1, 2)).get("items")
327+
assert not any(s for s in completions if contains_autoimport_completion(s, "os"))
328+
329+
# 3.
330+
# Same as in 2.
331+
completions = server.completions("cell_3_uri", position(0, 2)).get("items")
332+
assert not any(s for s in completions if contains_autoimport_completion(s, "os"))
333+
334+
# 4.
335+
quick_fixes = server.code_actions("cell_4_uri", {}, make_context("sys", 0, 0, 3))
336+
assert any(s for s in quick_fixes if contains_autoimport_quickfix(s, "sys"))
337+
338+
completions = server.completions("cell_4_uri", position(0, 3)).get("items")
339+
assert any(s for s in completions if contains_autoimport_completion(s, "sys"))
340+
341+
# 5.
342+
context = {"diagnostics": [{"message": "A random message"}]}
343+
quick_fixes = server.code_actions("cell_4_uri", {}, context)
344+
assert len(quick_fixes) == 0

0 commit comments

Comments
 (0)