import-completer 0.4.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.
- import_completer/__init__.py +0 -0
- import_completer/code_scanner.py +202 -0
- import_completer/completion_engine.py +80 -0
- import_completer/config.py +26 -0
- import_completer/language_server.py +203 -0
- import_completer/path_discovery.py +28 -0
- import_completer/symbols_database.py +98 -0
- import_completer/types.py +8 -0
- import_completer-0.4.0.dist-info/METADATA +13 -0
- import_completer-0.4.0.dist-info/RECORD +12 -0
- import_completer-0.4.0.dist-info/WHEEL +4 -0
- import_completer-0.4.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Generator, Iterator
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import tree_sitter_python as tspython
|
|
7
|
+
from tree_sitter import Language, Node, Parser
|
|
8
|
+
|
|
9
|
+
from import_completer.types import ScannedSymbol
|
|
10
|
+
|
|
11
|
+
PYTHON_TS_LANGUAGE = Language(tspython.language())
|
|
12
|
+
PYTHON_TS_PARSER = Parser()
|
|
13
|
+
PYTHON_TS_PARSER.language = PYTHON_TS_LANGUAGE
|
|
14
|
+
|
|
15
|
+
PYTHON_TOP_LEVEL_SYMBOLS_QUERY = PYTHON_TS_LANGUAGE.query(
|
|
16
|
+
"""
|
|
17
|
+
[
|
|
18
|
+
; Direct module children
|
|
19
|
+
(module
|
|
20
|
+
(function_definition name: (identifier) @function-name))
|
|
21
|
+
(module
|
|
22
|
+
(class_definition name: (identifier) @class-name))
|
|
23
|
+
(module
|
|
24
|
+
(decorated_definition
|
|
25
|
+
(function_definition name: (identifier) @function-name)))
|
|
26
|
+
(module
|
|
27
|
+
(decorated_definition
|
|
28
|
+
(class_definition name: (identifier) @class-name)))
|
|
29
|
+
(module
|
|
30
|
+
(expression_statement
|
|
31
|
+
(assignment left: (identifier) @assignment-name)))
|
|
32
|
+
|
|
33
|
+
; Inside top-level if statements
|
|
34
|
+
(module
|
|
35
|
+
(if_statement
|
|
36
|
+
(block
|
|
37
|
+
(function_definition name: (identifier) @function-name))))
|
|
38
|
+
(module
|
|
39
|
+
(if_statement
|
|
40
|
+
(block
|
|
41
|
+
(class_definition name: (identifier) @class-name))))
|
|
42
|
+
(module
|
|
43
|
+
(if_statement
|
|
44
|
+
(block
|
|
45
|
+
(decorated_definition
|
|
46
|
+
(function_definition name: (identifier) @function-name)))))
|
|
47
|
+
(module
|
|
48
|
+
(if_statement
|
|
49
|
+
(block
|
|
50
|
+
(decorated_definition
|
|
51
|
+
(class_definition name: (identifier) @class-name)))))
|
|
52
|
+
(module
|
|
53
|
+
(if_statement
|
|
54
|
+
(block
|
|
55
|
+
(expression_statement
|
|
56
|
+
(assignment left: (identifier) @assignment-name)))))
|
|
57
|
+
; Else clause
|
|
58
|
+
(module
|
|
59
|
+
(if_statement
|
|
60
|
+
(else_clause
|
|
61
|
+
(block
|
|
62
|
+
(function_definition name: (identifier) @function-name)))))
|
|
63
|
+
(module
|
|
64
|
+
(if_statement
|
|
65
|
+
(else_clause
|
|
66
|
+
(block
|
|
67
|
+
(class_definition name: (identifier) @class-name)))))
|
|
68
|
+
(module
|
|
69
|
+
(if_statement
|
|
70
|
+
(else_clause
|
|
71
|
+
(block
|
|
72
|
+
(decorated_definition
|
|
73
|
+
(function_definition name: (identifier) @function-name))))))
|
|
74
|
+
(module
|
|
75
|
+
(if_statement
|
|
76
|
+
(else_clause
|
|
77
|
+
(block
|
|
78
|
+
(decorated_definition
|
|
79
|
+
(class_definition name: (identifier) @class-name))))))
|
|
80
|
+
(module
|
|
81
|
+
(if_statement
|
|
82
|
+
(else_clause
|
|
83
|
+
(block
|
|
84
|
+
(expression_statement
|
|
85
|
+
(assignment left: (identifier) @assignment-name))))))
|
|
86
|
+
]
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
RE_PYTHON_VALID_MODULE_FILE_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*\.pyi?$")
|
|
91
|
+
RE_PYTHON_VALID_MODULE_DIRECTORY_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def discover_python_modules(directory: str | Path) -> Generator[Path]:
|
|
95
|
+
"""
|
|
96
|
+
Walk through directory and yield Python module files (.py and .pyi).
|
|
97
|
+
Descends into subdirectories only if they are Python modules (contain __init__.py).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
directory: Root directory to start scanning from
|
|
101
|
+
|
|
102
|
+
Yields:
|
|
103
|
+
Path objects for Python module files
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
directory = Path(directory)
|
|
107
|
+
|
|
108
|
+
if not directory.is_dir():
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
for child_entity_path in directory.iterdir():
|
|
112
|
+
if (
|
|
113
|
+
child_entity_path.is_file()
|
|
114
|
+
and RE_PYTHON_VALID_MODULE_FILE_NAME.match(child_entity_path.name)
|
|
115
|
+
is not None
|
|
116
|
+
):
|
|
117
|
+
yield child_entity_path
|
|
118
|
+
elif (
|
|
119
|
+
child_entity_path.is_dir()
|
|
120
|
+
and RE_PYTHON_VALID_MODULE_DIRECTORY_NAME.match(child_entity_path.name)
|
|
121
|
+
is not None
|
|
122
|
+
):
|
|
123
|
+
# Recursively process subdirectories if they are Python packages
|
|
124
|
+
if (child_entity_path / "__init__.py").exists() or (
|
|
125
|
+
child_entity_path / "__init__.pyi"
|
|
126
|
+
).exists():
|
|
127
|
+
yield from discover_python_modules(child_entity_path)
|
|
128
|
+
else:
|
|
129
|
+
pass
|
|
130
|
+
# print(f"Skipping {child_entity_path}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def extract_symbols_from_origin(origin_path: Path) -> Iterator[ScannedSymbol]:
|
|
134
|
+
for module_path in discover_python_modules(origin_path):
|
|
135
|
+
yield from extract_symbols_from_file(module_path, origin_path)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_symbols_from_file(
|
|
139
|
+
file_path: Path, origin_path: Path
|
|
140
|
+
) -> Iterator[ScannedSymbol]:
|
|
141
|
+
"""
|
|
142
|
+
Extract all top-level symbols from a given Python file.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
with open(file_path, "rb") as fh:
|
|
146
|
+
source_code = fh.read()
|
|
147
|
+
|
|
148
|
+
yield from extract_symbols(
|
|
149
|
+
source_code, annotated_file_path=file_path, origin_path=origin_path
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def extract_symbols(
|
|
154
|
+
source_code: bytes, annotated_file_path: Path, origin_path: Path
|
|
155
|
+
) -> Iterator[ScannedSymbol]:
|
|
156
|
+
"""
|
|
157
|
+
Extract all top-level symbols from a given Python source code.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
module_relative_path = Path(annotated_file_path.relative_to(origin_path))
|
|
161
|
+
parent_module = ".".join(module_relative_path.with_suffix("").parts)
|
|
162
|
+
|
|
163
|
+
def _make_scanned_symbol(
|
|
164
|
+
kind: Literal["function", "class", "variable"], node: Node
|
|
165
|
+
) -> ScannedSymbol:
|
|
166
|
+
assert node.text is not None
|
|
167
|
+
return ScannedSymbol(
|
|
168
|
+
kind=kind,
|
|
169
|
+
name=node.text.decode("utf-8"),
|
|
170
|
+
parent_module=parent_module,
|
|
171
|
+
metadata={},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
tree = PYTHON_TS_PARSER.parse(source_code)
|
|
175
|
+
root_node = tree.root_node
|
|
176
|
+
|
|
177
|
+
captures = PYTHON_TOP_LEVEL_SYMBOLS_QUERY.captures(root_node)
|
|
178
|
+
for node in captures.get("function-name", []):
|
|
179
|
+
if node.text:
|
|
180
|
+
yield _make_scanned_symbol("function", node)
|
|
181
|
+
|
|
182
|
+
for node in captures.get("class-name", []):
|
|
183
|
+
if node.text:
|
|
184
|
+
yield _make_scanned_symbol("class", node)
|
|
185
|
+
|
|
186
|
+
for node in captures.get("assignment-name", []):
|
|
187
|
+
if node.text:
|
|
188
|
+
yield _make_scanned_symbol("variable", node)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
import sys
|
|
193
|
+
|
|
194
|
+
for path in (Path(p) for p in sys.path):
|
|
195
|
+
if not path.is_dir():
|
|
196
|
+
continue
|
|
197
|
+
for module_path in discover_python_modules(path):
|
|
198
|
+
print(f"Discovered module: {module_path}")
|
|
199
|
+
for symbol in extract_symbols_from_file(
|
|
200
|
+
module_path, annotated_source_sys_path=path
|
|
201
|
+
):
|
|
202
|
+
print(f" + {symbol.kind}: {symbol.name}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections.abc import AsyncGenerator, AsyncIterable
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from import_completer.code_scanner import extract_symbols_from_origin
|
|
9
|
+
from import_completer.path_discovery import get_internal_python_paths
|
|
10
|
+
from import_completer.symbols_database import DatabaseHandler
|
|
11
|
+
from import_completer.types import ScannedSymbol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompletionEngine:
|
|
15
|
+
def __init__(
|
|
16
|
+
self, symbols_database: DatabaseHandler, origin_paths: list[Path]
|
|
17
|
+
) -> None:
|
|
18
|
+
self._symbols_database = symbols_database
|
|
19
|
+
self._origin_paths = origin_paths
|
|
20
|
+
self._is_fully_initialized = False
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_fully_initialized(self) -> bool:
|
|
24
|
+
return self._is_fully_initialized
|
|
25
|
+
|
|
26
|
+
async def initialize_database(self) -> None:
|
|
27
|
+
self._symbols_database = DatabaseHandler()
|
|
28
|
+
await self._symbols_database.__aenter__()
|
|
29
|
+
await self._symbols_database.create_schema()
|
|
30
|
+
await self._symbols_database.create_indices()
|
|
31
|
+
|
|
32
|
+
async def autoload_symbols(self) -> None:
|
|
33
|
+
assert self._symbols_database is not None, "Database not initialized!"
|
|
34
|
+
for origin_idx, path in enumerate((Path(p) for p in sys.path), 1):
|
|
35
|
+
if not path.is_dir():
|
|
36
|
+
continue
|
|
37
|
+
logger.info("Rebuilding symbols for path: {}", path)
|
|
38
|
+
await self._symbols_database.perform_rebuild_for_origin(
|
|
39
|
+
origin_idx, extract_symbols_from_origin(path)
|
|
40
|
+
)
|
|
41
|
+
self._is_fully_initialized = True
|
|
42
|
+
|
|
43
|
+
async def lookup_symbol_prefix(self, prefix: str) -> AsyncIterable[ScannedSymbol]:
|
|
44
|
+
assert self._symbols_database is not None, "Database not initialized!"
|
|
45
|
+
async for symbol in self._symbols_database.lookup_symbol_name(prefix):
|
|
46
|
+
yield symbol
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def create_completion_engine(
|
|
51
|
+
origin_paths: list[Path],
|
|
52
|
+
) -> AsyncGenerator[CompletionEngine]:
|
|
53
|
+
symbols_database = DatabaseHandler()
|
|
54
|
+
async with symbols_database:
|
|
55
|
+
await symbols_database.create_schema()
|
|
56
|
+
await symbols_database.create_indices()
|
|
57
|
+
completion_engine = CompletionEngine(symbols_database, origin_paths)
|
|
58
|
+
yield completion_engine
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
import asyncio
|
|
63
|
+
import sys
|
|
64
|
+
|
|
65
|
+
async def _main():
|
|
66
|
+
async with create_completion_engine(
|
|
67
|
+
list(get_internal_python_paths())
|
|
68
|
+
) as completion_engine:
|
|
69
|
+
await completion_engine.autoload_symbols()
|
|
70
|
+
print("Database fully initialized!")
|
|
71
|
+
|
|
72
|
+
for word_to_complete in sys.argv[1:]:
|
|
73
|
+
print(f"Completing for '{word_to_complete}':")
|
|
74
|
+
async for symbol in completion_engine.lookup_symbol_prefix(
|
|
75
|
+
word_to_complete
|
|
76
|
+
):
|
|
77
|
+
print(f" - {symbol.kind}: {symbol.name} ({symbol.parent_module})")
|
|
78
|
+
print()
|
|
79
|
+
|
|
80
|
+
asyncio.run(_main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from import_completer.path_discovery import (
|
|
6
|
+
discover_paths_for_python_executable,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_version() -> str:
|
|
11
|
+
"""Get the package version from installed metadata, or 'dev' if not installed."""
|
|
12
|
+
try:
|
|
13
|
+
return version("import-completer")
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
return "dev"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass
|
|
19
|
+
class Config:
|
|
20
|
+
origin_paths: list[Path]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def default(cls) -> "Config":
|
|
24
|
+
return cls(
|
|
25
|
+
origin_paths=list(discover_paths_for_python_executable()),
|
|
26
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from contextlib import AsyncExitStack
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from lsprotocol.types import (
|
|
7
|
+
INITIALIZED,
|
|
8
|
+
SHUTDOWN,
|
|
9
|
+
TEXT_DOCUMENT_COMPLETION,
|
|
10
|
+
CompletionItem,
|
|
11
|
+
CompletionItemKind,
|
|
12
|
+
CompletionList,
|
|
13
|
+
CompletionParams,
|
|
14
|
+
InitializeParams,
|
|
15
|
+
Position,
|
|
16
|
+
ProgressParams,
|
|
17
|
+
Range,
|
|
18
|
+
TextEdit,
|
|
19
|
+
WorkDoneProgressBegin,
|
|
20
|
+
WorkDoneProgressEnd,
|
|
21
|
+
WorkDoneProgressReport,
|
|
22
|
+
)
|
|
23
|
+
from pygls.lsp.server import LanguageServer
|
|
24
|
+
|
|
25
|
+
from import_completer.completion_engine import (
|
|
26
|
+
CompletionEngine,
|
|
27
|
+
create_completion_engine,
|
|
28
|
+
)
|
|
29
|
+
from import_completer.config import Config, get_version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _handler_exception_wrapper(func):
|
|
33
|
+
async def wrapper(*args, **kwargs):
|
|
34
|
+
try:
|
|
35
|
+
return await func(*args, **kwargs)
|
|
36
|
+
except Exception:
|
|
37
|
+
logger.exception("Error occurred during handling of {}.", func.__name__)
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ImportCompleterLanguageServer(LanguageServer):
|
|
44
|
+
def __init__(self, *args: Any, config: Config, **kwargs: Any) -> None:
|
|
45
|
+
super().__init__(*args, **kwargs)
|
|
46
|
+
self._exit_stack = AsyncExitStack()
|
|
47
|
+
self._config = config
|
|
48
|
+
self._completion_engine: CompletionEngine | None = None
|
|
49
|
+
|
|
50
|
+
self.feature(INITIALIZED)(
|
|
51
|
+
_handler_exception_wrapper(self.initialize_completion_engine)
|
|
52
|
+
)
|
|
53
|
+
self.feature(TEXT_DOCUMENT_COMPLETION)(
|
|
54
|
+
_handler_exception_wrapper(self.handle_completion)
|
|
55
|
+
)
|
|
56
|
+
self.feature(SHUTDOWN)(
|
|
57
|
+
_handler_exception_wrapper(self.shutdown_completion_engine)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def completion_engine(self) -> CompletionEngine:
|
|
62
|
+
assert self._completion_engine is not None, "Completion engine not initialized!"
|
|
63
|
+
return self._completion_engine
|
|
64
|
+
|
|
65
|
+
async def initialize_completion_engine(self, params: InitializeParams) -> None:
|
|
66
|
+
logger.info("Initializing language server.")
|
|
67
|
+
|
|
68
|
+
# Begin progress
|
|
69
|
+
token = "import-completer-init"
|
|
70
|
+
self.protocol.notify(
|
|
71
|
+
"$/progress",
|
|
72
|
+
ProgressParams(
|
|
73
|
+
token=token,
|
|
74
|
+
value=WorkDoneProgressBegin(
|
|
75
|
+
title="Import Completer", message="Initializing...", percentage=0
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Report progress - creating engine
|
|
82
|
+
self.protocol.notify(
|
|
83
|
+
"$/progress",
|
|
84
|
+
ProgressParams(
|
|
85
|
+
token=token,
|
|
86
|
+
value=WorkDoneProgressReport(
|
|
87
|
+
message="Creating completion engine...", percentage=30
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._completion_engine = await self._exit_stack.enter_async_context(
|
|
93
|
+
create_completion_engine(self._config.origin_paths)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Report progress - loading symbols
|
|
97
|
+
self.protocol.notify(
|
|
98
|
+
"$/progress",
|
|
99
|
+
ProgressParams(
|
|
100
|
+
token=token,
|
|
101
|
+
value=WorkDoneProgressReport(
|
|
102
|
+
message="Loading symbols...", percentage=60
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await self._completion_engine.autoload_symbols()
|
|
108
|
+
|
|
109
|
+
# End progress
|
|
110
|
+
self.protocol.notify(
|
|
111
|
+
"$/progress",
|
|
112
|
+
ProgressParams(
|
|
113
|
+
token=token, value=WorkDoneProgressEnd(message="Ready!")
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
logger.info("Language server initialization complete.")
|
|
117
|
+
|
|
118
|
+
except Exception:
|
|
119
|
+
# End progress with error
|
|
120
|
+
self.protocol.notify(
|
|
121
|
+
"$/progress",
|
|
122
|
+
ProgressParams(
|
|
123
|
+
token=token,
|
|
124
|
+
value=WorkDoneProgressEnd(message="Initialization failed"),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
logger.exception("Failed to initialize language server")
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
async def shutdown_completion_engine(self, params) -> None:
|
|
131
|
+
"""Clean up resources before server shutdown"""
|
|
132
|
+
logger.info("Shutting down language server, cleaning up resources...")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Close all async context managers (including completion engine)
|
|
136
|
+
await self._exit_stack.aclose()
|
|
137
|
+
self._completion_engine = None
|
|
138
|
+
logger.info("Resources cleaned up successfully.")
|
|
139
|
+
except Exception:
|
|
140
|
+
logger.exception("Error during shutdown cleanup")
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
async def handle_completion(self, params: CompletionParams) -> CompletionList:
|
|
145
|
+
logger.debug("Handling completion, with params: {}", params)
|
|
146
|
+
document = self.workspace.get_text_document(params.text_document.uri)
|
|
147
|
+
current_line = document.lines[params.position.line]
|
|
148
|
+
last_word = re.split(
|
|
149
|
+
r"[^a-zA-Z0-9_.]", current_line[: params.position.character]
|
|
150
|
+
)[-1]
|
|
151
|
+
logger.debug("Completing for word: '{}'", last_word)
|
|
152
|
+
|
|
153
|
+
if "." in last_word:
|
|
154
|
+
logger.debug("Ignoring completions for dotted names.")
|
|
155
|
+
return CompletionList(is_incomplete=False, items=[])
|
|
156
|
+
|
|
157
|
+
completions = [
|
|
158
|
+
symbol
|
|
159
|
+
async for symbol in self.completion_engine.lookup_symbol_prefix(last_word)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
items = []
|
|
163
|
+
for symbol in completions:
|
|
164
|
+
match symbol.kind:
|
|
165
|
+
case "variable":
|
|
166
|
+
kind = CompletionItemKind.Variable
|
|
167
|
+
case "function":
|
|
168
|
+
kind = CompletionItemKind.Function
|
|
169
|
+
case "class":
|
|
170
|
+
kind = CompletionItemKind.Class
|
|
171
|
+
case _:
|
|
172
|
+
kind = CompletionItemKind.Text
|
|
173
|
+
items.append(
|
|
174
|
+
CompletionItem(
|
|
175
|
+
label=symbol.name,
|
|
176
|
+
detail=f"from {symbol.parent_module} import {symbol.name}\n# import_completer",
|
|
177
|
+
kind=kind,
|
|
178
|
+
additional_text_edits=[
|
|
179
|
+
TextEdit(
|
|
180
|
+
new_text=f"from {symbol.parent_module} import {symbol.name}\n",
|
|
181
|
+
range=Range(
|
|
182
|
+
start=Position(line=0, character=0),
|
|
183
|
+
end=Position(line=0, character=0),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
],
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
logger.debug("Generated {} completions.", len(items))
|
|
191
|
+
return CompletionList(is_incomplete=False, items=items)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def start_server():
|
|
195
|
+
server = ImportCompleterLanguageServer(
|
|
196
|
+
"import-completer", get_version(), config=Config.default()
|
|
197
|
+
)
|
|
198
|
+
logger.info("Starting ImportCompleter language server...")
|
|
199
|
+
server.start_io()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
start_server()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def discover_paths_for_python_executable(
|
|
8
|
+
python_executable: str = "python",
|
|
9
|
+
) -> Iterable[Path]:
|
|
10
|
+
output = subprocess.check_output(
|
|
11
|
+
[python_executable, "-c", r"import sys; print('\n'.join(sys.path))"]
|
|
12
|
+
)
|
|
13
|
+
for output_line in output.decode().splitlines():
|
|
14
|
+
path = Path(output_line).absolute()
|
|
15
|
+
if path.is_dir():
|
|
16
|
+
yield path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_internal_python_paths() -> Iterable[Path]:
|
|
20
|
+
return (Path(p) for p in sys.path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
python_executable = sys.argv[1] if len(sys.argv) > 1 else sys.executable
|
|
27
|
+
for path in discover_paths_for_python_executable(python_executable):
|
|
28
|
+
print(path)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from collections.abc import AsyncIterable, Iterable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import aiosqlite
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from import_completer.types import ScannedSymbol
|
|
8
|
+
|
|
9
|
+
DB_SYMBOLS_NAME = "symbols"
|
|
10
|
+
DB_SYMBOLS_SCHEMA = [
|
|
11
|
+
("generation", "INTEGER"),
|
|
12
|
+
("kind", "TEXT"),
|
|
13
|
+
("name", "TEXT"),
|
|
14
|
+
("origin_index", "INTEGER"),
|
|
15
|
+
("parent_module", "TEXT"),
|
|
16
|
+
("metadata_json", "TEXT"),
|
|
17
|
+
]
|
|
18
|
+
DB_INDICES = [
|
|
19
|
+
("idx_symbols_name", DB_SYMBOLS_NAME, ["name"]),
|
|
20
|
+
("idx_symbols_parent_module", DB_SYMBOLS_NAME, ["origin_index", "parent_module"]),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DatabaseHandler:
|
|
25
|
+
def __init__(self, database_path: str | Path = ":memory:") -> None:
|
|
26
|
+
self._database_path = database_path
|
|
27
|
+
self._connection = None
|
|
28
|
+
|
|
29
|
+
async def __aenter__(self):
|
|
30
|
+
self._connection = await aiosqlite.connect(str(self._database_path))
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
34
|
+
if self._connection:
|
|
35
|
+
await self._connection.close()
|
|
36
|
+
|
|
37
|
+
async def create_schema(self) -> None:
|
|
38
|
+
assert self._connection is not None, "Database connection not initialized!"
|
|
39
|
+
await self._connection.execute(
|
|
40
|
+
f"CREATE TABLE IF NOT EXISTS {DB_SYMBOLS_NAME} ({', '.join(f'{name} {type}' for name, type in DB_SYMBOLS_SCHEMA)})"
|
|
41
|
+
)
|
|
42
|
+
await self._connection.commit()
|
|
43
|
+
|
|
44
|
+
async def create_indices(self) -> None:
|
|
45
|
+
assert self._connection is not None, "Database connection not initialized!"
|
|
46
|
+
for index_name, table_name, index_columns in DB_INDICES:
|
|
47
|
+
await self._connection.execute(
|
|
48
|
+
f"CREATE INDEX IF NOT EXISTS {index_name} ON {table_name} ({', '.join(index_columns)})"
|
|
49
|
+
)
|
|
50
|
+
await self._connection.commit()
|
|
51
|
+
|
|
52
|
+
async def drop_indices(self) -> None:
|
|
53
|
+
assert self._connection is not None, "Database connection not initialized!"
|
|
54
|
+
for index_name, _, _ in DB_INDICES:
|
|
55
|
+
await self._connection.execute(f"DROP INDEX IF EXISTS {index_name}")
|
|
56
|
+
await self._connection.commit()
|
|
57
|
+
|
|
58
|
+
async def perform_rebuild_for_origin(
|
|
59
|
+
self, origin_index: int, symbols: Iterable[ScannedSymbol]
|
|
60
|
+
) -> None:
|
|
61
|
+
assert self._connection is not None, "Database connection not initialized!"
|
|
62
|
+
|
|
63
|
+
async with self._connection.cursor() as cursor:
|
|
64
|
+
await cursor.execute("BEGIN")
|
|
65
|
+
logger.info(f"Rebuilding symbols for origin_index: {origin_index}")
|
|
66
|
+
await cursor.execute(
|
|
67
|
+
f"""
|
|
68
|
+
DELETE FROM {DB_SYMBOLS_NAME} WHERE origin_index = ?
|
|
69
|
+
""",
|
|
70
|
+
(origin_index,),
|
|
71
|
+
)
|
|
72
|
+
await cursor.executemany(
|
|
73
|
+
"""
|
|
74
|
+
INSERT INTO symbols (generation, kind, name, origin_index, parent_module, metadata_json)
|
|
75
|
+
VALUES (1, ?, ?, ?, ?, ?)
|
|
76
|
+
""",
|
|
77
|
+
(
|
|
78
|
+
(symbol.kind, symbol.name, origin_index, symbol.parent_module, "")
|
|
79
|
+
for symbol in symbols
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Added {cursor.rowcount} symbols for origin_index: {origin_index}"
|
|
84
|
+
)
|
|
85
|
+
await cursor.execute("COMMIT")
|
|
86
|
+
|
|
87
|
+
async def lookup_symbol_name(self, name_part: str) -> AsyncIterable[ScannedSymbol]:
|
|
88
|
+
assert self._connection is not None, "Database connection not initialized!"
|
|
89
|
+
cursor = await self._connection.execute(
|
|
90
|
+
"""
|
|
91
|
+
SELECT kind, name, parent_module
|
|
92
|
+
FROM symbols
|
|
93
|
+
WHERE name >= ? AND name < ?
|
|
94
|
+
""",
|
|
95
|
+
(f"{name_part}", f"{name_part}~"),
|
|
96
|
+
)
|
|
97
|
+
async for row in cursor:
|
|
98
|
+
yield ScannedSymbol(*row, metadata={})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: import-completer
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Requires-Python: >=3.13
|
|
5
|
+
Requires-Dist: aiosqlite==0.21.0
|
|
6
|
+
Requires-Dist: click>=8.0.0
|
|
7
|
+
Requires-Dist: loguru==0.7.3
|
|
8
|
+
Requires-Dist: pygls==2.0.0
|
|
9
|
+
Requires-Dist: tree-sitter-python==0.23.6
|
|
10
|
+
Requires-Dist: tree-sitter==0.24.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import_completer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
import_completer/code_scanner.py,sha256=99P9Zn2PVN-JUV6jq1ATExfRA7xU3394ZO_Pntl5c-M,6256
|
|
3
|
+
import_completer/completion_engine.py,sha256=R8QhTsdU7DUfbiINpfGz-7azncDj1hWPFEyTzwb1I14,2929
|
|
4
|
+
import_completer/config.py,sha256=EQJdEPwiTjD52rsqPuPipHRvG8_V3LpYZmJK8Xf6Kzo,637
|
|
5
|
+
import_completer/language_server.py,sha256=uM3kgaRfM_mMvR7nGTNJuOKmiS9oGvDPQf5CasonVdo,6764
|
|
6
|
+
import_completer/path_discovery.py,sha256=QFtzI229QR_4hkww8EAShbLOBCWadbFOTKfvuVK57vk,769
|
|
7
|
+
import_completer/symbols_database.py,sha256=xhnqXc094doy1V4cVJqkuqAXFHMvAkpMpWCP1rBiArw,3677
|
|
8
|
+
import_completer/types.py,sha256=KYpDfQ6MTPCD1S2zAO81nXWLr-yvW3ZbC3MGGYdArrk,214
|
|
9
|
+
import_completer-0.4.0.dist-info/METADATA,sha256=7UK9c9gslfBpR4R5cDtQAugSlCMGae4bQazjZyJIlMU,398
|
|
10
|
+
import_completer-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
import_completer-0.4.0.dist-info/entry_points.txt,sha256=TVRn0ssT9CrESNAP4vRIsEqykaHVsrGC2dKKH-XcYKk,93
|
|
12
|
+
import_completer-0.4.0.dist-info/RECORD,,
|