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.
- deepnote_python_lsp_server-1.13.1rc2.dist-info/METADATA +264 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/RECORD +46 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/WHEEL +5 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/entry_points.txt +25 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/licenses/LICENSE +22 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/top_level.txt +1 -0
- pylsp/__init__.py +33 -0
- pylsp/__main__.py +132 -0
- pylsp/_utils.py +418 -0
- pylsp/_version.py +1 -0
- pylsp/config/__init__.py +2 -0
- pylsp/config/config.py +203 -0
- pylsp/config/flake8_conf.py +64 -0
- pylsp/config/pycodestyle_conf.py +34 -0
- pylsp/config/source.py +88 -0
- pylsp/hookspecs.py +150 -0
- pylsp/lsp.py +115 -0
- pylsp/plugins/__init__.py +2 -0
- pylsp/plugins/_resolvers.py +132 -0
- pylsp/plugins/_rope_task_handle.py +107 -0
- pylsp/plugins/autopep8_format.py +95 -0
- pylsp/plugins/definition.py +84 -0
- pylsp/plugins/flake8_lint.py +243 -0
- pylsp/plugins/folding.py +210 -0
- pylsp/plugins/highlight.py +36 -0
- pylsp/plugins/hover.py +59 -0
- pylsp/plugins/jedi_completion.py +337 -0
- pylsp/plugins/jedi_rename.py +56 -0
- pylsp/plugins/mccabe_lint.py +56 -0
- pylsp/plugins/preload_imports.py +72 -0
- pylsp/plugins/pycodestyle_lint.py +113 -0
- pylsp/plugins/pydocstyle_lint.py +127 -0
- pylsp/plugins/pyflakes_lint.py +97 -0
- pylsp/plugins/pylint_lint.py +367 -0
- pylsp/plugins/references.py +33 -0
- pylsp/plugins/rope_autoimport.py +408 -0
- pylsp/plugins/rope_completion.py +161 -0
- pylsp/plugins/signature.py +81 -0
- pylsp/plugins/symbols.py +219 -0
- pylsp/plugins/type_definition.py +38 -0
- pylsp/plugins/yapf_format.py +199 -0
- pylsp/py.typed +0 -0
- pylsp/python_lsp.py +945 -0
- pylsp/text_edit.py +97 -0
- pylsp/uris.py +139 -0
- pylsp/workspace.py +791 -0
pylsp/_utils.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import docstring_to_markdown
|
|
17
|
+
import jedi
|
|
18
|
+
|
|
19
|
+
JEDI_VERSION = jedi.__version__
|
|
20
|
+
|
|
21
|
+
# Eol chars accepted by the LSP protocol
|
|
22
|
+
# the ordering affects performance
|
|
23
|
+
EOL_CHARS = ["\r\n", "\r", "\n"]
|
|
24
|
+
EOL_REGEX = re.compile(f"({'|'.join(EOL_CHARS)})")
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def debounce(interval_s, keyed_by=None):
|
|
30
|
+
"""Debounce calls to this function until interval_s seconds have passed."""
|
|
31
|
+
|
|
32
|
+
def wrapper(func):
|
|
33
|
+
timers = {}
|
|
34
|
+
lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def debounced(*args, **kwargs):
|
|
38
|
+
sig = inspect.signature(func)
|
|
39
|
+
call_args = sig.bind(*args, **kwargs)
|
|
40
|
+
key = call_args.arguments[keyed_by] if keyed_by else None
|
|
41
|
+
|
|
42
|
+
def run():
|
|
43
|
+
with lock:
|
|
44
|
+
del timers[key]
|
|
45
|
+
return func(*args, **kwargs)
|
|
46
|
+
|
|
47
|
+
with lock:
|
|
48
|
+
old_timer = timers.get(key)
|
|
49
|
+
if old_timer:
|
|
50
|
+
old_timer.cancel()
|
|
51
|
+
|
|
52
|
+
timer = threading.Timer(interval_s, run)
|
|
53
|
+
timers[key] = timer
|
|
54
|
+
timer.start()
|
|
55
|
+
|
|
56
|
+
return debounced
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def throttle(seconds=1):
|
|
62
|
+
"""Throttles calls to a function every `seconds` seconds."""
|
|
63
|
+
|
|
64
|
+
def decorator(func):
|
|
65
|
+
@functools.wraps(func)
|
|
66
|
+
def wrapper(*args, **kwargs):
|
|
67
|
+
if not hasattr(wrapper, "last_call"):
|
|
68
|
+
wrapper.last_call = 0
|
|
69
|
+
if time.time() - wrapper.last_call >= seconds:
|
|
70
|
+
wrapper.last_call = time.time()
|
|
71
|
+
return func(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
return wrapper
|
|
74
|
+
|
|
75
|
+
return decorator
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_parents(root, path, names):
|
|
79
|
+
"""Find files matching the given names relative to the given path.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path (str): The file path to start searching up from.
|
|
83
|
+
names (list[str]): The file/directory names to look for.
|
|
84
|
+
root (str): The directory at which to stop recursing upwards.
|
|
85
|
+
|
|
86
|
+
Note:
|
|
87
|
+
The path MUST be within the root.
|
|
88
|
+
"""
|
|
89
|
+
if not root:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
if not os.path.commonprefix((root, path)):
|
|
93
|
+
log.warning("Path %r not in %r", path, root)
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
# Split the relative by directory, generate all the parent directories, then check each of them.
|
|
97
|
+
# This avoids running a loop that has different base-cases for unix/windows
|
|
98
|
+
# e.g. /a/b and /a/b/c/d/e.py -> ['/a/b', 'c', 'd']
|
|
99
|
+
dirs = [root] + os.path.relpath(os.path.dirname(path), root).split(os.path.sep)
|
|
100
|
+
|
|
101
|
+
# Search each of /a/b/c, /a/b, /a
|
|
102
|
+
while dirs:
|
|
103
|
+
search_dir = os.path.join(*dirs)
|
|
104
|
+
existing = list(
|
|
105
|
+
filter(os.path.exists, [os.path.join(search_dir, n) for n in names])
|
|
106
|
+
)
|
|
107
|
+
if existing:
|
|
108
|
+
return existing
|
|
109
|
+
dirs.pop()
|
|
110
|
+
|
|
111
|
+
# Otherwise nothing
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def path_to_dot_name(path):
|
|
116
|
+
"""Given a path to a module, derive its dot-separated full name."""
|
|
117
|
+
directory = os.path.dirname(path)
|
|
118
|
+
module_name, _ = os.path.splitext(os.path.basename(path))
|
|
119
|
+
full_name = [module_name]
|
|
120
|
+
while os.path.exists(os.path.join(directory, "__init__.py")):
|
|
121
|
+
this_directory = os.path.basename(directory)
|
|
122
|
+
directory = os.path.dirname(directory)
|
|
123
|
+
full_name = [this_directory] + full_name
|
|
124
|
+
return ".".join(full_name)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def match_uri_to_workspace(uri, workspaces):
|
|
128
|
+
if uri is None:
|
|
129
|
+
return None
|
|
130
|
+
max_len, chosen_workspace = -1, None
|
|
131
|
+
path = pathlib.Path(uri).parts
|
|
132
|
+
for workspace in workspaces:
|
|
133
|
+
workspace_parts = pathlib.Path(workspace).parts
|
|
134
|
+
if len(workspace_parts) > len(path):
|
|
135
|
+
continue
|
|
136
|
+
match_len = 0
|
|
137
|
+
for workspace_part, path_part in zip(workspace_parts, path):
|
|
138
|
+
if workspace_part == path_part:
|
|
139
|
+
match_len += 1
|
|
140
|
+
if match_len > 0:
|
|
141
|
+
if match_len > max_len:
|
|
142
|
+
max_len = match_len
|
|
143
|
+
chosen_workspace = workspace
|
|
144
|
+
return chosen_workspace
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def list_to_string(value):
|
|
148
|
+
return ",".join(value) if isinstance(value, list) else value
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def merge_dicts(dict_a, dict_b):
|
|
152
|
+
"""Recursively merge dictionary b into dictionary a.
|
|
153
|
+
|
|
154
|
+
If override_nones is True, then
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def _merge_dicts_(a, b):
|
|
158
|
+
for key in set(a.keys()).union(b.keys()):
|
|
159
|
+
if key in a and key in b:
|
|
160
|
+
if isinstance(a[key], dict) and isinstance(b[key], dict):
|
|
161
|
+
yield (key, dict(_merge_dicts_(a[key], b[key])))
|
|
162
|
+
elif isinstance(a[key], list) and isinstance(b[key], list):
|
|
163
|
+
yield (key, list(set(a[key] + b[key])))
|
|
164
|
+
elif b[key] is not None:
|
|
165
|
+
yield (key, b[key])
|
|
166
|
+
else:
|
|
167
|
+
yield (key, a[key])
|
|
168
|
+
elif key in a:
|
|
169
|
+
yield (key, a[key])
|
|
170
|
+
elif b[key] is not None:
|
|
171
|
+
yield (key, b[key])
|
|
172
|
+
|
|
173
|
+
return dict(_merge_dicts_(dict_a, dict_b))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def escape_plain_text(contents: str) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Format plain text to display nicely in environments which do not respect whitespaces.
|
|
179
|
+
"""
|
|
180
|
+
contents = contents.replace("\t", "\u00a0" * 4)
|
|
181
|
+
contents = contents.replace(" ", "\u00a0" * 2)
|
|
182
|
+
return contents
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def escape_markdown(contents: str) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Format plain text to display nicely in Markdown environment.
|
|
188
|
+
"""
|
|
189
|
+
# escape markdown syntax
|
|
190
|
+
contents = re.sub(r"([\\*_#[\]])", r"\\\1", contents)
|
|
191
|
+
# preserve white space characters
|
|
192
|
+
contents = escape_plain_text(contents)
|
|
193
|
+
return contents
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def wrap_signature(signature):
|
|
197
|
+
return "```python\n" + signature + "\n```\n"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
SERVER_SUPPORTED_MARKUP_KINDS = {"markdown", "plaintext"}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def choose_markup_kind(client_supported_markup_kinds: list[str]):
|
|
204
|
+
"""Choose a markup kind supported by both client and the server.
|
|
205
|
+
|
|
206
|
+
This gives priority to the markup kinds provided earlier on the client preference list.
|
|
207
|
+
"""
|
|
208
|
+
for kind in client_supported_markup_kinds:
|
|
209
|
+
if kind in SERVER_SUPPORTED_MARKUP_KINDS:
|
|
210
|
+
return kind
|
|
211
|
+
return "markdown"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Formatter:
|
|
215
|
+
command: list[str]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def is_installed(self) -> bool:
|
|
219
|
+
"""Returns whether formatter is available"""
|
|
220
|
+
if not hasattr(self, "_is_installed"):
|
|
221
|
+
self._is_installed = self._is_available_via_cli()
|
|
222
|
+
return self._is_installed
|
|
223
|
+
|
|
224
|
+
def format(self, code: str, line_length: int) -> str:
|
|
225
|
+
"""Formats code"""
|
|
226
|
+
return subprocess.check_output(
|
|
227
|
+
[
|
|
228
|
+
sys.executable,
|
|
229
|
+
"-m",
|
|
230
|
+
*self.command,
|
|
231
|
+
"--line-length",
|
|
232
|
+
str(line_length),
|
|
233
|
+
"-",
|
|
234
|
+
],
|
|
235
|
+
input=code,
|
|
236
|
+
text=True,
|
|
237
|
+
).strip()
|
|
238
|
+
|
|
239
|
+
def _is_available_via_cli(self) -> bool:
|
|
240
|
+
try:
|
|
241
|
+
subprocess.check_output(
|
|
242
|
+
[
|
|
243
|
+
sys.executable,
|
|
244
|
+
"-m",
|
|
245
|
+
*self.command,
|
|
246
|
+
"--help",
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
return True
|
|
250
|
+
except subprocess.CalledProcessError:
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class RuffFormatter(Formatter):
|
|
255
|
+
command = ["ruff", "format"]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class BlackFormatter(Formatter):
|
|
259
|
+
command = ["black"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
formatters = {"ruff": RuffFormatter(), "black": BlackFormatter()}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def format_signature(signature: str, config: dict, signature_formatter: str) -> str:
|
|
266
|
+
"""Formats signature using ruff or black if either is available."""
|
|
267
|
+
as_func = f"def {signature.strip()}:\n pass"
|
|
268
|
+
line_length = config.get("line_length", 88)
|
|
269
|
+
formatter = formatters[signature_formatter]
|
|
270
|
+
if formatter.is_installed:
|
|
271
|
+
try:
|
|
272
|
+
return (
|
|
273
|
+
formatter.format(as_func, line_length=line_length)
|
|
274
|
+
.removeprefix("def ")
|
|
275
|
+
.removesuffix(":\n pass")
|
|
276
|
+
)
|
|
277
|
+
except subprocess.CalledProcessError as e:
|
|
278
|
+
log.warning("Signature formatter failed %s", e)
|
|
279
|
+
else:
|
|
280
|
+
log.warning(
|
|
281
|
+
"Formatter %s was requested but it does not appear to be installed",
|
|
282
|
+
signature_formatter,
|
|
283
|
+
)
|
|
284
|
+
return signature
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def convert_signatures_to_markdown(signatures: list[str], config: dict) -> str:
|
|
288
|
+
signature_formatter = config.get("formatter", "black")
|
|
289
|
+
if signature_formatter:
|
|
290
|
+
signatures = [
|
|
291
|
+
format_signature(
|
|
292
|
+
signature, signature_formatter=signature_formatter, config=config
|
|
293
|
+
)
|
|
294
|
+
for signature in signatures
|
|
295
|
+
]
|
|
296
|
+
return wrap_signature("\n".join(signatures))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def format_docstring(
|
|
300
|
+
contents: str,
|
|
301
|
+
markup_kind: str,
|
|
302
|
+
signatures: Optional[list[str]] = None,
|
|
303
|
+
signature_config: Optional[dict] = None,
|
|
304
|
+
):
|
|
305
|
+
"""Transform the provided docstring into a MarkupContent object.
|
|
306
|
+
|
|
307
|
+
If `markup_kind` is 'markdown' the docstring will get converted to
|
|
308
|
+
markdown representation using `docstring-to-markdown`; if it is
|
|
309
|
+
`plaintext`, it will be returned as plain text.
|
|
310
|
+
Call signatures of functions (or equivalent code summaries)
|
|
311
|
+
provided in optional `signatures` argument will be prepended
|
|
312
|
+
to the provided contents of the docstring if given.
|
|
313
|
+
"""
|
|
314
|
+
if not isinstance(contents, str):
|
|
315
|
+
contents = ""
|
|
316
|
+
|
|
317
|
+
if markup_kind == "markdown":
|
|
318
|
+
try:
|
|
319
|
+
value = docstring_to_markdown.convert(contents)
|
|
320
|
+
except docstring_to_markdown.UnknownFormatError:
|
|
321
|
+
# try to escape the Markdown syntax instead:
|
|
322
|
+
value = escape_markdown(contents)
|
|
323
|
+
|
|
324
|
+
if signatures:
|
|
325
|
+
wrapped_signatures = convert_signatures_to_markdown(
|
|
326
|
+
signatures, config=signature_config or {}
|
|
327
|
+
)
|
|
328
|
+
value = wrapped_signatures + "\n\n" + value
|
|
329
|
+
|
|
330
|
+
return {"kind": "markdown", "value": value}
|
|
331
|
+
value = contents
|
|
332
|
+
if signatures:
|
|
333
|
+
value = "\n".join(signatures) + "\n\n" + value
|
|
334
|
+
return {"kind": "plaintext", "value": escape_plain_text(value)}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def clip_column(column, lines, line_number):
|
|
338
|
+
"""
|
|
339
|
+
Normalise the position as per the LSP that accepts character positions > line length
|
|
340
|
+
|
|
341
|
+
https://microsoft.github.io/language-server-protocol/specification#position
|
|
342
|
+
"""
|
|
343
|
+
max_column = (
|
|
344
|
+
len(lines[line_number].rstrip("\r\n")) if len(lines) > line_number else 0
|
|
345
|
+
)
|
|
346
|
+
return min(column, max_column)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def position_to_jedi_linecolumn(document, position):
|
|
350
|
+
"""
|
|
351
|
+
Convert the LSP format 'line', 'character' to Jedi's 'line', 'column'
|
|
352
|
+
|
|
353
|
+
https://microsoft.github.io/language-server-protocol/specification#position
|
|
354
|
+
"""
|
|
355
|
+
code_position = {}
|
|
356
|
+
if position:
|
|
357
|
+
code_position = {
|
|
358
|
+
"line": position["line"] + 1,
|
|
359
|
+
"column": clip_column(
|
|
360
|
+
position["character"], document.lines, position["line"]
|
|
361
|
+
),
|
|
362
|
+
}
|
|
363
|
+
return code_position
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if os.name == "nt":
|
|
367
|
+
import ctypes
|
|
368
|
+
|
|
369
|
+
kernel32 = ctypes.windll.kernel32
|
|
370
|
+
PROCESS_QUERY_INFROMATION = 0x1000
|
|
371
|
+
|
|
372
|
+
def is_process_alive(pid):
|
|
373
|
+
"""Check whether the process with the given pid is still alive.
|
|
374
|
+
|
|
375
|
+
Running `os.kill()` on Windows always exits the process, so it can't be used to check for an alive process.
|
|
376
|
+
see: https://docs.python.org/3/library/os.html?highlight=os%20kill#os.kill
|
|
377
|
+
|
|
378
|
+
Hence ctypes is used to check for the process directly via windows API avoiding any other 3rd-party dependency.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
pid (int): process ID
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
bool: False if the process is not alive or don't have permission to check, True otherwise.
|
|
385
|
+
"""
|
|
386
|
+
process = kernel32.OpenProcess(PROCESS_QUERY_INFROMATION, 0, pid)
|
|
387
|
+
if process != 0:
|
|
388
|
+
kernel32.CloseHandle(process)
|
|
389
|
+
return True
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
else:
|
|
393
|
+
import errno
|
|
394
|
+
|
|
395
|
+
def is_process_alive(pid):
|
|
396
|
+
"""Check whether the process with the given pid is still alive.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
pid (int): process ID
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
bool: False if the process is not alive or don't have permission to check, True otherwise.
|
|
403
|
+
"""
|
|
404
|
+
if pid < 0:
|
|
405
|
+
return False
|
|
406
|
+
try:
|
|
407
|
+
os.kill(pid, 0)
|
|
408
|
+
except OSError as e:
|
|
409
|
+
return e.errno == errno.EPERM
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def get_eol_chars(text):
|
|
414
|
+
"""Get EOL chars used in text."""
|
|
415
|
+
match = EOL_REGEX.search(text)
|
|
416
|
+
if match:
|
|
417
|
+
return match.group(0)
|
|
418
|
+
return None
|
pylsp/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.13.1rc2"
|
pylsp/config/__init__.py
ADDED
pylsp/config/config.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
import pluggy
|
|
11
|
+
from pluggy._hooks import HookImpl
|
|
12
|
+
|
|
13
|
+
from pylsp import PYLSP, _utils, hookspecs, uris
|
|
14
|
+
|
|
15
|
+
# See compatibility note on `group` keyword:
|
|
16
|
+
# https://docs.python.org/3/library/importlib.metadata.html#entry-points
|
|
17
|
+
if sys.version_info < (3, 10): # pragma: no cover
|
|
18
|
+
from importlib_metadata import entry_points
|
|
19
|
+
else: # pragma: no cover
|
|
20
|
+
from importlib.metadata import entry_points
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Sources of config, first source overrides next source
|
|
26
|
+
DEFAULT_CONFIG_SOURCES = ["pycodestyle"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginManager(pluggy.PluginManager):
|
|
30
|
+
def _hookexec(
|
|
31
|
+
self,
|
|
32
|
+
hook_name: str,
|
|
33
|
+
methods: Sequence[HookImpl],
|
|
34
|
+
kwargs: Mapping[str, object],
|
|
35
|
+
firstresult: bool,
|
|
36
|
+
) -> Union[object, list[object]]:
|
|
37
|
+
# called from all hookcaller instances.
|
|
38
|
+
# enable_tracing will set its own wrapping function at self._inner_hookexec
|
|
39
|
+
try:
|
|
40
|
+
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
log.warning(f"Failed to load hook {hook_name}: {e}", exc_info=True)
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
def __init__(self, root_uri, init_opts, process_id, capabilities) -> None:
|
|
48
|
+
self._root_path = uris.to_fs_path(root_uri)
|
|
49
|
+
self._root_uri = root_uri
|
|
50
|
+
self._init_opts = init_opts
|
|
51
|
+
self._process_id = process_id
|
|
52
|
+
self._capabilities = capabilities
|
|
53
|
+
|
|
54
|
+
self._settings = {}
|
|
55
|
+
self._plugin_settings = {}
|
|
56
|
+
|
|
57
|
+
self._config_sources = {}
|
|
58
|
+
try:
|
|
59
|
+
from .flake8_conf import Flake8Config
|
|
60
|
+
|
|
61
|
+
self._config_sources["flake8"] = Flake8Config(self._root_path)
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
try:
|
|
65
|
+
from .pycodestyle_conf import PyCodeStyleConfig
|
|
66
|
+
|
|
67
|
+
self._config_sources["pycodestyle"] = PyCodeStyleConfig(self._root_path)
|
|
68
|
+
except ImportError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
self._pm = PluginManager(PYLSP)
|
|
72
|
+
self._pm.trace.root.setwriter(log.debug)
|
|
73
|
+
self._pm.enable_tracing()
|
|
74
|
+
self._pm.add_hookspecs(hookspecs)
|
|
75
|
+
|
|
76
|
+
# Pluggy will skip loading a plugin if it throws a DistributionNotFound exception.
|
|
77
|
+
# However I don't want all plugins to have to catch ImportError and re-throw. So here we'll filter
|
|
78
|
+
# out any entry points that throw ImportError assuming one or more of their dependencies isn't present.
|
|
79
|
+
for entry_point in entry_points(group=PYLSP):
|
|
80
|
+
try:
|
|
81
|
+
entry_point.load()
|
|
82
|
+
except Exception as e:
|
|
83
|
+
log.info(
|
|
84
|
+
"Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e
|
|
85
|
+
)
|
|
86
|
+
self._pm.set_blocked(entry_point.name)
|
|
87
|
+
|
|
88
|
+
# Load the entry points into pluggy, having blocked any failing ones.
|
|
89
|
+
# Despite the API name, recent Pluggy versions will use ``importlib_metadata``.
|
|
90
|
+
self._pm.load_setuptools_entrypoints(PYLSP)
|
|
91
|
+
|
|
92
|
+
for name, plugin in self._pm.list_name_plugin():
|
|
93
|
+
if plugin is not None:
|
|
94
|
+
log.info("Loaded pylsp plugin %s from %s", name, plugin)
|
|
95
|
+
|
|
96
|
+
for plugin_conf in self._pm.hook.pylsp_settings(config=self):
|
|
97
|
+
self._plugin_settings = _utils.merge_dicts(
|
|
98
|
+
self._plugin_settings, plugin_conf
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self._plugin_settings = _utils.merge_dicts(
|
|
102
|
+
self._plugin_settings, self._init_opts.get("pylsp", {})
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self._update_disabled_plugins()
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def disabled_plugins(self):
|
|
109
|
+
return self._disabled_plugins
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def plugin_manager(self):
|
|
113
|
+
return self._pm
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def init_opts(self):
|
|
117
|
+
return self._init_opts
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def root_uri(self):
|
|
121
|
+
return self._root_uri
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def process_id(self):
|
|
125
|
+
return self._process_id
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def capabilities(self):
|
|
129
|
+
return self._capabilities
|
|
130
|
+
|
|
131
|
+
@lru_cache(maxsize=32)
|
|
132
|
+
def settings(self, document_path=None):
|
|
133
|
+
"""Settings are constructed from a few sources:
|
|
134
|
+
|
|
135
|
+
1. User settings, found in user's home directory
|
|
136
|
+
2. Plugin settings, reported by PyLS plugins
|
|
137
|
+
3. LSP settings, given to us from didChangeConfiguration
|
|
138
|
+
4. Project settings, found in config files in the current project.
|
|
139
|
+
|
|
140
|
+
Since this function is nondeterministic, it is important to call
|
|
141
|
+
settings.cache_clear() when the config is updated
|
|
142
|
+
"""
|
|
143
|
+
settings = {}
|
|
144
|
+
sources = self._settings.get("configurationSources", DEFAULT_CONFIG_SOURCES)
|
|
145
|
+
|
|
146
|
+
# Plugin configuration
|
|
147
|
+
settings = _utils.merge_dicts(settings, self._plugin_settings)
|
|
148
|
+
|
|
149
|
+
# LSP configuration
|
|
150
|
+
settings = _utils.merge_dicts(settings, self._settings)
|
|
151
|
+
|
|
152
|
+
# User configuration
|
|
153
|
+
for source_name in reversed(sources):
|
|
154
|
+
source = self._config_sources.get(source_name)
|
|
155
|
+
if not source:
|
|
156
|
+
continue
|
|
157
|
+
source_conf = source.user_config()
|
|
158
|
+
log.debug(
|
|
159
|
+
"Got user config from %s: %s", source.__class__.__name__, source_conf
|
|
160
|
+
)
|
|
161
|
+
settings = _utils.merge_dicts(settings, source_conf)
|
|
162
|
+
|
|
163
|
+
# Project configuration
|
|
164
|
+
for source_name in reversed(sources):
|
|
165
|
+
source = self._config_sources.get(source_name)
|
|
166
|
+
if not source:
|
|
167
|
+
continue
|
|
168
|
+
source_conf = source.project_config(document_path or self._root_path)
|
|
169
|
+
log.debug(
|
|
170
|
+
"Got project config from %s: %s", source.__class__.__name__, source_conf
|
|
171
|
+
)
|
|
172
|
+
settings = _utils.merge_dicts(settings, source_conf)
|
|
173
|
+
|
|
174
|
+
log.debug("With configuration: %s", settings)
|
|
175
|
+
|
|
176
|
+
return settings
|
|
177
|
+
|
|
178
|
+
def find_parents(self, path, names):
|
|
179
|
+
root_path = uris.to_fs_path(self._root_uri)
|
|
180
|
+
return _utils.find_parents(root_path, path, names)
|
|
181
|
+
|
|
182
|
+
def plugin_settings(self, plugin, document_path=None):
|
|
183
|
+
return (
|
|
184
|
+
self.settings(document_path=document_path)
|
|
185
|
+
.get("plugins", {})
|
|
186
|
+
.get(plugin, {})
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def update(self, settings) -> None:
|
|
190
|
+
"""Recursively merge the given settings into the current settings."""
|
|
191
|
+
self.settings.cache_clear()
|
|
192
|
+
self._settings = settings
|
|
193
|
+
log.info("Updated settings to %s", self._settings)
|
|
194
|
+
self._update_disabled_plugins()
|
|
195
|
+
|
|
196
|
+
def _update_disabled_plugins(self) -> None:
|
|
197
|
+
# All plugins default to enabled
|
|
198
|
+
self._disabled_plugins = [
|
|
199
|
+
plugin
|
|
200
|
+
for name, plugin in self.plugin_manager.list_name_plugin()
|
|
201
|
+
if not self.settings().get("plugins", {}).get(name, {}).get("enabled", True)
|
|
202
|
+
]
|
|
203
|
+
log.info("Disabled plugins: %s", self._disabled_plugins)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from pylsp._utils import find_parents
|
|
8
|
+
|
|
9
|
+
from .source import ConfigSource
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
CONFIG_KEY = "flake8"
|
|
14
|
+
PROJECT_CONFIGS = [".flake8", "setup.cfg", "tox.ini"]
|
|
15
|
+
|
|
16
|
+
OPTIONS = [
|
|
17
|
+
# mccabe
|
|
18
|
+
("max-complexity", "plugins.mccabe.threshold", int),
|
|
19
|
+
# pycodestyle
|
|
20
|
+
("exclude", "plugins.pycodestyle.exclude", list),
|
|
21
|
+
("filename", "plugins.pycodestyle.filename", list),
|
|
22
|
+
("hang-closing", "plugins.pycodestyle.hangClosing", bool),
|
|
23
|
+
("ignore", "plugins.pycodestyle.ignore", list),
|
|
24
|
+
("max-line-length", "plugins.pycodestyle.maxLineLength", int),
|
|
25
|
+
("indent-size", "plugins.pycodestyle.indentSize", int),
|
|
26
|
+
("select", "plugins.pycodestyle.select", list),
|
|
27
|
+
# flake8
|
|
28
|
+
("exclude", "plugins.flake8.exclude", list),
|
|
29
|
+
("extend-ignore", "plugins.flake8.extendIgnore", list),
|
|
30
|
+
("extend-select", "plugins.flake8.extendSelect", list),
|
|
31
|
+
("filename", "plugins.flake8.filename", list),
|
|
32
|
+
("hang-closing", "plugins.flake8.hangClosing", bool),
|
|
33
|
+
("ignore", "plugins.flake8.ignore", list),
|
|
34
|
+
("max-complexity", "plugins.flake8.maxComplexity", int),
|
|
35
|
+
("max-line-length", "plugins.flake8.maxLineLength", int),
|
|
36
|
+
("indent-size", "plugins.flake8.indentSize", int),
|
|
37
|
+
("select", "plugins.flake8.select", list),
|
|
38
|
+
("per-file-ignores", "plugins.flake8.perFileIgnores", list),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Flake8Config(ConfigSource):
|
|
43
|
+
"""Parse flake8 configurations."""
|
|
44
|
+
|
|
45
|
+
def user_config(self):
|
|
46
|
+
config_file = self._user_config_file()
|
|
47
|
+
config = self.read_config_from_files([config_file])
|
|
48
|
+
return self.parse_config(config, CONFIG_KEY, OPTIONS)
|
|
49
|
+
|
|
50
|
+
def _user_config_file(self):
|
|
51
|
+
if self.is_windows:
|
|
52
|
+
return os.path.expanduser("~\\.flake8")
|
|
53
|
+
return os.path.join(self.xdg_home, "flake8")
|
|
54
|
+
|
|
55
|
+
def project_config(self, document_path):
|
|
56
|
+
files = find_parents(self.root_path, document_path, PROJECT_CONFIGS)
|
|
57
|
+
config = self.read_config_from_files(files)
|
|
58
|
+
return self.parse_config(config, CONFIG_KEY, OPTIONS)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def _parse_list_opt(cls, string):
|
|
62
|
+
if string.startswith("\n"):
|
|
63
|
+
return [s.strip().rstrip(",") for s in string.split("\n") if s.strip()]
|
|
64
|
+
return [s.strip() for s in string.split(",") if s.strip()]
|