deepnote-python-lsp-server 1.13.1rc2__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.
Files changed (46) hide show
  1. deepnote_python_lsp_server-1.13.1rc2.dist-info/METADATA +264 -0
  2. deepnote_python_lsp_server-1.13.1rc2.dist-info/RECORD +46 -0
  3. deepnote_python_lsp_server-1.13.1rc2.dist-info/WHEEL +5 -0
  4. deepnote_python_lsp_server-1.13.1rc2.dist-info/entry_points.txt +25 -0
  5. deepnote_python_lsp_server-1.13.1rc2.dist-info/licenses/LICENSE +22 -0
  6. deepnote_python_lsp_server-1.13.1rc2.dist-info/top_level.txt +1 -0
  7. pylsp/__init__.py +33 -0
  8. pylsp/__main__.py +132 -0
  9. pylsp/_utils.py +418 -0
  10. pylsp/_version.py +1 -0
  11. pylsp/config/__init__.py +2 -0
  12. pylsp/config/config.py +203 -0
  13. pylsp/config/flake8_conf.py +64 -0
  14. pylsp/config/pycodestyle_conf.py +34 -0
  15. pylsp/config/source.py +88 -0
  16. pylsp/hookspecs.py +150 -0
  17. pylsp/lsp.py +115 -0
  18. pylsp/plugins/__init__.py +2 -0
  19. pylsp/plugins/_resolvers.py +132 -0
  20. pylsp/plugins/_rope_task_handle.py +107 -0
  21. pylsp/plugins/autopep8_format.py +95 -0
  22. pylsp/plugins/definition.py +84 -0
  23. pylsp/plugins/flake8_lint.py +243 -0
  24. pylsp/plugins/folding.py +210 -0
  25. pylsp/plugins/highlight.py +36 -0
  26. pylsp/plugins/hover.py +59 -0
  27. pylsp/plugins/jedi_completion.py +337 -0
  28. pylsp/plugins/jedi_rename.py +56 -0
  29. pylsp/plugins/mccabe_lint.py +56 -0
  30. pylsp/plugins/preload_imports.py +72 -0
  31. pylsp/plugins/pycodestyle_lint.py +113 -0
  32. pylsp/plugins/pydocstyle_lint.py +127 -0
  33. pylsp/plugins/pyflakes_lint.py +97 -0
  34. pylsp/plugins/pylint_lint.py +367 -0
  35. pylsp/plugins/references.py +33 -0
  36. pylsp/plugins/rope_autoimport.py +408 -0
  37. pylsp/plugins/rope_completion.py +161 -0
  38. pylsp/plugins/signature.py +81 -0
  39. pylsp/plugins/symbols.py +219 -0
  40. pylsp/plugins/type_definition.py +38 -0
  41. pylsp/plugins/yapf_format.py +199 -0
  42. pylsp/py.typed +0 -0
  43. pylsp/python_lsp.py +945 -0
  44. pylsp/text_edit.py +97 -0
  45. pylsp/uris.py +139 -0
  46. pylsp/workspace.py +791 -0
