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/plugins/hover.py ADDED
@@ -0,0 +1,59 @@
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(
34
+ definition.docstring(raw=True), preferred_markup_kind
35
+ )["value"]
36
+
37
+ # Find first exact matching signature
38
+ signature = next(
39
+ (
40
+ x.to_string()
41
+ for x in definition.get_signatures()
42
+ if (x.name == word and x.type not in ["module"])
43
+ ),
44
+ "",
45
+ )
46
+
47
+ contents = []
48
+ if signature:
49
+ contents.append(
50
+ {
51
+ "language": "python",
52
+ "value": signature,
53
+ }
54
+ )
55
+
56
+ if doc:
57
+ contents.append(doc)
58
+
59
+ return {"contents": contents or ""}
@@ -0,0 +1,337 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+ import os
6
+
7
+ import parso
8
+
9
+ from pylsp import _utils, hookimpl, lsp
10
+ from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ # Map to the LSP type
15
+ # > Valid values for type are ``module``, `` class ``, ``instance``, ``function``,
16
+ # > ``param``, ``path``, ``keyword``, ``property`` and ``statement``.
17
+ # see: https://jedi.readthedocs.io/en/latest/docs/api-classes.html#jedi.api.classes.BaseName.type
18
+ _TYPE_MAP = {
19
+ "module": lsp.CompletionItemKind.Module,
20
+ "namespace": lsp.CompletionItemKind.Module, # to be added in Jedi 0.18+
21
+ "class": lsp.CompletionItemKind.Class,
22
+ "instance": lsp.CompletionItemKind.Reference,
23
+ "function": lsp.CompletionItemKind.Function,
24
+ "param": lsp.CompletionItemKind.Variable,
25
+ "path": lsp.CompletionItemKind.File,
26
+ "keyword": lsp.CompletionItemKind.Keyword,
27
+ "property": lsp.CompletionItemKind.Property, # added in Jedi 0.18
28
+ "statement": lsp.CompletionItemKind.Variable,
29
+ }
30
+
31
+ COMPLETION_CACHE = {}
32
+
33
+ # Types of parso nodes for which snippet is not included in the completion
34
+ _IMPORTS = ("import_name", "import_from")
35
+
36
+ # Types of parso node for errors
37
+ _ERRORS = ("error_node",)
38
+
39
+
40
+ @hookimpl
41
+ def pylsp_completions(config, document, position):
42
+ """Get formatted completions for current code position"""
43
+ settings = config.plugin_settings("jedi_completion", document_path=document.path)
44
+ resolve_eagerly = settings.get("eager", False)
45
+ signature_config = config.settings().get("signature", {})
46
+
47
+ code_position = _utils.position_to_jedi_linecolumn(document, position)
48
+ code_position["fuzzy"] = settings.get("fuzzy", False)
49
+ completions = document.jedi_script(use_document_path=True).complete(**code_position)
50
+
51
+ if not completions:
52
+ return None
53
+
54
+ completion_capabilities = config.capabilities.get("textDocument", {}).get(
55
+ "completion", {}
56
+ )
57
+ item_capabilities = completion_capabilities.get("completionItem", {})
58
+ snippet_support = item_capabilities.get("snippetSupport")
59
+ supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
60
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
61
+
62
+ should_include_params = settings.get("include_params")
63
+ should_include_class_objects = settings.get("include_class_objects", False)
64
+ should_include_function_objects = settings.get("include_function_objects", False)
65
+
66
+ max_to_resolve = settings.get("resolve_at_most", 25)
67
+ modules_to_cache_for = settings.get("cache_for", None)
68
+ if modules_to_cache_for is not None:
69
+ LABEL_RESOLVER.cached_modules = modules_to_cache_for
70
+ SNIPPET_RESOLVER.cached_modules = modules_to_cache_for
71
+
72
+ include_params = (
73
+ snippet_support and should_include_params and use_snippets(document, position)
74
+ )
75
+ include_class_objects = (
76
+ snippet_support
77
+ and should_include_class_objects
78
+ and use_snippets(document, position)
79
+ )
80
+ include_function_objects = (
81
+ snippet_support
82
+ and should_include_function_objects
83
+ and use_snippets(document, position)
84
+ )
85
+
86
+ ready_completions = [
87
+ _format_completion(
88
+ c,
89
+ markup_kind=preferred_markup_kind,
90
+ include_params=include_params if c.type in ["class", "function"] else False,
91
+ resolve=resolve_eagerly,
92
+ resolve_label_or_snippet=(i < max_to_resolve),
93
+ snippet_support=snippet_support,
94
+ signature_config=signature_config,
95
+ )
96
+ for i, c in enumerate(completions)
97
+ ]
98
+
99
+ # TODO split up once other improvements are merged
100
+ if include_class_objects:
101
+ for i, c in enumerate(completions):
102
+ if c.type == "class":
103
+ completion_dict = _format_completion(
104
+ c,
105
+ markup_kind=preferred_markup_kind,
106
+ include_params=False,
107
+ resolve=resolve_eagerly,
108
+ resolve_label_or_snippet=(i < max_to_resolve),
109
+ snippet_support=snippet_support,
110
+ signature_config=signature_config,
111
+ )
112
+ completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
113
+ completion_dict["label"] += " object"
114
+ ready_completions.append(completion_dict)
115
+
116
+ if include_function_objects:
117
+ for i, c in enumerate(completions):
118
+ if c.type == "function":
119
+ completion_dict = _format_completion(
120
+ c,
121
+ markup_kind=preferred_markup_kind,
122
+ include_params=False,
123
+ resolve=resolve_eagerly,
124
+ resolve_label_or_snippet=(i < max_to_resolve),
125
+ snippet_support=snippet_support,
126
+ signature_config=signature_config,
127
+ )
128
+ completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
129
+ completion_dict["label"] += " object"
130
+ ready_completions.append(completion_dict)
131
+
132
+ for completion_dict in ready_completions:
133
+ completion_dict["data"] = {"doc_uri": document.uri}
134
+
135
+ # most recently retrieved completion items, used for resolution
136
+ document.shared_data["LAST_JEDI_COMPLETIONS"] = {
137
+ # label is the only required property; here it is assumed to be unique
138
+ completion["label"]: (completion, data)
139
+ for completion, data in zip(ready_completions, completions)
140
+ }
141
+
142
+ return ready_completions or None
143
+
144
+
145
+ @hookimpl
146
+ def pylsp_completion_detail(config, item):
147
+ d = COMPLETION_CACHE.get(item)
148
+ if d:
149
+ completion = {
150
+ "label": "", # _label(d),
151
+ "kind": _TYPE_MAP.get(d.type),
152
+ "detail": "", # _detail(d),
153
+ "documentation": _utils.format_docstring(d.docstring()),
154
+ "sortText": "", # _sort_text(d),
155
+ "insertText": d.name,
156
+ }
157
+ return completion
158
+ else:
159
+ log.info("Completion missing")
160
+ return None
161
+
162
+
163
+ @hookimpl
164
+ def pylsp_completion_item_resolve(
165
+ config,
166
+ completion_item,
167
+ document,
168
+ ):
169
+ """Resolve formatted completion for given non-resolved completion"""
170
+ shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get(
171
+ completion_item["label"]
172
+ )
173
+
174
+ completion_capabilities = config.capabilities.get("textDocument", {}).get(
175
+ "completion", {}
176
+ )
177
+ item_capabilities = completion_capabilities.get("completionItem", {})
178
+ supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
179
+ preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
180
+
181
+ if shared_data:
182
+ completion, data = shared_data
183
+ return _resolve_completion(
184
+ completion,
185
+ data,
186
+ markup_kind=preferred_markup_kind,
187
+ signature_config=config.settings().get("signature", {}),
188
+ )
189
+ return completion_item
190
+
191
+
192
+ def is_exception_class(name):
193
+ """
194
+ Determine if a class name is an instance of an Exception.
195
+
196
+ This returns `False` if the name given corresponds with a instance of
197
+ the 'Exception' class, `True` otherwise
198
+ """
199
+ try:
200
+ return name in [cls.__name__ for cls in Exception.__subclasses__()]
201
+ except AttributeError:
202
+ # Needed in case a class don't uses new-style
203
+ # class definition in Python 2
204
+ return False
205
+
206
+
207
+ def use_snippets(document, position):
208
+ """
209
+ Determine if it's necessary to return snippets in code completions.
210
+
211
+ This returns `False` if a completion is being requested on an import
212
+ statement, `True` otherwise.
213
+ """
214
+ line = position["line"]
215
+ lines = document.source.split("\n", line)
216
+ act_lines = [lines[line][: position["character"]]]
217
+ line -= 1
218
+ last_character = ""
219
+ while line > -1:
220
+ act_line = lines[line]
221
+ if (
222
+ act_line.rstrip().endswith("\\")
223
+ or act_line.rstrip().endswith("(")
224
+ or act_line.rstrip().endswith(",")
225
+ ):
226
+ act_lines.insert(0, act_line)
227
+ line -= 1
228
+ if act_line.rstrip().endswith("("):
229
+ # Needs to be added to the end of the code before parsing
230
+ # to make it valid, otherwise the node type could end
231
+ # being an 'error_node' for multi-line imports that use '('
232
+ last_character = ")"
233
+ else:
234
+ break
235
+ if "(" in act_lines[-1].strip():
236
+ last_character = ")"
237
+ code = "\n".join(act_lines).rsplit(";", maxsplit=1)[-1].strip() + last_character
238
+ tokens = parso.parse(code)
239
+ expr_type = tokens.children[0].type
240
+ return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code)
241
+
242
+
243
+ def _resolve_completion(completion, d, markup_kind: str, signature_config: dict):
244
+ completion["detail"] = _detail(d)
245
+ try:
246
+ docs = _utils.format_docstring(
247
+ d.docstring(raw=True),
248
+ signatures=[signature.to_string() for signature in d.get_signatures()],
249
+ markup_kind=markup_kind,
250
+ signature_config=signature_config,
251
+ )
252
+ except Exception:
253
+ docs = ""
254
+ completion["documentation"] = docs
255
+ return completion
256
+
257
+
258
+ def _format_completion(
259
+ d,
260
+ markup_kind: str,
261
+ include_params=True,
262
+ resolve=False,
263
+ resolve_label_or_snippet=False,
264
+ snippet_support=False,
265
+ signature_config=None,
266
+ ):
267
+ COMPLETION_CACHE[d.name] = d
268
+ completion = {
269
+ "label": _label(d, resolve_label_or_snippet),
270
+ "kind": _TYPE_MAP.get(d.type),
271
+ "sortText": _sort_text(d),
272
+ "insertText": d.name,
273
+ }
274
+
275
+ if resolve:
276
+ completion = _resolve_completion(
277
+ completion, d, markup_kind, signature_config=signature_config
278
+ )
279
+
280
+ # Adjustments for file completions
281
+ if d.type == "path":
282
+ path = os.path.normpath(d.name)
283
+
284
+ # If the completion ends with os.sep, it means it's a directory. So we add os.sep at the end
285
+ # to ease additional file completions.
286
+ if d.name.endswith(os.sep):
287
+ if os.name == "nt":
288
+ path = path + "\\"
289
+ else:
290
+ path = path + "/"
291
+
292
+ # Escape to prevent conflicts with the code snippets grammer
293
+ # See also https://github.com/python-lsp/python-lsp-server/issues/373
294
+ if snippet_support:
295
+ path = path.replace("\\", "\\\\")
296
+ path = path.replace("/", "\\/")
297
+
298
+ completion["insertText"] = path
299
+
300
+ if include_params and not is_exception_class(d.name):
301
+ snippet = _snippet(d, resolve_label_or_snippet)
302
+ completion.update(snippet)
303
+
304
+ return completion
305
+
306
+
307
+ def _label(definition, resolve=False):
308
+ if not resolve:
309
+ return definition.name
310
+ sig = LABEL_RESOLVER.get_or_create(definition)
311
+ if sig:
312
+ return sig
313
+ return definition.name
314
+
315
+
316
+ def _snippet(definition, resolve=False):
317
+ if not resolve:
318
+ return {}
319
+ snippet = SNIPPET_RESOLVER.get_or_create(definition)
320
+ return snippet
321
+
322
+
323
+ def _detail(definition):
324
+ try:
325
+ return definition.parent().full_name or ""
326
+ except AttributeError:
327
+ return definition.full_name or ""
328
+
329
+
330
+ def _sort_text(definition):
331
+ """Ensure builtins appear at the bottom.
332
+ Description is of format <type>: <module>.<item>
333
+ """
334
+
335
+ # If its 'hidden', put it next last
336
+ prefix = "z{}" if definition.name.startswith("_") else "a{}"
337
+ return prefix.format(definition.name)
@@ -0,0 +1,56 @@
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, uris
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ @hookimpl
12
+ def pylsp_rename(config, workspace, document, position, new_name):
13
+ log.debug(
14
+ "Executing rename of %s to %s", document.word_at_position(position), new_name
15
+ )
16
+ kwargs = _utils.position_to_jedi_linecolumn(document, position)
17
+ kwargs["new_name"] = new_name
18
+ try:
19
+ refactoring = document.jedi_script().rename(**kwargs)
20
+ except NotImplementedError as exc:
21
+ raise Exception(
22
+ "No support for renaming in Python 2/3.5 with Jedi. "
23
+ "Consider using the pylsp-rope plugin instead"
24
+ ) from exc
25
+ log.debug("Finished rename: %s", refactoring.get_diff())
26
+ changes = []
27
+
28
+ changed_files = refactoring.get_changed_files()
29
+ for file_path, changed_file in changed_files.items():
30
+ uri = uris.from_fs_path(str(file_path))
31
+ doc = workspace.get_maybe_document(uri)
32
+ changes.append(
33
+ {
34
+ "textDocument": {"uri": uri, "version": doc.version if doc else None},
35
+ "edits": [
36
+ {
37
+ "range": {
38
+ "start": {"line": 0, "character": 0},
39
+ "end": {
40
+ "line": _num_lines(changed_file.get_new_code()),
41
+ "character": 0,
42
+ },
43
+ },
44
+ "newText": changed_file.get_new_code(),
45
+ }
46
+ ],
47
+ }
48
+ )
49
+ return {"documentChanges": changes}
50
+
51
+
52
+ def _num_lines(file_contents):
53
+ "Count the number of lines in the given string."
54
+ if _utils.get_eol_chars(file_contents):
55
+ return len(file_contents.splitlines())
56
+ return 0
@@ -0,0 +1,56 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import ast
5
+ import logging
6
+
7
+ import mccabe
8
+
9
+ from pylsp import hookimpl, lsp
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+ THRESHOLD = "threshold"
14
+ DEFAULT_THRESHOLD = 15
15
+
16
+
17
+ @hookimpl
18
+ def pylsp_lint(config, workspace, document):
19
+ with workspace.report_progress("lint: mccabe"):
20
+ threshold = config.plugin_settings("mccabe", document_path=document.path).get(
21
+ THRESHOLD, DEFAULT_THRESHOLD
22
+ )
23
+ log.debug("Running mccabe lint with threshold: %s", threshold)
24
+
25
+ try:
26
+ tree = compile(document.source, document.path, "exec", ast.PyCF_ONLY_AST)
27
+ except SyntaxError:
28
+ # We'll let the other linters point this one out
29
+ return None
30
+
31
+ visitor = mccabe.PathGraphingAstVisitor()
32
+ visitor.preorder(tree, visitor)
33
+
34
+ diags = []
35
+ for graph in visitor.graphs.values():
36
+ if graph.complexity() >= threshold:
37
+ diags.append(
38
+ {
39
+ "source": "mccabe",
40
+ "range": {
41
+ "start": {
42
+ "line": graph.lineno - 1,
43
+ "character": graph.column,
44
+ },
45
+ "end": {
46
+ "line": graph.lineno - 1,
47
+ "character": len(document.lines[graph.lineno]),
48
+ },
49
+ },
50
+ "message": "Cyclomatic complexity too high: %s (threshold %s)"
51
+ % (graph.complexity(), threshold),
52
+ "severity": lsp.DiagnosticSeverity.Warning,
53
+ }
54
+ )
55
+
56
+ return diags
@@ -0,0 +1,72 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+
6
+ from pylsp import hookimpl
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ MODULES = [
11
+ "numpy",
12
+ "tensorflow",
13
+ "sklearn",
14
+ "array",
15
+ "binascii",
16
+ "cmath",
17
+ "collections",
18
+ "datetime",
19
+ "errno",
20
+ "exceptions",
21
+ "gc",
22
+ "imageop",
23
+ "imp",
24
+ "itertools",
25
+ "marshal",
26
+ "math",
27
+ "matplotlib",
28
+ "mmap",
29
+ "mpmath",
30
+ "msvcrt",
31
+ "networkx",
32
+ "nose",
33
+ "nt",
34
+ "operator",
35
+ "os",
36
+ "os.path",
37
+ "pandas",
38
+ "parser",
39
+ "scipy",
40
+ "signal",
41
+ "skimage",
42
+ "statsmodels",
43
+ "strop",
44
+ "sympy",
45
+ "sys",
46
+ "thread",
47
+ "time",
48
+ "wx",
49
+ "zlib",
50
+ ]
51
+
52
+
53
+ @hookimpl
54
+ def pylsp_settings():
55
+ # Setup default modules to preload, and rope extension modules
56
+ return {
57
+ "plugins": {"preload": {"modules": MODULES}},
58
+ "rope": {"extensionModules": MODULES},
59
+ }
60
+
61
+
62
+ @hookimpl
63
+ def pylsp_initialize(config) -> None:
64
+ for mod_name in config.plugin_settings("preload").get("modules", []):
65
+ try:
66
+ __import__(mod_name)
67
+ log.debug("Preloaded module %s", mod_name)
68
+ except Exception:
69
+ # Catch any exception since not only ImportError can be raised here
70
+ # For example, old versions of NumPy can cause a ValueError.
71
+ # See spyder-ide/spyder#13985
72
+ pass
@@ -0,0 +1,113 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+
6
+ import pycodestyle
7
+
8
+ from pylsp import hookimpl, lsp
9
+ from pylsp._utils import get_eol_chars
10
+
11
+ try:
12
+ from autopep8 import continued_indentation as autopep8_c_i
13
+ except ImportError:
14
+ pass
15
+ else:
16
+ # Check if autopep8's continued_indentation implementation
17
+ # is overriding pycodestyle's and if so, re-register
18
+ # the check using pycodestyle's implementation as expected
19
+ if autopep8_c_i in pycodestyle._checks["logical_line"]:
20
+ del pycodestyle._checks["logical_line"][autopep8_c_i]
21
+ pycodestyle.register_check(pycodestyle.continued_indentation)
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ @hookimpl
27
+ def pylsp_lint(workspace, document):
28
+ with workspace.report_progress("lint: pycodestyle"):
29
+ config = workspace._config
30
+ settings = config.plugin_settings("pycodestyle", document_path=document.path)
31
+ log.debug("Got pycodestyle settings: %s", settings)
32
+
33
+ opts = {
34
+ "exclude": settings.get("exclude"),
35
+ "filename": settings.get("filename"),
36
+ "hang_closing": settings.get("hangClosing"),
37
+ "ignore": settings.get("ignore"),
38
+ "max_line_length": settings.get("maxLineLength"),
39
+ "indent_size": settings.get("indentSize"),
40
+ "select": settings.get("select"),
41
+ }
42
+ kwargs = {k: v for k, v in opts.items() if v}
43
+ styleguide = pycodestyle.StyleGuide(kwargs)
44
+
45
+ # Use LF to lint file because other line endings can give false positives.
46
+ # See spyder-ide/spyder#19565 for context.
47
+ source = document.source
48
+ eol_chars = get_eol_chars(source)
49
+ if eol_chars in ["\r", "\r\n"]:
50
+ source = source.replace(eol_chars, "\n")
51
+ lines = source.splitlines(keepends=True)
52
+ else:
53
+ lines = document.lines
54
+
55
+ c = pycodestyle.Checker(
56
+ filename=document.path,
57
+ lines=lines,
58
+ options=styleguide.options,
59
+ report=PyCodeStyleDiagnosticReport(styleguide.options),
60
+ )
61
+ c.check_all()
62
+ diagnostics = c.report.diagnostics
63
+
64
+ return diagnostics
65
+
66
+
67
+ class PyCodeStyleDiagnosticReport(pycodestyle.BaseReport):
68
+ def __init__(self, options) -> None:
69
+ self.diagnostics = []
70
+ super().__init__(options=options)
71
+
72
+ def error(self, line_number, offset, text, check):
73
+ code = text[:4]
74
+ if self._ignore_code(code):
75
+ return
76
+
77
+ # Don't care about expected errors or warnings
78
+ if code in self.expected:
79
+ return
80
+
81
+ # PyCodeStyle will sometimes give you an error the line after the end of the file
82
+ # e.g. no newline at end of file
83
+ # In that case, the end offset should just be some number ~100
84
+ # (because why not? There's nothing to underline anyways)
85
+ err_range = {
86
+ "start": {"line": line_number - 1, "character": offset},
87
+ "end": {
88
+ # FIXME: It's a little naiive to mark until the end of the line, can we not easily do better?
89
+ "line": line_number - 1,
90
+ "character": 100
91
+ if line_number > len(self.lines)
92
+ else len(self.lines[line_number - 1]),
93
+ },
94
+ }
95
+ diagnostic = {
96
+ "source": "pycodestyle",
97
+ "range": err_range,
98
+ "message": text,
99
+ "code": code,
100
+ # Are style errors really ever errors?
101
+ "severity": _get_severity(code),
102
+ }
103
+ if code.startswith("W6"):
104
+ diagnostic["tags"] = [lsp.DiagnosticTag.Deprecated]
105
+ self.diagnostics.append(diagnostic)
106
+
107
+
108
+ def _get_severity(code):
109
+ # Are style errors ever really errors?
110
+ if code[0] == "E" or code[0] == "W":
111
+ return lsp.DiagnosticSeverity.Warning
112
+ # If no severity is specified, why wouldn't this be informational only?
113
+ return lsp.DiagnosticSeverity.Information