deepnote-python-lsp-server 0.0.0__py2.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 (43) hide show
  1. deepnote_python_lsp_server-0.0.0.dist-info/METADATA +263 -0
  2. deepnote_python_lsp_server-0.0.0.dist-info/RECORD +43 -0
  3. deepnote_python_lsp_server-0.0.0.dist-info/WHEEL +6 -0
  4. deepnote_python_lsp_server-0.0.0.dist-info/entry_points.txt +24 -0
  5. deepnote_python_lsp_server-0.0.0.dist-info/licenses/LICENSE +22 -0
  6. deepnote_python_lsp_server-0.0.0.dist-info/top_level.txt +1 -0
  7. pylsp/__init__.py +33 -0
  8. pylsp/__main__.py +132 -0
  9. pylsp/_utils.py +325 -0
  10. pylsp/config/__init__.py +2 -0
  11. pylsp/config/config.py +202 -0
  12. pylsp/config/flake8_conf.py +64 -0
  13. pylsp/config/pycodestyle_conf.py +34 -0
  14. pylsp/config/source.py +88 -0
  15. pylsp/hookspecs.py +140 -0
  16. pylsp/lsp.py +115 -0
  17. pylsp/plugins/__init__.py +2 -0
  18. pylsp/plugins/_resolvers.py +132 -0
  19. pylsp/plugins/_rope_task_handle.py +106 -0
  20. pylsp/plugins/autopep8_format.py +95 -0
  21. pylsp/plugins/definition.py +84 -0
  22. pylsp/plugins/flake8_lint.py +243 -0
  23. pylsp/plugins/folding.py +210 -0
  24. pylsp/plugins/highlight.py +36 -0
  25. pylsp/plugins/hover.py +57 -0
  26. pylsp/plugins/jedi_completion.py +318 -0
  27. pylsp/plugins/jedi_rename.py +56 -0
  28. pylsp/plugins/mccabe_lint.py +56 -0
  29. pylsp/plugins/preload_imports.py +38 -0
  30. pylsp/plugins/pycodestyle_lint.py +113 -0
  31. pylsp/plugins/pydocstyle_lint.py +127 -0
  32. pylsp/plugins/pyflakes_lint.py +97 -0
  33. pylsp/plugins/pylint_lint.py +367 -0
  34. pylsp/plugins/references.py +33 -0
  35. pylsp/plugins/rope_autoimport.py +407 -0
  36. pylsp/plugins/rope_completion.py +161 -0
  37. pylsp/plugins/signature.py +81 -0
  38. pylsp/plugins/symbols.py +214 -0
  39. pylsp/plugins/yapf_format.py +199 -0
  40. pylsp/python_lsp.py +921 -0
  41. pylsp/text_edit.py +97 -0
  42. pylsp/uris.py +139 -0
  43. pylsp/workspace.py +730 -0