@@ -0,0 +1,408 @@
1
+ # Copyright 2022- Python Language Server Contributors.
2
+
3
+ import logging
4
+ import threading
5
+ from collections.abc import Generator
6
+ from typing import Any, Optional, Union
7
+
8
+ import parso
9
+ from jedi import Script
10
+ from parso.python import tree
11
+ from parso.tree import NodeOrLeaf
12
+ from rope.base.resources import Resource
13
+ from rope.contrib.autoimport.defs import SearchResult
14
+ from rope.contrib.autoimport.sqlite import AutoImport
15
+
16
+ from pylsp import hookimpl
17
+ from pylsp.config.config import Config
18
+ from pylsp.workspace import Document, Workspace
19
+
20
+ from ._rope_task_handle import PylspTaskHandle
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ _score_pow = 5
25
+ _score_max = 10**_score_pow
26
+ MAX_RESULTS_COMPLETIONS = 1000
27
+ MAX_RESULTS_CODE_ACTIONS = 5
28
+
29
+
30
+ class AutoimportCache:
31
+ """Handles the cache creation."""
32
+
33
+ def __init__(self) -> None:
34
+ self.thread = None
35
+
36
+ def reload_cache(
37
+ self,
38
+ config: Config,
39
+ workspace: Workspace,
40
+ files: Optional[list[Document]] = None,
41
+ single_thread: Optional[bool] = True,
42
+ ):
43
+ if self.is_blocked():
44
+ return
45
+
46
+ memory: bool = config.plugin_settings("rope_autoimport").get("memory", False)
47
+ rope_config = config.settings().get("rope", {})
48
+ autoimport = workspace._rope_autoimport(rope_config, memory)
49
+ resources: Optional[list[Resource]] = (
50
+ None
51
+ if files is None
52
+ else [document._rope_resource(rope_config) for document in files]
53
+ )
54
+
55
+ if single_thread:
56
+ self._reload_cache(workspace, autoimport, resources)
57
+ else:
58
+ # Creating the cache may take 10-20s for a environment with 5k python modules. That's
59
+ # why we decided to move cache creation into its own thread.
60
+ self.thread = threading.Thread(
61
+ target=self._reload_cache, args=(workspace, autoimport, resources)
62
+ )
63
+ self.thread.start()
64
+
65
+ def _reload_cache(
66
+ self,
67
+ workspace: Workspace,
68
+ autoimport: AutoImport,
69
+ resources: Optional[list[Resource]] = None,
70
+ ) -> None:
71
+ task_handle = PylspTaskHandle(workspace)
72
+ autoimport.generate_cache(task_handle=task_handle, resources=resources)
73
+ autoimport.generate_modules_cache(task_handle=task_handle)
74
+
75
+ def is_blocked(self):
76
+ return self.thread and self.thread.is_alive()
77
+
78
+
79
+ @hookimpl
80
+ def pylsp_settings() -> dict[str, dict[str, dict[str, Any]]]:
81
+ # Default rope_completion to disabled
82
+ return {
83
+ "plugins": {
84
+ "rope_autoimport": {
85
+ "enabled": False,
86
+ "memory": False,
87
+ "completions": {
88
+ "enabled": True,
89
+ },
90
+ "code_actions": {
91
+ "enabled": True,
92
+ },
93
+ }
94
+ }
95
+ }
96
+
97
+
98
+ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool:
99
+ """
100
+ Check if we should insert the word_node on the given expr.
101
+
102
+ Works for both correct and incorrect code. This is because the
103
+ user is often working on the code as they write it.
104
+ """
105
+ if not word_node:
106
+ return False
107
+ if len(expr.children) == 0:
108
+ return True
109
+ first_child = expr.children[0]
110
+ if isinstance(first_child, tree.EndMarker):
111
+ if "#" in first_child.prefix:
112
+ return False # Check for single line comment
113
+ if first_child == word_node:
114
+ return True # If the word is the first word then its fine
115
+ if len(expr.children) > 1:
116
+ if any(
117
+ node.type == "operator" and "." in node.value or node.type == "trailer"
118
+ for node in expr.children
119
+ ):
120
+ return False # Check if we're on a method of a function
121
+ if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)):
122
+ # The tree will often include error nodes like this to indicate errors
123
+ # we want to ignore errors since the code is being written
124
+ return _should_insert(first_child, word_node)
125
+ return _handle_first_child(first_child, expr, word_node)
126
+
127
+
128
+ def _handle_first_child(
129
+ first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf
130
+ ) -> bool:
131
+ """Check if we suggest imports given the following first child."""
132
+ if isinstance(first_child, tree.Import):
133
+ return False
134
+ if isinstance(first_child, (tree.PythonLeaf, tree.PythonErrorLeaf)):
135
+ # Check if the first item is a from or import statement even when incomplete
136
+ if first_child.value in ("import", "from"):
137
+ return False
138
+ if isinstance(first_child, tree.Keyword):
139
+ if first_child.value == "def":
140
+ return _should_import_function(word_node, expr)
141
+ if first_child.value == "class":
142
+ return _should_import_class(word_node, expr)
143
+ return True
144
+
145
+
146
+ def _should_import_class(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
147
+ prev_node = None
148
+ for node in expr.children:
149
+ if isinstance(node, tree.Name):
150
+ if isinstance(prev_node, tree.Operator):
151
+ if node == word_node and prev_node.value == "(":
152
+ return True
153
+ prev_node = node
154
+
155
+ return False
156
+
157
+
158
+ def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
159
+ prev_node = None
160
+ for node in expr.children:
161
+ if _handle_argument(node, word_node):
162
+ return True
163
+ if isinstance(prev_node, tree.Operator):
164
+ if prev_node.value == "->":
165
+ if node == word_node:
166
+ return True
167
+ prev_node = node
168
+ return False
169
+
170
+
171
+ def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf):
172
+ if isinstance(node, tree.PythonNode):
173
+ if node.type == "tfpdef":
174
+ if node.children[2] == word_node:
175
+ return True
176
+ if node.type == "parameters":
177
+ for parameter in node.children:
178
+ if _handle_argument(parameter, word_node):
179
+ return True
180
+ return False
181
+
182
+
183
+ def _process_statements(
184
+ suggestions: list[SearchResult],
185
+ doc_uri: str,
186
+ word: str,
187
+ autoimport: AutoImport,
188
+ document: Document,
189
+ feature: str = "completions",
190
+ ) -> Generator[dict[str, Any], None, None]:
191
+ for suggestion in suggestions:
192
+ insert_line = autoimport.find_insertion_line(document.source) - 1
193
+ start = {"line": insert_line, "character": 0}
194
+ edit_range = {"start": start, "end": start}
195
+ edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"}
196
+ score = _get_score(
197
+ suggestion.source, suggestion.import_statement, suggestion.name, word
198
+ )
199
+ if score > _score_max:
200
+ continue
201
+ # TODO make this markdown
202
+ if feature == "completions":
203
+ yield {
204
+ "label": suggestion.name,
205
+ "kind": suggestion.itemkind,
206
+ "sortText": _sort_import(score),
207
+ "data": {"doc_uri": doc_uri},
208
+ "detail": _document(suggestion.import_statement),
209
+ "additionalTextEdits": [edit],
210
+ }
211
+ elif feature == "code_actions":
212
+ yield {
213
+ "title": suggestion.import_statement,
214
+ "kind": "quickfix",
215
+ "edit": {"changes": {doc_uri: [edit]}},
216
+ # data is a supported field for codeAction responses
217
+ # See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction
218
+ "data": {"sortText": _sort_import(score)},
219
+ }
220
+ else:
221
+ raise ValueError(f"Unknown feature: {feature}")
222
+
223
+
224
+ def get_names(script: Script) -> set[str]:
225
+ """Get all names to ignore from the current file."""
226
+ raw_names = script.get_names(definitions=True)
227
+ log.debug(raw_names)
228
+ return {name.name for name in raw_names}
229
+
230
+
231
+ @hookimpl
232
+ def pylsp_completions(
233
+ config: Config,
234
+ workspace: Workspace,
235
+ document: Document,
236
+ position,
237
+ ignored_names: Union[set[str], None],
238
+ ):
239
+ """Get autoimport suggestions."""
240
+ if (
241
+ not config.plugin_settings("rope_autoimport")
242
+ .get("completions", {})
243
+ .get("enabled", True)
244
+ ) or cache.is_blocked():
245
+ return []
246
+
247
+ line = document.lines[position["line"]]
248
+ expr = parso.parse(line)
249
+ word_node = expr.get_leaf_for_position((1, position["character"]))
250
+ if not _should_insert(expr, word_node):
251
+ return []
252
+ word = word_node.value
253
+ log.debug(f"autoimport: searching for word: {word}")
254
+ rope_config = config.settings(document_path=document.path).get("rope", {})
255
+ ignored_names: set[str] = ignored_names or get_names(
256
+ document.jedi_script(use_document_path=True)
257
+ )
258
+ autoimport = workspace._rope_autoimport(rope_config)
259
+ suggestions = list(autoimport.search_full(word, ignored_names=ignored_names))
260
+ results = sorted(
261
+ _process_statements(
262
+ suggestions, document.uri, word, autoimport, document, "completions"
263
+ ),
264
+ key=lambda statement: statement["sortText"],
265
+ )
266
+ if len(results) > MAX_RESULTS_COMPLETIONS:
267
+ results = results[:MAX_RESULTS_COMPLETIONS]
268
+ return results
269
+
270
+
271
+ def _document(import_statement: str) -> str:
272
+ return """# Auto-Import\n""" + import_statement
273
+
274
+
275
+ def _get_score(
276
+ source: int, full_statement: str, suggested_name: str, desired_name
277
+ ) -> int:
278
+ import_length = len("import")
279
+ full_statement_score = len(full_statement) - import_length
280
+ suggested_name_score = (len(suggested_name) - len(desired_name)) ** 2
281
+ source_score = 20 * source
282
+ return suggested_name_score + full_statement_score + source_score
283
+
284
+
285
+ def _sort_import(score: int) -> str:
286
+ score = max(min(score, (_score_max) - 1), 0)
287
+ # Since we are using ints, we need to pad them.
288
+ # We also want to prioritize autoimport behind everything since its the last priority.
289
+ # The minimum is to prevent score from overflowing the pad
290
+ return "[z" + str(score).rjust(_score_pow, "0")
291
+
292
+
293
+ def get_name_or_module(document, diagnostic) -> str:
294
+ start = diagnostic["range"]["start"]
295
+ return (
296
+ parso.parse(document.lines[start["line"]])
297
+ .get_leaf_for_position((1, start["character"] + 1))
298
+ .value
299
+ )
300
+
301
+
302
+ @hookimpl
303
+ def pylsp_code_actions(
304
+ config: Config,
305
+ workspace: Workspace,
306
+ document: Document,
307
+ range: dict,
308
+ context: dict,
309
+ ) -> list[dict]:
310
+ """
311
+ Provide code actions through rope.
312
+
313
+ Parameters
314
+ ----------
315
+ config : pylsp.config.config.Config
316
+ Current config.
317
+ workspace : pylsp.workspace.Workspace
318
+ Current workspace.
319
+ document : pylsp.workspace.Document
320
+ Document to apply code actions on.
321
+ range : dict
322
+ Range argument given by pylsp. Not used here.
323
+ context : dict
324
+ CodeActionContext given as dict.
325
+
326
+ Returns
327
+ -------
328
+ List of dicts containing the code actions.
329
+ """
330
+ if (
331
+ not config.plugin_settings("rope_autoimport")
332
+ .get("code_actions", {})
333
+ .get("enabled", True)
334
+ ) or cache.is_blocked():
335
+ return []
336
+
337
+ log.debug(f"textDocument/codeAction: {document} {range} {context}")
338
+ code_actions = []
339
+ for diagnostic in context.get("diagnostics", []):
340
+ if "undefined name" not in diagnostic.get("message", "").lower():
341
+ continue
342
+
343
+ word = get_name_or_module(document, diagnostic)
344
+ log.debug(f"autoimport: searching for word: {word}")
345
+ rope_config = config.settings(document_path=document.path).get("rope", {})
346
+ autoimport = workspace._rope_autoimport(rope_config)
347
+ suggestions = list(autoimport.search_full(word))
348
+ log.debug("autoimport: suggestions: %s", suggestions)
349
+ results = sorted(
350
+ _process_statements(
351
+ suggestions,
352
+ document.uri,
353
+ word,
354
+ autoimport,
355
+ document,
356
+ "code_actions",
357
+ ),
358
+ key=lambda statement: statement["data"]["sortText"],
359
+ )
360
+
361
+ if len(results) > MAX_RESULTS_CODE_ACTIONS:
362
+ results = results[:MAX_RESULTS_CODE_ACTIONS]
363
+ code_actions.extend(results)
364
+
365
+ return code_actions
366
+
367
+
368
+ @hookimpl
369
+ def pylsp_initialize(config: Config, workspace: Workspace) -> None:
370
+ """Initialize AutoImport.
371
+
372
+ Generates the cache for local and global items.
373
+ """
374
+ cache.reload_cache(config, workspace)
375
+
376
+
377
+ @hookimpl
378
+ def pylsp_document_did_open(config: Config, workspace: Workspace) -> None:
379
+ """Initialize AutoImport.
380
+
381
+ Generates the cache for local and global items.
382
+ """
383
+ cache.reload_cache(config, workspace)
384
+
385
+
386
+ @hookimpl
387
+ def pylsp_document_did_save(
388
+ config: Config, workspace: Workspace, document: Document
389
+ ) -> None:
390
+ """Update the names associated with this document."""
391
+ cache.reload_cache(config, workspace, [document])
392
+
393
+
394
+ @hookimpl
395
+ def pylsp_workspace_configuration_changed(config: Config, workspace: Workspace) -> None:
396
+ """
397
+ Initialize autoimport if it has been enabled through a
398
+ workspace/didChangeConfiguration message from the frontend.
399
+
400
+ Generates the cache for local and global items.
401
+ """
402
+ if config.plugin_settings("rope_autoimport").get("enabled", False):
403
+ cache.reload_cache(config, workspace)
404
+ else:
405
+ log.debug("autoimport: Skipping cache reload.")
406
+
407
+
408
+ cache: AutoimportCache = AutoimportCache()
@@ -0,0 +1,161 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+
6
+ from rope.contrib.codeassist import code_assist, sorted_proposals
7
+
8
+ from pylsp import _utils, hookimpl, lsp
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ @hookimpl
14
+ def pylsp_settings():
15
+ # Default rope_completion to disabled
16
+ return {"plugins": {"rope_completion": {"enabled": False, "eager": False}}}
17
+
18
+
19
+ def _resolve_completion(completion, data, markup_kind):
20
+ try:
21
+ doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind)
22
+ except Exception as e:
23
+ log.debug("Failed to resolve Rope completion: %s", e)
24
+ doc = ""
25
+ completion["detail"] = "{} {}".format(data.scope or "", data.name)
26
+ completion["documentation"] = doc
27
+ return completion
28
+
29
+
30
+ @hookimpl
31
+ def pylsp_completions(config, workspace, document, position):
32
+ settings = config.plugin_settings("rope_completion", document_path=document.path)
33
+ resolve_eagerly = settings.get("eager", False)
34
+
35
+ # Rope is a bit rubbish at completing module imports, so we'll return None
36
+ word = document.word_at_position(
37
+ {
38
+ # The -1 should really be trying to look at the previous word, but that might be quite expensive
39
+ # So we only skip import completions when the cursor is one space after `import`
40
+ "line": position["line"],
41
+ "character": max(position["character"] - 1, 0),
42
+ }
43
+ )
44
+ if word == "import":
45
+ return None
46
+
47
+ offset = document.offset_at_position(position)
48
+ rope_config = config.settings(document_path=document.path).get("rope", {})
49
+ rope_project = workspace._rope_project_builder(rope_config)
50
+ document_rope = document._rope_resource(rope_config)
51
+
52
+ completion_capabilities = config.capabilities.get("textDocument", {}).get(
53
+ "completion", {}
54
+ )
55
+ item_capabilities = completion_capabilities.get("completionItem", {})
56
+ supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
57
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
58
+
59
+ try:
60
+ definitions = code_assist(
61
+ rope_project, document.source, offset, document_rope, maxfixes=3
62
+ )
63
+ except Exception as e:
64
+ log.debug("Failed to run Rope code assist: %s", e)
65
+ return []
66
+
67
+ definitions = sorted_proposals(definitions)
68
+ new_definitions = []
69
+ for d in definitions:
70
+ item = {
71
+ "label": d.name,
72
+ "kind": _kind(d),
73
+ "sortText": _sort_text(d),
74
+ "data": {"doc_uri": document.uri},
75
+ }
76
+ if resolve_eagerly:
77
+ item = _resolve_completion(item, d, preferred_markup_kind)
78
+ new_definitions.append(item)
79
+
80
+ # most recently retrieved completion items, used for resolution
81
+ document.shared_data["LAST_ROPE_COMPLETIONS"] = {
82
+ # label is the only required property; here it is assumed to be unique
83
+ completion["label"]: (completion, data)
84
+ for completion, data in zip(new_definitions, definitions)
85
+ }
86
+
87
+ definitions = new_definitions
88
+
89
+ return definitions or None
90
+
91
+
92
+ @hookimpl
93
+ def pylsp_completion_item_resolve(config, completion_item, document):
94
+ """Resolve formatted completion for given non-resolved completion"""
95
+ shared_data = document.shared_data["LAST_ROPE_COMPLETIONS"].get(
96
+ completion_item["label"]
97
+ )
98
+
99
+ completion_capabilities = config.capabilities.get("textDocument", {}).get(
100
+ "completion", {}
101
+ )
102
+ item_capabilities = completion_capabilities.get("completionItem", {})
103
+ supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
104
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
105
+
106
+ if shared_data:
107
+ completion, data = shared_data
108
+ return _resolve_completion(completion, data, preferred_markup_kind)
109
+ return completion_item
110
+
111
+
112
+ def _sort_text(definition):
113
+ """Ensure builtins appear at the bottom.
114
+ Description is of format <type>: <module>.<item>
115
+ """
116
+ if definition.name.startswith("_"):
117
+ # It's a 'hidden' func, put it next last
118
+ return "z" + definition.name
119
+ if definition.scope == "builtin":
120
+ return "y" + definition.name
121
+
122
+ # Else put it at the front
123
+ return "a" + definition.name
124
+
125
+
126
+ def _kind(d):
127
+ """Return the LSP type"""
128
+ MAP = {
129
+ "none": lsp.CompletionItemKind.Value,
130
+ "type": lsp.CompletionItemKind.Class,
131
+ "tuple": lsp.CompletionItemKind.Class,
132
+ "dict": lsp.CompletionItemKind.Class,
133
+ "dictionary": lsp.CompletionItemKind.Class,
134
+ "function": lsp.CompletionItemKind.Function,
135
+ "lambda": lsp.CompletionItemKind.Function,
136
+ "generator": lsp.CompletionItemKind.Function,
137
+ "class": lsp.CompletionItemKind.Class,
138
+ "instance": lsp.CompletionItemKind.Reference,
139
+ "method": lsp.CompletionItemKind.Method,
140
+ "builtin": lsp.CompletionItemKind.Class,
141
+ "builtinfunction": lsp.CompletionItemKind.Function,
142
+ "module": lsp.CompletionItemKind.Module,
143
+ "file": lsp.CompletionItemKind.File,
144
+ "xrange": lsp.CompletionItemKind.Class,
145
+ "slice": lsp.CompletionItemKind.Class,
146
+ "traceback": lsp.CompletionItemKind.Class,
147
+ "frame": lsp.CompletionItemKind.Class,
148
+ "buffer": lsp.CompletionItemKind.Class,
149
+ "dictproxy": lsp.CompletionItemKind.Class,
150
+ "funcdef": lsp.CompletionItemKind.Function,
151
+ "property": lsp.CompletionItemKind.Property,
152
+ "import": lsp.CompletionItemKind.Module,
153
+ "keyword": lsp.CompletionItemKind.Keyword,
154
+ "constant": lsp.CompletionItemKind.Variable,
155
+ "variable": lsp.CompletionItemKind.Variable,
156
+ "value": lsp.CompletionItemKind.Value,
157
+ "param": lsp.CompletionItemKind.Variable,
158
+ "statement": lsp.CompletionItemKind.Keyword,
159
+ }
160
+
161
+ return MAP.get(d.type)
@@ -0,0 +1,81 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+ import re
6
+
7
+ from pylsp import _utils, hookimpl
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+ SPHINX = re.compile(r"\s*:param\s+(?P<param>\w+):\s*(?P<doc>[^\n]+)")
12
+ EPYDOC = re.compile(r"\s*@param\s+(?P<param>\w+):\s*(?P<doc>[^\n]+)")
13
+ GOOGLE = re.compile(r"\s*(?P<param>\w+).*:\s*(?P<doc>[^\n]+)")
14
+
15
+ DOC_REGEX = [SPHINX, EPYDOC, GOOGLE]
16
+
17
+
18
+ @hookimpl
19
+ def pylsp_signature_help(config, document, position):
20
+ code_position = _utils.position_to_jedi_linecolumn(document, position)
21
+ signatures = document.jedi_script().get_signatures(**code_position)
22
+
23
+ if not signatures:
24
+ return {"signatures": []}
25
+
26
+ signature_capabilities = config.capabilities.get("textDocument", {}).get(
27
+ "signatureHelp", {}
28
+ )
29
+ signature_information_support = signature_capabilities.get(
30
+ "signatureInformation", {}
31
+ )
32
+ supported_markup_kinds = signature_information_support.get(
33
+ "documentationFormat", ["markdown"]
34
+ )
35
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
36
+
37
+ s = signatures[0]
38
+
39
+ docstring = s.docstring()
40
+
41
+ # Docstring contains one or more lines of signature, followed by empty line, followed by docstring
42
+ function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
43
+ function_sig = " ".join([line.strip() for line in function_sig_lines])
44
+ sig = {
45
+ "label": function_sig,
46
+ "documentation": _utils.format_docstring(
47
+ s.docstring(raw=True), markup_kind=preferred_markup_kind
48
+ )["value"],
49
+ }
50
+
51
+ # If there are params, add those
52
+ if s.params:
53
+ sig["parameters"] = [
54
+ {
55
+ "label": p.name,
56
+ "documentation": _utils.format_docstring(
57
+ _param_docs(docstring, p.name), markup_kind=preferred_markup_kind
58
+ )["value"],
59
+ }
60
+ for p in s.params
61
+ ]
62
+
63
+ # We only return a single signature because Python doesn't allow overloading
64
+ sig_info = {"signatures": [sig], "activeSignature": 0}
65
+
66
+ if s.index is not None and s.params:
67
+ # Then we know which parameter we're looking at
68
+ sig_info["activeParameter"] = s.index
69
+
70
+ return sig_info
71
+
72
+
73
+ def _param_docs(docstring, param_name):
74
+ for line in docstring.splitlines():
75
+ for regex in DOC_REGEX:
76
+ m = regex.match(line)
77
+ if not m:
78
+ continue
79
+ if m.group("param") != param_name:
80
+ continue
81
+ return m.group("doc") or ""