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
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"
@@ -0,0 +1,2 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
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()]