@@ -0,0 +1,407 @@
1
+ # Copyright 2022- Python Language Server Contributors.
2
+
3
+ import logging
4
+ import threading
5
+ from typing import Any, Dict, Generator, List, Optional, Set, Union
6
+
7
+ import parso
8
+ from jedi import Script
9
+ from parso.python import tree
10
+ from parso.tree import NodeOrLeaf
11
+ from rope.base.resources import Resource
12
+ from rope.contrib.autoimport.defs import SearchResult
13
+ from rope.contrib.autoimport.sqlite import AutoImport
14
+
15
+ from pylsp import hookimpl
16
+ from pylsp.config.config import Config
17
+ from pylsp.workspace import Document, Workspace
18
+
19
+ from ._rope_task_handle import PylspTaskHandle
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ _score_pow = 5
24
+ _score_max = 10**_score_pow
25
+ MAX_RESULTS_COMPLETIONS = 1000
26
+ MAX_RESULTS_CODE_ACTIONS = 5
27
+
28
+
29
+ class AutoimportCache:
30
+ """Handles the cache creation."""
31
+
32
+ def __init__(self) -> None:
33
+ self.thread = None
34
+
35
+ def reload_cache(
36
+ self,
37
+ config: Config,
38
+ workspace: Workspace,
39
+ files: Optional[List[Document]] = None,
40
+ single_thread: Optional[bool] = True,
41
+ ):
42
+ if self.is_blocked():
43
+ return
44
+
45
+ memory: bool = config.plugin_settings("rope_autoimport").get("memory", False)
46
+ rope_config = config.settings().get("rope", {})
47
+ autoimport = workspace._rope_autoimport(rope_config, memory)
48
+ resources: Optional[List[Resource]] = (
49
+ None
50
+ if files is None
51
+ else [document._rope_resource(rope_config) for document in files]
52
+ )
53
+
54
+ if single_thread:
55
+ self._reload_cache(workspace, autoimport, resources)
56
+ else:
57
+ # Creating the cache may take 10-20s for a environment with 5k python modules. That's
58
+ # why we decided to move cache creation into its own thread.
59
+ self.thread = threading.Thread(
60
+ target=self._reload_cache, args=(workspace, autoimport, resources)
61
+ )
62
+ self.thread.start()
63
+
64
+ def _reload_cache(
65
+ self,
66
+ workspace: Workspace,
67
+ autoimport: AutoImport,
68
+ resources: Optional[List[Resource]] = None,
69
+ ) -> None:
70
+ task_handle = PylspTaskHandle(workspace)
71
+ autoimport.generate_cache(task_handle=task_handle, resources=resources)
72
+ autoimport.generate_modules_cache(task_handle=task_handle)
73
+
74
+ def is_blocked(self):
75
+ return self.thread and self.thread.is_alive()
76
+
77
+
78
+ @hookimpl
79
+ def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]:
80
+ # Default rope_completion to disabled
81
+ return {
82
+ "plugins": {
83
+ "rope_autoimport": {
84
+ "enabled": False,
85
+ "memory": False,
86
+ "completions": {
87
+ "enabled": True,
88
+ },
89
+ "code_actions": {
90
+ "enabled": True,
91
+ },
92
+ }
93
+ }
94
+ }
95
+
96
+
97
+ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool:
98
+ """
99
+ Check if we should insert the word_node on the given expr.
100
+
101
+ Works for both correct and incorrect code. This is because the
102
+ user is often working on the code as they write it.
103
+ """
104
+ if not word_node:
105
+ return False
106
+ if len(expr.children) == 0:
107
+ return True
108
+ first_child = expr.children[0]
109
+ if isinstance(first_child, tree.EndMarker):
110
+ if "#" in first_child.prefix:
111
+ return False # Check for single line comment
112
+ if first_child == word_node:
113
+ return True # If the word is the first word then its fine
114
+ if len(expr.children) > 1:
115
+ if any(
116
+ node.type == "operator" and "." in node.value or node.type == "trailer"
117
+ for node in expr.children
118
+ ):
119
+ return False # Check if we're on a method of a function
120
+ if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)):
121
+ # The tree will often include error nodes like this to indicate errors
122
+ # we want to ignore errors since the code is being written
123
+ return _should_insert(first_child, word_node)
124
+ return _handle_first_child(first_child, expr, word_node)
125
+
126
+
127
+ def _handle_first_child(
128
+ first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf
129
+ ) -> bool:
130
+ """Check if we suggest imports given the following first child."""
131
+ if isinstance(first_child, tree.Import):
132
+ return False
133
+ if isinstance(first_child, (tree.PythonLeaf, tree.PythonErrorLeaf)):
134
+ # Check if the first item is a from or import statement even when incomplete
135
+ if first_child.value in ("import", "from"):
136
+ return False
137
+ if isinstance(first_child, tree.Keyword):
138
+ if first_child.value == "def":
139
+ return _should_import_function(word_node, expr)
140
+ if first_child.value == "class":
141
+ return _should_import_class(word_node, expr)
142
+ return True
143
+
144
+
145
+ def _should_import_class(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
146
+ prev_node = None
147
+ for node in expr.children:
148
+ if isinstance(node, tree.Name):
149
+ if isinstance(prev_node, tree.Operator):
150
+ if node == word_node and prev_node.value == "(":
151
+ return True
152
+ prev_node = node
153
+
154
+ return False
155
+
156
+
157
+ def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
158
+ prev_node = None
159
+ for node in expr.children:
160
+ if _handle_argument(node, word_node):
161
+ return True
162
+ if isinstance(prev_node, tree.Operator):
163
+ if prev_node.value == "->":
164
+ if node == word_node:
165
+ return True
166
+ prev_node = node
167
+ return False
168
+
169
+
170
+ def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf):
171
+ if isinstance(node, tree.PythonNode):
172
+ if node.type == "tfpdef":
173
+ if node.children[2] == word_node:
174
+ return True
175
+ if node.type == "parameters":
176
+ for parameter in node.children:
177
+ if _handle_argument(parameter, word_node):
178
+ return True
179
+ return False
180
+
181
+
182
+ def _process_statements(
183
+ suggestions: List[SearchResult],
184
+ doc_uri: str,
185
+ word: str,
186
+ autoimport: AutoImport,
187
+ document: Document,
188
+ feature: str = "completions",
189
+ ) -> Generator[Dict[str, Any], None, None]:
190
+ for suggestion in suggestions:
191
+ insert_line = autoimport.find_insertion_line(document.source) - 1
192
+ start = {"line": insert_line, "character": 0}
193
+ edit_range = {"start": start, "end": start}
194
+ edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"}
195
+ score = _get_score(
196
+ suggestion.source, suggestion.import_statement, suggestion.name, word
197
+ )
198
+ if score > _score_max:
199
+ continue
200
+ # TODO make this markdown
201
+ if feature == "completions":
202
+ yield {
203
+ "label": suggestion.name,
204
+ "kind": suggestion.itemkind,
205
+ "sortText": _sort_import(score),
206
+ "data": {"doc_uri": doc_uri},
207
+ "detail": _document(suggestion.import_statement),
208
+ "additionalTextEdits": [edit],
209
+ }
210
+ elif feature == "code_actions":
211
+ yield {
212
+ "title": suggestion.import_statement,
213
+ "kind": "quickfix",
214
+ "edit": {"changes": {doc_uri: [edit]}},
215
+ # data is a supported field for codeAction responses
216
+ # See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction
217
+ "data": {"sortText": _sort_import(score)},
218
+ }
219
+ else:
220
+ raise ValueError(f"Unknown feature: {feature}")
221
+
222
+
223
+ def get_names(script: Script) -> Set[str]:
224
+ """Get all names to ignore from the current file."""
225
+ raw_names = script.get_names(definitions=True)
226
+ log.debug(raw_names)
227
+ return {name.name for name in raw_names}
228
+
229
+
230
+ @hookimpl
231
+ def pylsp_completions(
232
+ config: Config,
233
+ workspace: Workspace,
234
+ document: Document,
235
+ position,
236
+ ignored_names: Union[Set[str], None],
237
+ ):
238
+ """Get autoimport suggestions."""
239
+ if (
240
+ not config.plugin_settings("rope_autoimport")
241
+ .get("completions", {})
242
+ .get("enabled", True)
243
+ ) or cache.is_blocked():
244
+ return []
245
+
246
+ line = document.lines[position["line"]]
247
+ expr = parso.parse(line)
248
+ word_node = expr.get_leaf_for_position((1, position["character"]))
249
+ if not _should_insert(expr, word_node):
250
+ return []
251
+ word = word_node.value
252
+ log.debug(f"autoimport: searching for word: {word}")
253
+ rope_config = config.settings(document_path=document.path).get("rope", {})
254
+ ignored_names: Set[str] = ignored_names or get_names(
255
+ document.jedi_script(use_document_path=True)
256
+ )
257
+ autoimport = workspace._rope_autoimport(rope_config)
258
+ suggestions = list(autoimport.search_full(word, ignored_names=ignored_names))
259
+ results = sorted(
260
+ _process_statements(
261
+ suggestions, document.uri, word, autoimport, document, "completions"
262
+ ),
263
+ key=lambda statement: statement["sortText"],
264
+ )
265
+ if len(results) > MAX_RESULTS_COMPLETIONS:
266
+ results = results[:MAX_RESULTS_COMPLETIONS]
267
+ return results
268
+
269
+
270
+ def _document(import_statement: str) -> str:
271
+ return """# Auto-Import\n""" + import_statement
272
+
273
+
274
+ def _get_score(
275
+ source: int, full_statement: str, suggested_name: str, desired_name
276
+ ) -> int:
277
+ import_length = len("import")
278
+ full_statement_score = len(full_statement) - import_length
279
+ suggested_name_score = (len(suggested_name) - len(desired_name)) ** 2
280
+ source_score = 20 * source
281
+ return suggested_name_score + full_statement_score + source_score
282
+
283
+
284
+ def _sort_import(score: int) -> str:
285
+ score = max(min(score, (_score_max) - 1), 0)
286
+ # Since we are using ints, we need to pad them.
287
+ # We also want to prioritize autoimport behind everything since its the last priority.
288
+ # The minimum is to prevent score from overflowing the pad
289
+ return "[z" + str(score).rjust(_score_pow, "0")
290
+
291
+
292
+ def get_name_or_module(document, diagnostic) -> str:
293
+ start = diagnostic["range"]["start"]
294
+ return (
295
+ parso.parse(document.lines[start["line"]])
296
+ .get_leaf_for_position((1, start["character"] + 1))
297
+ .value
298
+ )
299
+
300
+
301
+ @hookimpl
302
+ def pylsp_code_actions(
303
+ config: Config,
304
+ workspace: Workspace,
305
+ document: Document,
306
+ range: Dict,
307
+ context: Dict,
308
+ ) -> List[Dict]:
309
+ """
310
+ Provide code actions through rope.
311
+
312
+ Parameters
313
+ ----------
314
+ config : pylsp.config.config.Config
315
+ Current config.
316
+ workspace : pylsp.workspace.Workspace
317
+ Current workspace.
318
+ document : pylsp.workspace.Document
319
+ Document to apply code actions on.
320
+ range : Dict
321
+ Range argument given by pylsp. Not used here.
322
+ context : Dict
323
+ CodeActionContext given as dict.
324
+
325
+ Returns
326
+ -------
327
+ List of dicts containing the code actions.
328
+ """
329
+ if (
330
+ not config.plugin_settings("rope_autoimport")
331
+ .get("code_actions", {})
332
+ .get("enabled", True)
333
+ ) or cache.is_blocked():
334
+ return []
335
+
336
+ log.debug(f"textDocument/codeAction: {document} {range} {context}")
337
+ code_actions = []
338
+ for diagnostic in context.get("diagnostics", []):
339
+ if "undefined name" not in diagnostic.get("message", "").lower():
340
+ continue
341
+
342
+ word = get_name_or_module(document, diagnostic)
343
+ log.debug(f"autoimport: searching for word: {word}")
344
+ rope_config = config.settings(document_path=document.path).get("rope", {})
345
+ autoimport = workspace._rope_autoimport(rope_config)
346
+ suggestions = list(autoimport.search_full(word))
347
+ log.debug("autoimport: suggestions: %s", suggestions)
348
+ results = sorted(
349
+ _process_statements(
350
+ suggestions,
351
+ document.uri,
352
+ word,
353
+ autoimport,
354
+ document,
355
+ "code_actions",
356
+ ),
357
+ key=lambda statement: statement["data"]["sortText"],
358
+ )
359
+
360
+ if len(results) > MAX_RESULTS_CODE_ACTIONS:
361
+ results = results[:MAX_RESULTS_CODE_ACTIONS]
362
+ code_actions.extend(results)
363
+
364
+ return code_actions
365
+
366
+
367
+ @hookimpl
368
+ def pylsp_initialize(config: Config, workspace: Workspace) -> None:
369
+ """Initialize AutoImport.
370
+
371
+ Generates the cache for local and global items.
372
+ """
373
+ cache.reload_cache(config, workspace)
374
+
375
+
376
+ @hookimpl
377
+ def pylsp_document_did_open(config: Config, workspace: Workspace) -> None:
378
+ """Initialize AutoImport.
379
+
380
+ Generates the cache for local and global items.
381
+ """
382
+ cache.reload_cache(config, workspace)
383
+
384
+
385
+ @hookimpl
386
+ def pylsp_document_did_save(
387
+ config: Config, workspace: Workspace, document: Document
388
+ ) -> None:
389
+ """Update the names associated with this document."""
390
+ cache.reload_cache(config, workspace, [document])
391
+
392
+
393
+ @hookimpl
394
+ def pylsp_workspace_configuration_changed(config: Config, workspace: Workspace) -> None:
395
+ """
396
+ Initialize autoimport if it has been enabled through a
397
+ workspace/didChangeConfiguration message from the frontend.
398
+
399
+ Generates the cache for local and global items.
400
+ """
401
+ if config.plugin_settings("rope_autoimport").get("enabled", False):
402
+ cache.reload_cache(config, workspace)
403
+ else:
404
+ log.debug("autoimport: Skipping cache reload.")
405
+
406
+
407
+ 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"] = "{0} {1}".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 ""