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,84 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, Dict, List
7
+
8
+ import jedi
9
+
10
+ from pylsp import _utils, hookimpl, uris
11
+
12
+ if TYPE_CHECKING:
13
+ from jedi.api import Script
14
+ from jedi.api.classes import Name
15
+
16
+ from pylsp.config.config import Config
17
+ from pylsp.workspace import Document
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ MAX_JEDI_GOTO_HOPS = 100
23
+
24
+
25
+ def _resolve_definition(
26
+ maybe_defn: Name, script: Script, settings: Dict[str, Any]
27
+ ) -> Name:
28
+ for _ in range(MAX_JEDI_GOTO_HOPS):
29
+ if maybe_defn.is_definition() or maybe_defn.module_path != script.path:
30
+ break
31
+ defns = script.goto(
32
+ follow_imports=settings.get("follow_imports", True),
33
+ follow_builtin_imports=settings.get("follow_builtin_imports", True),
34
+ line=maybe_defn.line,
35
+ column=maybe_defn.column,
36
+ )
37
+ if len(defns) == 1:
38
+ maybe_defn = defns[0]
39
+ else:
40
+ break
41
+ return maybe_defn
42
+
43
+
44
+ @hookimpl
45
+ def pylsp_definitions(
46
+ config: Config, document: Document, position: Dict[str, int]
47
+ ) -> List[Dict[str, Any]]:
48
+ settings = config.plugin_settings("jedi_definition")
49
+ code_position = _utils.position_to_jedi_linecolumn(document, position)
50
+ script = document.jedi_script(use_document_path=True)
51
+ auto_import_modules = jedi.settings.auto_import_modules
52
+
53
+ try:
54
+ jedi.settings.auto_import_modules = []
55
+ definitions = script.goto(
56
+ follow_imports=settings.get("follow_imports", True),
57
+ follow_builtin_imports=settings.get("follow_builtin_imports", True),
58
+ **code_position,
59
+ )
60
+ definitions = [_resolve_definition(d, script, settings) for d in definitions]
61
+ finally:
62
+ jedi.settings.auto_import_modules = auto_import_modules
63
+
64
+ follow_builtin_defns = settings.get("follow_builtin_definitions", True)
65
+ return [
66
+ {
67
+ "uri": uris.uri_with(document.uri, path=str(d.module_path)),
68
+ "range": {
69
+ "start": {"line": d.line - 1, "character": d.column},
70
+ "end": {"line": d.line - 1, "character": d.column + len(d.name)},
71
+ },
72
+ }
73
+ for d in definitions
74
+ if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d))
75
+ ]
76
+
77
+
78
+ def _not_internal_definition(definition: Name) -> bool:
79
+ return (
80
+ definition.line is not None
81
+ and definition.column is not None
82
+ and definition.module_path is not None
83
+ and not definition.in_builtin_module()
84
+ )
@@ -0,0 +1,243 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ """Linter pluging for flake8"""
5
+
6
+ import logging
7
+ import os.path
8
+ import re
9
+ import sys
10
+ from pathlib import PurePath
11
+ from subprocess import PIPE, Popen
12
+
13
+ from flake8.plugins.pyflakes import FLAKE8_PYFLAKES_CODES
14
+
15
+ from pylsp import hookimpl, lsp
16
+ from pylsp.plugins.pyflakes_lint import PYFLAKES_ERROR_MESSAGES
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ FIX_IGNORES_RE = re.compile(r"([^a-zA-Z0-9_,]*;.*(\W+||$))")
21
+ UNNECESSITY_CODES = {
22
+ "F401", # `module` imported but unused
23
+ "F504", # % format unused named arguments
24
+ "F522", # .format(...) unused named arguments
25
+ "F523", # .format(...) unused positional arguments
26
+ "F841", # local variable `name` is assigned to but never used
27
+ }
28
+ # NOTE: If the user sets the flake8 executable with workspace configuration, the
29
+ # error codes in this set may be inaccurate.
30
+ ERROR_CODES = (
31
+ # Errors from the pyflakes plugin of flake8
32
+ {FLAKE8_PYFLAKES_CODES.get(m.__name__, "E999") for m in PYFLAKES_ERROR_MESSAGES}
33
+ # Syntax error from flake8 itself
34
+ | {"E999"}
35
+ )
36
+
37
+
38
+ @hookimpl
39
+ def pylsp_settings():
40
+ # Default flake8 to disabled
41
+ return {"plugins": {"flake8": {"enabled": False}}}
42
+
43
+
44
+ @hookimpl
45
+ def pylsp_lint(workspace, document):
46
+ with workspace.report_progress("lint: flake8"):
47
+ config = workspace._config
48
+ settings = config.plugin_settings("flake8", document_path=document.path)
49
+ log.debug("Got flake8 settings: %s", settings)
50
+
51
+ ignores = settings.get("ignore", [])
52
+ per_file_ignores = settings.get("perFileIgnores")
53
+
54
+ if per_file_ignores:
55
+ prev_file_pat = None
56
+ for path in per_file_ignores:
57
+ try:
58
+ file_pat, errors = path.split(":")
59
+ prev_file_pat = file_pat
60
+ except ValueError:
61
+ # It's legal to just specify another error type for the same
62
+ # file pattern:
63
+ if prev_file_pat is None:
64
+ log.warning("skipping a Per-file-ignore with no file pattern")
65
+ continue
66
+ file_pat = prev_file_pat
67
+ errors = path
68
+ if PurePath(document.path).match(file_pat):
69
+ ignores.extend(errors.split(","))
70
+
71
+ opts = {
72
+ "config": settings.get("config"),
73
+ "exclude": settings.get("exclude"),
74
+ "extend-ignore": settings.get("extendIgnore"),
75
+ "extend-select": settings.get("extendSelect"),
76
+ "filename": settings.get("filename"),
77
+ "hang-closing": settings.get("hangClosing"),
78
+ "ignore": ignores or None,
79
+ "max-complexity": settings.get("maxComplexity"),
80
+ "max-line-length": settings.get("maxLineLength"),
81
+ "indent-size": settings.get("indentSize"),
82
+ "select": settings.get("select"),
83
+ }
84
+
85
+ # flake takes only absolute path to the config. So we should check and
86
+ # convert if necessary
87
+ if opts.get("config") and not os.path.isabs(opts.get("config")):
88
+ opts["config"] = os.path.abspath(
89
+ os.path.expanduser(os.path.expandvars(opts.get("config")))
90
+ )
91
+ log.debug("using flake8 with config: %s", opts["config"])
92
+
93
+ # Call the flake8 utility then parse diagnostics from stdout
94
+ flake8_executable = settings.get("executable", "flake8")
95
+
96
+ args = build_args(opts)
97
+
98
+ # ensure the same source is used for flake8 execution and result parsing;
99
+ # single source access improves performance as it is only one disk access
100
+ source = document.source
101
+ output = run_flake8(flake8_executable, args, document, source)
102
+ return parse_stdout(source, output)
103
+
104
+
105
+ def run_flake8(flake8_executable, args, document, source):
106
+ """Run flake8 with the provided arguments, logs errors
107
+ from stderr if any.
108
+ """
109
+ # a quick temporary fix to deal with Atom
110
+ args = [
111
+ (i if not i.startswith("--ignore=") else FIX_IGNORES_RE.sub("", i))
112
+ for i in args
113
+ if i is not None
114
+ ]
115
+
116
+ if document.path and document.path.startswith(document._workspace.root_path):
117
+ args.extend(
118
+ [
119
+ "--stdin-display-name",
120
+ os.path.relpath(document.path, document._workspace.root_path),
121
+ ]
122
+ )
123
+
124
+ # if executable looks like a path resolve it
125
+ if not os.path.isfile(flake8_executable) and os.sep in flake8_executable:
126
+ flake8_executable = os.path.abspath(
127
+ os.path.expanduser(os.path.expandvars(flake8_executable))
128
+ )
129
+
130
+ log.debug("Calling %s with args: '%s'", flake8_executable, args)
131
+ popen_kwargs = {}
132
+ if cwd := document._workspace.root_path:
133
+ popen_kwargs["cwd"] = cwd
134
+ try:
135
+ cmd = [flake8_executable]
136
+ cmd.extend(args)
137
+ p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs)
138
+ except IOError:
139
+ log.debug(
140
+ "Can't execute %s. Trying with '%s -m flake8'",
141
+ flake8_executable,
142
+ sys.executable,
143
+ )
144
+ cmd = [sys.executable, "-m", "flake8"]
145
+ cmd.extend(args)
146
+ p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs)
147
+ (stdout, stderr) = p.communicate(source.encode())
148
+ if stderr:
149
+ log.error("Error while running flake8 '%s'", stderr.decode())
150
+ return stdout.decode()
151
+
152
+
153
+ def build_args(options):
154
+ """Build arguments for calling flake8.
155
+
156
+ Args:
157
+ options: dictionary of argument names and their values.
158
+ """
159
+ args = ["-"] # use stdin
160
+ for arg_name, arg_val in options.items():
161
+ if arg_val is None:
162
+ continue
163
+ arg = None
164
+ if isinstance(arg_val, list):
165
+ arg = "--{}={}".format(arg_name, ",".join(arg_val))
166
+ elif isinstance(arg_val, bool):
167
+ if arg_val:
168
+ arg = "--{}".format(arg_name)
169
+ else:
170
+ arg = "--{}={}".format(arg_name, arg_val)
171
+ args.append(arg)
172
+ return args
173
+
174
+
175
+ def parse_stdout(source, stdout):
176
+ """
177
+ Build a diagnostics from flake8's output, it should extract every result and format
178
+ it into a dict that looks like this:
179
+ {
180
+ 'source': 'flake8',
181
+ 'code': code, # 'E501'
182
+ 'range': {
183
+ 'start': {
184
+ 'line': start_line,
185
+ 'character': start_column,
186
+ },
187
+ 'end': {
188
+ 'line': end_line,
189
+ 'character': end_column,
190
+ },
191
+ },
192
+ 'message': msg,
193
+ 'severity': lsp.DiagnosticSeverity.*,
194
+ }
195
+
196
+ Args:
197
+ document: The document to be linted.
198
+ stdout: output from flake8
199
+ Returns:
200
+ A list of dictionaries.
201
+ """
202
+
203
+ document_lines = source.splitlines(True)
204
+ diagnostics = []
205
+ lines = stdout.splitlines()
206
+ for raw_line in lines:
207
+ parsed_line = re.match(r"(.*):(\d*):(\d*): (\w*) (.*)", raw_line)
208
+ if not parsed_line:
209
+ log.debug("Flake8 output parser can't parse line '%s'", raw_line)
210
+ continue
211
+
212
+ parsed_line = parsed_line.groups()
213
+ if len(parsed_line) != 5:
214
+ log.debug("Flake8 output parser can't parse line '%s'", raw_line)
215
+ continue
216
+
217
+ _, line, character, code, msg = parsed_line
218
+ line = int(line) - 1
219
+ character = int(character) - 1
220
+ # show also the code in message
221
+ msg = code + " " + msg
222
+ severity = lsp.DiagnosticSeverity.Warning
223
+ if code in ERROR_CODES:
224
+ severity = lsp.DiagnosticSeverity.Error
225
+ diagnostic = {
226
+ "source": "flake8",
227
+ "code": code,
228
+ "range": {
229
+ "start": {"line": line, "character": character},
230
+ "end": {
231
+ "line": line,
232
+ # no way to determine the column
233
+ "character": len(document_lines[line]),
234
+ },
235
+ },
236
+ "message": msg,
237
+ "severity": severity,
238
+ }
239
+ if code in UNNECESSITY_CODES:
240
+ diagnostic["tags"] = [lsp.DiagnosticTag.Unnecessary]
241
+ diagnostics.append(diagnostic)
242
+
243
+ return diagnostics
@@ -0,0 +1,210 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import re
5
+
6
+ import parso
7
+ import parso.python.tree as tree_nodes
8
+
9
+ from pylsp import hookimpl
10
+
11
+ SKIP_NODES = (tree_nodes.Module, tree_nodes.IfStmt, tree_nodes.TryStmt)
12
+ IDENTATION_REGEX = re.compile(r"(\s+).+")
13
+
14
+
15
+ @hookimpl
16
+ def pylsp_folding_range(document):
17
+ program = document.source + "\n"
18
+ lines = program.splitlines()
19
+ tree = parso.parse(program)
20
+ ranges = __compute_folding_ranges(tree, lines)
21
+
22
+ results = []
23
+ for start_line, end_line in ranges:
24
+ start_line -= 1
25
+ end_line -= 1
26
+ # If start/end character is not defined, then it defaults to the
27
+ # corresponding line last character
28
+ results.append(
29
+ {
30
+ "startLine": start_line,
31
+ "endLine": end_line,
32
+ }
33
+ )
34
+ return results
35
+
36
+
37
+ def __merge_folding_ranges(left, right):
38
+ for start in list(left.keys()):
39
+ right_start = right.pop(start, None)
40
+ if right_start is not None:
41
+ left[start] = max(right_start, start)
42
+ left.update(right)
43
+ return left
44
+
45
+
46
+ def __empty_identation_stack(
47
+ identation_stack, level_limits, current_line, folding_ranges
48
+ ):
49
+ while identation_stack != []:
50
+ upper_level = identation_stack.pop(0)
51
+ level_start = level_limits.pop(upper_level)
52
+ folding_ranges.append((level_start, current_line))
53
+ return folding_ranges
54
+
55
+
56
+ def __match_identation_stack(
57
+ identation_stack, level, level_limits, folding_ranges, current_line
58
+ ):
59
+ upper_level = identation_stack.pop(0)
60
+ while upper_level >= level:
61
+ level_start = level_limits.pop(upper_level)
62
+ folding_ranges.append((level_start, current_line))
63
+ upper_level = identation_stack.pop(0)
64
+ identation_stack.insert(0, upper_level)
65
+ return identation_stack, folding_ranges
66
+
67
+
68
+ def __compute_folding_ranges_identation(text):
69
+ lines = text.splitlines()
70
+ folding_ranges = []
71
+ identation_stack = []
72
+ level_limits = {}
73
+ current_level = 0
74
+ current_line = 0
75
+ while lines[current_line] == "":
76
+ current_line += 1
77
+ for i, line in enumerate(lines):
78
+ if i < current_line:
79
+ continue
80
+ i += 1
81
+ identation_match = IDENTATION_REGEX.match(line)
82
+ if identation_match is not None:
83
+ whitespace = identation_match.group(1)
84
+ level = len(whitespace)
85
+ if level > current_level:
86
+ level_limits[current_level] = current_line
87
+ identation_stack.insert(0, current_level)
88
+ current_level = level
89
+ elif level < current_level:
90
+ identation_stack, folding_ranges = __match_identation_stack(
91
+ identation_stack, level, level_limits, folding_ranges, current_line
92
+ )
93
+ current_level = level
94
+ else:
95
+ folding_ranges = __empty_identation_stack(
96
+ identation_stack, level_limits, current_line, folding_ranges
97
+ )
98
+ current_level = 0
99
+ if line.strip() != "":
100
+ current_line = i
101
+ folding_ranges = __empty_identation_stack(
102
+ identation_stack, level_limits, current_line, folding_ranges
103
+ )
104
+ return dict(folding_ranges)
105
+
106
+
107
+ def __check_if_node_is_valid(node):
108
+ valid = True
109
+ if isinstance(node, tree_nodes.PythonNode):
110
+ kind = node.type
111
+ valid = kind not in {
112
+ "decorated",
113
+ "parameters",
114
+ "dictorsetmaker",
115
+ "testlist_comp",
116
+ }
117
+ if kind == "suite":
118
+ if isinstance(node.parent, tree_nodes.Function):
119
+ valid = False
120
+ return valid
121
+
122
+
123
+ def __handle_skip(stack, skip):
124
+ body = stack[skip]
125
+ children = [body]
126
+ if hasattr(body, "children"):
127
+ children = body.children
128
+ stack = stack[:skip] + children + stack[skip + 1 :]
129
+ node = body
130
+ end_line, _ = body.end_pos
131
+ return node, end_line
132
+
133
+
134
+ def __handle_flow_nodes(node, end_line, stack):
135
+ from_keyword = False
136
+ if isinstance(node, tree_nodes.Keyword):
137
+ from_keyword = True
138
+ if node.value in {"if", "elif", "with", "while"}:
139
+ node, end_line = __handle_skip(stack, 2)
140
+ elif node.value in {"except"}:
141
+ first_node = stack[0]
142
+ if isinstance(first_node, tree_nodes.Operator):
143
+ node, end_line = __handle_skip(stack, 1)
144
+ else:
145
+ node, end_line = __handle_skip(stack, 2)
146
+ elif node.value in {"for"}:
147
+ node, end_line = __handle_skip(stack, 4)
148
+ elif node.value in {"else"}:
149
+ node, end_line = __handle_skip(stack, 1)
150
+ return end_line, from_keyword, node, stack
151
+
152
+
153
+ def __compute_start_end_lines(node, stack):
154
+ start_line, _ = node.start_pos
155
+ end_line, _ = node.end_pos
156
+ modified = False
157
+ end_line, from_keyword, node, stack = __handle_flow_nodes(node, end_line, stack)
158
+
159
+ last_leaf = node.get_last_leaf()
160
+ last_newline = isinstance(last_leaf, tree_nodes.Newline)
161
+ last_operator = isinstance(last_leaf, tree_nodes.Operator)
162
+ node_is_operator = isinstance(node, tree_nodes.Operator)
163
+ last_operator = last_operator or not node_is_operator
164
+
165
+ end_line -= 1
166
+
167
+ if isinstance(node.parent, tree_nodes.PythonNode) and not from_keyword:
168
+ kind = node.type
169
+ if kind in {"suite", "atom", "atom_expr", "arglist"}:
170
+ if len(stack) > 0:
171
+ next_node = stack[0]
172
+ next_line, _ = next_node.start_pos
173
+ if next_line > end_line:
174
+ end_line += 1
175
+ modified = True
176
+ if not last_newline and not modified and not last_operator:
177
+ end_line += 1
178
+ return start_line, end_line, stack
179
+
180
+
181
+ def __compute_folding_ranges(tree, lines):
182
+ folding_ranges = {}
183
+ stack = [tree]
184
+
185
+ while len(stack) > 0:
186
+ node = stack.pop(0)
187
+ if isinstance(node, tree_nodes.Newline):
188
+ # Skip newline nodes
189
+ continue
190
+ if isinstance(node, tree_nodes.PythonErrorNode):
191
+ # Fallback to indentation-based (best-effort) folding
192
+ start_line, _ = node.start_pos
193
+ start_line -= 1
194
+ padding = [""] * start_line
195
+ text = "\n".join(padding + lines[start_line:]) + "\n"
196
+ identation_ranges = __compute_folding_ranges_identation(text)
197
+ folding_ranges = __merge_folding_ranges(folding_ranges, identation_ranges)
198
+ break
199
+ if not isinstance(node, SKIP_NODES):
200
+ valid = __check_if_node_is_valid(node)
201
+ if valid:
202
+ start_line, end_line, stack = __compute_start_end_lines(node, stack)
203
+ if end_line > start_line:
204
+ current_end = folding_ranges.get(start_line, -1)
205
+ folding_ranges[start_line] = max(current_end, end_line)
206
+ if hasattr(node, "children"):
207
+ stack = node.children + stack
208
+
209
+ folding_ranges = sorted(folding_ranges.items())
210
+ return folding_ranges
@@ -0,0 +1,36 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+
6
+ from pylsp import _utils, hookimpl, lsp
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ @hookimpl
12
+ def pylsp_document_highlight(document, position):
13
+ code_position = _utils.position_to_jedi_linecolumn(document, position)
14
+ usages = document.jedi_script().get_references(**code_position)
15
+
16
+ def is_valid(definition):
17
+ return definition.line is not None and definition.column is not None
18
+
19
+ def local_to_document(definition):
20
+ return (
21
+ not definition.module_path or str(definition.module_path) == document.path
22
+ )
23
+
24
+ return [
25
+ {
26
+ "range": {
27
+ "start": {"line": d.line - 1, "character": d.column},
28
+ "end": {"line": d.line - 1, "character": d.column + len(d.name)},
29
+ },
30
+ "kind": lsp.DocumentHighlightKind.Write
31
+ if d.is_definition()
32
+ else lsp.DocumentHighlightKind.Read,
33
+ }
34
+ for d in usages
35
+ if is_valid(d) and local_to_document(d)
36
+ ]
pylsp/plugins/hover.py ADDED
@@ -0,0 +1,57 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+
6
+ from pylsp import _utils, hookimpl
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ @hookimpl
12
+ def pylsp_hover(config, document, position):
13
+ code_position = _utils.position_to_jedi_linecolumn(document, position)
14
+ definitions = document.jedi_script(use_document_path=True).infer(**code_position)
15
+ word = document.word_at_position(position)
16
+
17
+ # Find first exact matching definition
18
+ definition = next((x for x in definitions if x.name == word), None)
19
+
20
+ # Ensure a definition is used if only one is available
21
+ # even if the word doesn't match. An example of this case is 'np'
22
+ # where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs
23
+ if len(definitions) == 1:
24
+ definition = definitions[0]
25
+
26
+ if not definition:
27
+ return {"contents": ""}
28
+
29
+ hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {})
30
+ supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"])
31
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
32
+
33
+ doc = _utils.format_docstring(definition.docstring(raw=True), preferred_markup_kind)["value"]
34
+
35
+ # Find first exact matching signature
36
+ signature = next(
37
+ (
38
+ x.to_string()
39
+ for x in definition.get_signatures()
40
+ if (x.name == word and x.type not in ["module"])
41
+ ),
42
+ "",
43
+ )
44
+
45
+ contents = []
46
+ if signature:
47
+ contents.append({
48
+ 'language': 'python',
49
+ 'value': signature,
50
+ })
51
+
52
+ if doc:
53
+ contents.append(doc)
54
+
55
+ return {
56
+ "contents": contents or ''
57
+ }