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/python_lsp.py ADDED
@@ -0,0 +1,945 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import logging
5
+ import os
6
+ import socketserver
7
+ import threading
8
+ import uuid
9
+ from functools import partial
10
+ from hashlib import sha256
11
+ from typing import Any
12
+
13
+ try:
14
+ import ujson as json
15
+ except Exception:
16
+ import json
17
+
18
+ from pylsp_jsonrpc.dispatchers import MethodDispatcher
19
+ from pylsp_jsonrpc.endpoint import Endpoint
20
+ from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
21
+
22
+ from . import _utils, lsp, uris
23
+ from ._version import __version__
24
+ from .config import config
25
+ from .workspace import Cell, Document, Notebook, Workspace
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+
30
+ LINT_DEBOUNCE_S = 0.5 # 500 ms
31
+ PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s
32
+ MAX_WORKERS = 64
33
+ PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
34
+ CONFIG_FILEs = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8")
35
+
36
+
37
+ class _StreamHandlerWrapper(socketserver.StreamRequestHandler):
38
+ """A wrapper class that is used to construct a custom handler class."""
39
+
40
+ delegate = None
41
+
42
+ def setup(self) -> None:
43
+ super().setup()
44
+ self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile)
45
+
46
+ def handle(self) -> None:
47
+ self.auth(self.delegate.start)
48
+
49
+ self.SHUTDOWN_CALL()
50
+
51
+ def auth(self, cb):
52
+ token = ""
53
+ if "JUPYTER_TOKEN" in os.environ:
54
+ token = os.environ["JUPYTER_TOKEN"]
55
+ else:
56
+ log.warn("! Missing jupyter token !")
57
+
58
+ data = self.rfile.readline()
59
+ try:
60
+ auth_req = json.loads(data.decode().split("\n")[0])
61
+ except:
62
+ log.error("Error parsing authentication message")
63
+ auth_error_msg = {"msg": "AUTH_ERROR"}
64
+ self.wfile.write(json.dumps(auth_error_msg).encode())
65
+ return
66
+
67
+ hashed_token = sha256(token.encode()).hexdigest()
68
+ if auth_req.get("token") == hashed_token:
69
+ auth_success_msg = {"msg": "AUTH_SUCCESS"}
70
+ self.wfile.write(json.dumps(auth_success_msg).encode())
71
+ cb()
72
+ else:
73
+ log.info("Failed to authenticate: invalid credentials")
74
+ auth_invalid_msg = {"msg": "AUTH_INVALID_CRED"}
75
+ self.wfile.write(json.dumps(auth_invalid_msg).encode())
76
+
77
+
78
+ def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class) -> None:
79
+ if not issubclass(handler_class, PythonLSPServer):
80
+ raise ValueError("Handler class must be an instance of PythonLSPServer")
81
+
82
+ def shutdown_server(check_parent_process, *args):
83
+ if check_parent_process:
84
+ log.debug("Shutting down server")
85
+ # Shutdown call must be done on a thread, to prevent deadlocks
86
+ stop_thread = threading.Thread(target=server.shutdown)
87
+ stop_thread.start()
88
+
89
+ # Construct a custom wrapper class around the user's handler_class
90
+ wrapper_class = type(
91
+ handler_class.__name__ + "Handler",
92
+ (_StreamHandlerWrapper,),
93
+ {
94
+ "DELEGATE_CLASS": partial(
95
+ handler_class, check_parent_process=check_parent_process
96
+ ),
97
+ "SHUTDOWN_CALL": partial(shutdown_server, check_parent_process),
98
+ },
99
+ )
100
+
101
+ server = socketserver.TCPServer(
102
+ (bind_addr, port), wrapper_class, bind_and_activate=False
103
+ )
104
+ server.allow_reuse_address = True
105
+
106
+ try:
107
+ server.server_bind()
108
+ server.server_activate()
109
+ log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port)
110
+ server.serve_forever()
111
+ finally:
112
+ log.info("Shutting down")
113
+ server.server_close()
114
+
115
+
116
+ def start_io_lang_server(rfile, wfile, check_parent_process, handler_class) -> None:
117
+ if not issubclass(handler_class, PythonLSPServer):
118
+ raise ValueError("Handler class must be an instance of PythonLSPServer")
119
+ log.info("Starting %s IO language server", handler_class.__name__)
120
+ server = handler_class(rfile, wfile, check_parent_process)
121
+ server.start()
122
+
123
+
124
+ def start_ws_lang_server(port, check_parent_process, handler_class) -> None:
125
+ if not issubclass(handler_class, PythonLSPServer):
126
+ raise ValueError("Handler class must be an instance of PythonLSPServer")
127
+
128
+ # imports needed only for websockets based server
129
+ try:
130
+ import asyncio
131
+ from concurrent.futures import ThreadPoolExecutor
132
+
133
+ import websockets
134
+ except ImportError as e:
135
+ raise ImportError(
136
+ "websocket modules missing. Please run: pip install 'python-lsp-server[websockets]'"
137
+ ) from e
138
+
139
+ with ThreadPoolExecutor(max_workers=10) as tpool:
140
+ send_queue = None
141
+ loop = None
142
+
143
+ async def pylsp_ws(websocket):
144
+ log.debug("Creating LSP object")
145
+
146
+ # creating a partial function and suppling the websocket connection
147
+ response_handler = partial(send_message, websocket=websocket)
148
+
149
+ # Not using default stream reader and writer.
150
+ # Instead using a consumer based approach to handle processed requests
151
+ pylsp_handler = handler_class(
152
+ rx=None,
153
+ tx=None,
154
+ consumer=response_handler,
155
+ check_parent_process=check_parent_process,
156
+ )
157
+
158
+ async for message in websocket:
159
+ try:
160
+ log.debug("consuming payload and feeding it to LSP handler")
161
+ request = json.loads(message)
162
+ loop = asyncio.get_running_loop()
163
+ await loop.run_in_executor(tpool, pylsp_handler.consume, request)
164
+ except Exception as e:
165
+ log.exception("Failed to process request %s, %s", message, str(e))
166
+
167
+ def send_message(message, websocket):
168
+ """Handler to send responses of processed requests to respective web socket clients"""
169
+ try:
170
+ payload = json.dumps(message, ensure_ascii=False)
171
+ loop.call_soon_threadsafe(send_queue.put_nowait, (payload, websocket))
172
+ except Exception as e:
173
+ log.exception("Failed to write message %s, %s", message, str(e))
174
+
175
+ async def run_server():
176
+ nonlocal send_queue, loop
177
+ send_queue = asyncio.Queue()
178
+ loop = asyncio.get_running_loop()
179
+
180
+ async with websockets.serve(pylsp_ws, port=port):
181
+ while 1:
182
+ # Wait until payload is available for sending
183
+ payload, websocket = await send_queue.get()
184
+ await websocket.send(payload)
185
+
186
+ asyncio.run(run_server())
187
+
188
+
189
+ class PythonLSPServer(MethodDispatcher):
190
+ """Implementation of the Microsoft VSCode Language Server Protocol
191
+ https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
192
+ """
193
+
194
+ def __init__(
195
+ self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None
196
+ ) -> None:
197
+ self.workspace = None
198
+ self.config = None
199
+ self.root_uri = None
200
+ self.watching_thread = None
201
+ self.workspaces = {}
202
+ self.uri_workspace_mapper = {}
203
+
204
+ self._check_parent_process = check_parent_process
205
+
206
+ if rx is not None:
207
+ self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
208
+ else:
209
+ self._jsonrpc_stream_reader = None
210
+
211
+ if tx is not None:
212
+ self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
213
+ else:
214
+ self._jsonrpc_stream_writer = None
215
+
216
+ endpoint_cls = endpoint_cls or Endpoint
217
+
218
+ # if consumer is None, it is assumed that the default streams-based approach is being used
219
+ if consumer is None:
220
+ self._endpoint = endpoint_cls(
221
+ self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS
222
+ )
223
+ else:
224
+ self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)
225
+
226
+ self._dispatchers = []
227
+ self._shutdown = False
228
+
229
+ def start(self) -> None:
230
+ """Entry point for the server."""
231
+ self._jsonrpc_stream_reader.listen(self._endpoint.consume)
232
+
233
+ def consume(self, message) -> None:
234
+ """Entry point for consumer based server. Alternative to stream listeners."""
235
+ # assuming message will be JSON
236
+ self._endpoint.consume(message)
237
+
238
+ def __getitem__(self, item):
239
+ """Override getitem to fallback through multiple dispatchers."""
240
+ if self._shutdown and item != "exit":
241
+ # exit is the only allowed method during shutdown
242
+ log.debug("Ignoring non-exit method during shutdown: %s", item)
243
+ item = "invalid_request_after_shutdown"
244
+
245
+ try:
246
+ return super().__getitem__(item)
247
+ except KeyError:
248
+ # Fallback through extra dispatchers
249
+ for dispatcher in self._dispatchers:
250
+ try:
251
+ return dispatcher[item]
252
+ except KeyError:
253
+ continue
254
+
255
+ raise KeyError()
256
+
257
+ def m_shutdown(self, **_kwargs) -> None:
258
+ for workspace in self.workspaces.values():
259
+ workspace.close()
260
+ self._hook("pylsp_shutdown")
261
+ self._shutdown = True
262
+
263
+ def m_invalid_request_after_shutdown(self, **_kwargs):
264
+ return {
265
+ "error": {
266
+ "code": lsp.ErrorCodes.InvalidRequest,
267
+ "message": "Requests after shutdown are not valid",
268
+ }
269
+ }
270
+
271
+ def m_exit(self, **_kwargs) -> None:
272
+ self._endpoint.shutdown()
273
+ if self._jsonrpc_stream_reader is not None:
274
+ self._jsonrpc_stream_reader.close()
275
+ if self._jsonrpc_stream_writer is not None:
276
+ self._jsonrpc_stream_writer.close()
277
+
278
+ def _match_uri_to_workspace(self, uri):
279
+ workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
280
+ return self.workspaces.get(workspace_uri, self.workspace)
281
+
282
+ def _hook(self, hook_name, doc_uri=None, **kwargs):
283
+ """Calls hook_name and returns a list of results from all registered handlers"""
284
+ workspace = self._match_uri_to_workspace(doc_uri)
285
+ doc = workspace.get_document(doc_uri) if doc_uri else None
286
+ hook_handlers = self.config.plugin_manager.subset_hook_caller(
287
+ hook_name, self.config.disabled_plugins
288
+ )
289
+ return hook_handlers(
290
+ config=self.config, workspace=workspace, document=doc, **kwargs
291
+ )
292
+
293
+ def capabilities(self):
294
+ server_capabilities = {
295
+ "codeActionProvider": True,
296
+ "codeLensProvider": {
297
+ "resolveProvider": False, # We may need to make this configurable
298
+ },
299
+ "completionProvider": {
300
+ "resolveProvider": True, # We could know everything ahead of time, but this takes time to transfer
301
+ "triggerCharacters": ["."],
302
+ },
303
+ "documentFormattingProvider": True,
304
+ "documentHighlightProvider": True,
305
+ "documentRangeFormattingProvider": True,
306
+ "documentSymbolProvider": True,
307
+ "definitionProvider": True,
308
+ "typeDefinitionProvider": True,
309
+ "executeCommandProvider": {
310
+ "commands": flatten(self._hook("pylsp_commands"))
311
+ },
312
+ "hoverProvider": True,
313
+ "referencesProvider": True,
314
+ "renameProvider": True,
315
+ "foldingRangeProvider": True,
316
+ "signatureHelpProvider": {"triggerCharacters": ["(", ",", "="]},
317
+ "textDocumentSync": {
318
+ "change": lsp.TextDocumentSyncKind.INCREMENTAL,
319
+ "save": {
320
+ "includeText": True,
321
+ },
322
+ "openClose": True,
323
+ },
324
+ "notebookDocumentSync": {
325
+ "notebookSelector": [{"cells": [{"language": "python"}]}]
326
+ },
327
+ "workspace": {
328
+ "workspaceFolders": {"supported": True, "changeNotifications": True}
329
+ },
330
+ "experimental": merge(self._hook("pylsp_experimental_capabilities")),
331
+ }
332
+ log.info("Server capabilities: %s", server_capabilities)
333
+ return server_capabilities
334
+
335
+ def m_initialize(
336
+ self,
337
+ processId=None,
338
+ rootUri=None,
339
+ rootPath=None,
340
+ initializationOptions=None,
341
+ workspaceFolders=None,
342
+ **_kwargs,
343
+ ):
344
+ log.debug(
345
+ "Language server initialized with %s %s %s %s",
346
+ processId,
347
+ rootUri,
348
+ rootPath,
349
+ initializationOptions,
350
+ )
351
+ if rootUri is None:
352
+ rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ""
353
+
354
+ self.workspaces.pop(self.root_uri, None)
355
+ self.root_uri = rootUri
356
+ self.config = config.Config(
357
+ rootUri,
358
+ initializationOptions or {},
359
+ processId,
360
+ _kwargs.get("capabilities", {}),
361
+ )
362
+ self.workspace = Workspace(rootUri, self._endpoint, self.config)
363
+ self.workspaces[rootUri] = self.workspace
364
+ if workspaceFolders:
365
+ for folder in workspaceFolders:
366
+ uri = folder["uri"]
367
+ if uri == rootUri:
368
+ # Already created
369
+ continue
370
+ workspace_config = config.Config(
371
+ uri,
372
+ self.config._init_opts,
373
+ self.config._process_id,
374
+ self.config._capabilities,
375
+ )
376
+ workspace_config.update(self.config._settings)
377
+ self.workspaces[uri] = Workspace(uri, self._endpoint, workspace_config)
378
+
379
+ self._dispatchers = self._hook("pylsp_dispatchers")
380
+ self._hook("pylsp_initialize")
381
+
382
+ if (
383
+ self._check_parent_process
384
+ and processId is not None
385
+ and self.watching_thread is None
386
+ ):
387
+
388
+ def watch_parent_process(pid):
389
+ # exit when the given pid is not alive
390
+ if not _utils.is_process_alive(pid):
391
+ log.info("parent process %s is not alive, exiting!", pid)
392
+ self.m_exit()
393
+ else:
394
+ threading.Timer(
395
+ PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]
396
+ ).start()
397
+
398
+ self.watching_thread = threading.Thread(
399
+ target=watch_parent_process, args=(processId,)
400
+ )
401
+ self.watching_thread.daemon = True
402
+ self.watching_thread.start()
403
+ # Get our capabilities
404
+ return {
405
+ "capabilities": self.capabilities(),
406
+ "serverInfo": {
407
+ "name": "pylsp",
408
+ "version": __version__,
409
+ },
410
+ }
411
+
412
+ def m_initialized(self, **_kwargs) -> None:
413
+ self._hook("pylsp_initialized")
414
+
415
+ def code_actions(self, doc_uri: str, range: dict, context: dict):
416
+ return flatten(
417
+ self._hook("pylsp_code_actions", doc_uri, range=range, context=context)
418
+ )
419
+
420
+ def code_lens(self, doc_uri):
421
+ return flatten(self._hook("pylsp_code_lens", doc_uri))
422
+
423
+ def completions(self, doc_uri, position):
424
+ workspace = self._match_uri_to_workspace(doc_uri)
425
+ document = workspace.get_document(doc_uri)
426
+ ignored_names = None
427
+ if isinstance(document, Cell):
428
+ # We need to get the ignored names from the whole notebook document
429
+ notebook_document = workspace.get_maybe_document(document.notebook_uri)
430
+ ignored_names = notebook_document.jedi_names(doc_uri)
431
+ completions = self._hook(
432
+ "pylsp_completions", doc_uri, position=position, ignored_names=ignored_names
433
+ )
434
+ return {"isIncomplete": True, "items": flatten(completions)}
435
+
436
+ def completion_detail(self, item):
437
+ detail = self._hook("pylsp_completion_detail", item=item)
438
+ return detail
439
+
440
+ def completion_item_resolve(self, completion_item):
441
+ doc_uri = completion_item.get("data", {}).get("doc_uri", None)
442
+ return self._hook(
443
+ "pylsp_completion_item_resolve", doc_uri, completion_item=completion_item
444
+ )
445
+
446
+ def definitions(self, doc_uri, position):
447
+ return flatten(self._hook("pylsp_definitions", doc_uri, position=position))
448
+
449
+ def type_definition(self, doc_uri, position):
450
+ return self._hook("pylsp_type_definition", doc_uri, position=position)
451
+
452
+ def document_symbols(self, doc_uri):
453
+ return flatten(self._hook("pylsp_document_symbols", doc_uri))
454
+
455
+ def document_did_save(self, doc_uri):
456
+ return self._hook("pylsp_document_did_save", doc_uri)
457
+
458
+ def execute_command(self, command, arguments):
459
+ return self._hook("pylsp_execute_command", command=command, arguments=arguments)
460
+
461
+ def format_document(self, doc_uri, options):
462
+ return lambda: self._hook("pylsp_format_document", doc_uri, options=options)
463
+
464
+ def format_range(self, doc_uri, range, options):
465
+ return self._hook("pylsp_format_range", doc_uri, range=range, options=options)
466
+
467
+ def highlight(self, doc_uri, position):
468
+ return (
469
+ flatten(self._hook("pylsp_document_highlight", doc_uri, position=position))
470
+ or None
471
+ )
472
+
473
+ def hover(self, doc_uri, position):
474
+ return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
475
+
476
+ @_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
477
+ def lint(self, doc_uri, is_saved) -> None:
478
+ # Since we're debounced, the document may no longer be open
479
+ workspace = self._match_uri_to_workspace(doc_uri)
480
+ document_object = workspace.documents.get(doc_uri, None)
481
+ if isinstance(document_object, Document):
482
+ self._lint_text_document(
483
+ doc_uri, workspace, is_saved, document_object.version
484
+ )
485
+ elif isinstance(document_object, Notebook):
486
+ self._lint_notebook_document(document_object, workspace)
487
+
488
+ def _lint_text_document(
489
+ self, doc_uri, workspace, is_saved, doc_version=None
490
+ ) -> None:
491
+ workspace.publish_diagnostics(
492
+ doc_uri,
493
+ flatten(self._hook("pylsp_lint", doc_uri, is_saved=is_saved)),
494
+ doc_version,
495
+ )
496
+
497
+ def _lint_notebook_document(self, notebook_document, workspace) -> None:
498
+ """
499
+ Lint a notebook document.
500
+
501
+ This is a bit more complicated than linting a text document, because we need to
502
+ send the entire notebook document to the pylsp_lint hook, but we need to send
503
+ the diagnostics back to the client on a per-cell basis.
504
+ """
505
+
506
+ # First, we create a temp TextDocument that represents the whole notebook
507
+ # contents. We'll use this to send to the pylsp_lint hook.
508
+ random_uri = str(uuid.uuid4())
509
+
510
+ # cell_list helps us map the diagnostics back to the correct cell later.
511
+ cell_list: list[dict[str, Any]] = []
512
+
513
+ offset = 0
514
+ total_source = ""
515
+ for cell in notebook_document.cells:
516
+ cell_uri = cell["document"]
517
+ cell_document = workspace.get_cell_document(cell_uri)
518
+
519
+ num_lines = cell_document.line_count
520
+
521
+ data = {
522
+ "uri": cell_uri,
523
+ "line_start": offset,
524
+ "line_end": offset + num_lines - 1,
525
+ "source": cell_document.source,
526
+ }
527
+
528
+ cell_list.append(data)
529
+ if offset == 0:
530
+ total_source = cell_document.source
531
+ else:
532
+ total_source += "\n" + cell_document.source
533
+
534
+ offset += num_lines
535
+
536
+ workspace.put_document(random_uri, total_source)
537
+
538
+ try:
539
+ document_diagnostics = flatten(
540
+ self._hook("pylsp_lint", random_uri, is_saved=True)
541
+ )
542
+
543
+ # Now we need to map the diagnostics back to the correct cell and publish them.
544
+ # Note: this is O(n*m) in the number of cells and diagnostics, respectively.
545
+ for cell in cell_list:
546
+ cell_diagnostics = []
547
+ for diagnostic in document_diagnostics:
548
+ start_line = diagnostic["range"]["start"]["line"]
549
+ end_line = diagnostic["range"]["end"]["line"]
550
+
551
+ if start_line > cell["line_end"] or end_line < cell["line_start"]:
552
+ continue
553
+ diagnostic["range"]["start"]["line"] = (
554
+ start_line - cell["line_start"]
555
+ )
556
+ diagnostic["range"]["end"]["line"] = end_line - cell["line_start"]
557
+ cell_diagnostics.append(diagnostic)
558
+
559
+ workspace.publish_diagnostics(cell["uri"], cell_diagnostics)
560
+ finally:
561
+ workspace.rm_document(random_uri)
562
+
563
+ def references(self, doc_uri, position, exclude_declaration):
564
+ return flatten(
565
+ self._hook(
566
+ "pylsp_references",
567
+ doc_uri,
568
+ position=position,
569
+ exclude_declaration=exclude_declaration,
570
+ )
571
+ )
572
+
573
+ def rename(self, doc_uri, position, new_name):
574
+ return self._hook("pylsp_rename", doc_uri, position=position, new_name=new_name)
575
+
576
+ def signature_help(self, doc_uri, position):
577
+ return self._hook("pylsp_signature_help", doc_uri, position=position)
578
+
579
+ def folding(self, doc_uri):
580
+ return flatten(self._hook("pylsp_folding_range", doc_uri))
581
+
582
+ def m_completion_item__resolve(self, **completionItem):
583
+ return self.completion_detail(completionItem.get("label"))
584
+
585
+ def m_notebook_document__did_open(
586
+ self, notebookDocument=None, cellTextDocuments=None, **_kwargs
587
+ ) -> None:
588
+ workspace = self._match_uri_to_workspace(notebookDocument["uri"])
589
+ workspace.put_notebook_document(
590
+ notebookDocument["uri"],
591
+ notebookDocument["notebookType"],
592
+ cells=notebookDocument["cells"],
593
+ version=notebookDocument.get("version"),
594
+ metadata=notebookDocument.get("metadata"),
595
+ )
596
+ for cell in cellTextDocuments or []:
597
+ workspace.put_cell_document(
598
+ cell["uri"],
599
+ notebookDocument["uri"],
600
+ cell["languageId"],
601
+ cell["text"],
602
+ version=cell.get("version"),
603
+ )
604
+ self.lint(notebookDocument["uri"], is_saved=True)
605
+
606
+ def m_notebook_document__did_close(
607
+ self, notebookDocument=None, cellTextDocuments=None, **_kwargs
608
+ ) -> None:
609
+ workspace = self._match_uri_to_workspace(notebookDocument["uri"])
610
+ for cell in cellTextDocuments or []:
611
+ workspace.publish_diagnostics(cell["uri"], [])
612
+ workspace.rm_document(cell["uri"])
613
+ workspace.rm_document(notebookDocument["uri"])
614
+
615
+ def m_notebook_document__did_change(
616
+ self, notebookDocument=None, change=None, **_kwargs
617
+ ) -> None:
618
+ """
619
+ Changes to the notebook document.
620
+
621
+ This could be one of the following:
622
+ 1. Notebook metadata changed
623
+ 2. Cell(s) added
624
+ 3. Cell(s) deleted
625
+ 4. Cell(s) data changed
626
+ 4.1 Cell metadata changed
627
+ 4.2 Cell source changed
628
+ """
629
+ workspace = self._match_uri_to_workspace(notebookDocument["uri"])
630
+
631
+ if change.get("metadata"):
632
+ # Case 1
633
+ workspace.update_notebook_metadata(
634
+ notebookDocument["uri"], change.get("metadata")
635
+ )
636
+
637
+ cells = change.get("cells")
638
+ if cells:
639
+ # Change to cells
640
+ structure = cells.get("structure")
641
+ if structure:
642
+ # Case 2 or 3
643
+ notebook_cell_array_change = structure["array"]
644
+ start = notebook_cell_array_change["start"]
645
+ cell_delete_count = notebook_cell_array_change["deleteCount"]
646
+ if cell_delete_count == 0:
647
+ # Case 2
648
+ # Cell documents
649
+ for cell_document in structure["didOpen"]:
650
+ workspace.put_cell_document(
651
+ cell_document["uri"],
652
+ notebookDocument["uri"],
653
+ cell_document["languageId"],
654
+ cell_document["text"],
655
+ cell_document.get("version"),
656
+ )
657
+ # Cell metadata which is added to Notebook
658
+ workspace.add_notebook_cells(
659
+ notebookDocument["uri"],
660
+ notebook_cell_array_change["cells"],
661
+ start,
662
+ )
663
+ else:
664
+ # Case 3
665
+ # Cell documents
666
+ for cell_document in structure["didClose"]:
667
+ workspace.rm_document(cell_document["uri"])
668
+ workspace.publish_diagnostics(cell_document["uri"], [])
669
+ # Cell metadata which is removed from Notebook
670
+ workspace.remove_notebook_cells(
671
+ notebookDocument["uri"], start, cell_delete_count
672
+ )
673
+
674
+ data = cells.get("data")
675
+ if data:
676
+ # Case 4.1
677
+ for cell in data:
678
+ # update NotebookDocument.cells properties
679
+ pass
680
+
681
+ text_content = cells.get("textContent")
682
+ if text_content:
683
+ # Case 4.2
684
+ for cell in text_content:
685
+ cell_uri = cell["document"]["uri"]
686
+ # Even though the protocol says that `changes` is an array, we assume that it's always a single
687
+ # element array that contains the last change to the cell source.
688
+ workspace.update_document(cell_uri, cell["changes"][0])
689
+ self.lint(notebookDocument["uri"], is_saved=True)
690
+
691
+ def m_text_document__did_close(self, textDocument=None, **_kwargs) -> None:
692
+ workspace = self._match_uri_to_workspace(textDocument["uri"])
693
+ workspace.publish_diagnostics(textDocument["uri"], [])
694
+ workspace.rm_document(textDocument["uri"])
695
+
696
+ def m_text_document__did_open(self, textDocument=None, **_kwargs) -> None:
697
+ workspace = self._match_uri_to_workspace(textDocument["uri"])
698
+ workspace.put_document(
699
+ textDocument["uri"],
700
+ textDocument["text"],
701
+ version=textDocument.get("version"),
702
+ )
703
+ self._hook("pylsp_document_did_open", textDocument["uri"])
704
+ self.lint(textDocument["uri"], is_saved=True)
705
+
706
+ def m_text_document__did_change(
707
+ self, contentChanges=None, textDocument=None, **_kwargs
708
+ ) -> None:
709
+ workspace = self._match_uri_to_workspace(textDocument["uri"])
710
+ for change in contentChanges:
711
+ workspace.update_document(
712
+ textDocument["uri"], change, version=textDocument.get("version")
713
+ )
714
+ self.lint(textDocument["uri"], is_saved=False)
715
+
716
+ def m_text_document__did_save(self, textDocument=None, **_kwargs) -> None:
717
+ self.lint(textDocument["uri"], is_saved=True)
718
+ self.document_did_save(textDocument["uri"])
719
+
720
+ def m_text_document__code_action(
721
+ self, textDocument=None, range=None, context=None, **_kwargs
722
+ ):
723
+ return self.code_actions(textDocument["uri"], range, context)
724
+
725
+ def m_text_document__code_lens(self, textDocument=None, **_kwargs):
726
+ return self.code_lens(textDocument["uri"])
727
+
728
+ def _cell_document__completion(self, cellDocument, position=None, **_kwargs):
729
+ workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
730
+ notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
731
+ if notebookDocument is None:
732
+ raise ValueError("Invalid notebook document")
733
+
734
+ cell_data = notebookDocument.cell_data()
735
+
736
+ # Concatenate all cells to be a single temporary document
737
+ total_source = "\n".join(data["source"] for data in cell_data.values())
738
+ with workspace.temp_document(total_source) as temp_uri:
739
+ # update position to be the position in the temp document
740
+ if position is not None:
741
+ position["line"] += cell_data[cellDocument.uri]["line_start"]
742
+
743
+ completions = self.completions(temp_uri, position)
744
+
745
+ # Translate temp_uri locations to cell document locations
746
+ for item in completions.get("items", []):
747
+ if item.get("data", {}).get("doc_uri") == temp_uri:
748
+ item["data"]["doc_uri"] = cellDocument.uri
749
+
750
+ # Copy LAST_JEDI_COMPLETIONS to cell document so that completionItem/resolve will work
751
+ tempDocument = workspace.get_document(temp_uri)
752
+ cellDocument.shared_data["LAST_JEDI_COMPLETIONS"] = (
753
+ tempDocument.shared_data.get("LAST_JEDI_COMPLETIONS", None)
754
+ )
755
+
756
+ return completions
757
+
758
+ def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
759
+ # textDocument here is just a dict with a uri
760
+ workspace = self._match_uri_to_workspace(textDocument["uri"])
761
+ document = workspace.get_document(textDocument["uri"])
762
+ if isinstance(document, Cell):
763
+ return self._cell_document__completion(document, position, **_kwargs)
764
+ return self.completions(textDocument["uri"], position)
765
+
766
+ def _cell_document__definition(self, cellDocument, position=None, **_kwargs):
767
+ workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
768
+ notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
769
+ if notebookDocument is None:
770
+ raise ValueError("Invalid notebook document")
771
+
772
+ cell_data = notebookDocument.cell_data()
773
+
774
+ # Concatenate all cells to be a single temporary document
775
+ total_source = "\n".join(data["source"] for data in cell_data.values())
776
+ with workspace.temp_document(total_source) as temp_uri:
777
+ # update position to be the position in the temp document
778
+ if position is not None:
779
+ position["line"] += cell_data[cellDocument.uri]["line_start"]
780
+
781
+ definitions = self.definitions(temp_uri, position)
782
+
783
+ # Translate temp_uri locations to cell document locations
784
+ for definition in definitions:
785
+ if definition["uri"] == temp_uri:
786
+ # Find the cell the start line is in and adjust the uri and line numbers
787
+ for cell_uri, data in cell_data.items():
788
+ if (
789
+ data["line_start"]
790
+ <= definition["range"]["start"]["line"]
791
+ <= data["line_end"]
792
+ ):
793
+ definition["uri"] = cell_uri
794
+ definition["range"]["start"]["line"] -= data["line_start"]
795
+ definition["range"]["end"]["line"] -= data["line_start"]
796
+ break
797
+
798
+ return definitions
799
+
800
+ def m_text_document__definition(self, textDocument=None, position=None, **_kwargs):
801
+ # textDocument here is just a dict with a uri
802
+ workspace = self._match_uri_to_workspace(textDocument["uri"])
803
+ document = workspace.get_document(textDocument["uri"])
804
+ if isinstance(document, Cell):
805
+ return self._cell_document__definition(document, position, **_kwargs)
806
+ return self.definitions(textDocument["uri"], position)
807
+
808
+ def m_text_document__type_definition(
809
+ self, textDocument=None, position=None, **_kwargs
810
+ ):
811
+ return self.type_definition(textDocument["uri"], position)
812
+
813
+ def m_text_document__document_highlight(
814
+ self, textDocument=None, position=None, **_kwargs
815
+ ):
816
+ return self.highlight(textDocument["uri"], position)
817
+
818
+ def m_text_document__hover(self, textDocument=None, position=None, **_kwargs):
819
+ return self.hover(textDocument["uri"], position)
820
+
821
+ def m_text_document__document_symbol(self, textDocument=None, **_kwargs):
822
+ return self.document_symbols(textDocument["uri"])
823
+
824
+ def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs):
825
+ return self.format_document(textDocument["uri"], options)
826
+
827
+ def m_text_document__rename(
828
+ self, textDocument=None, position=None, newName=None, **_kwargs
829
+ ):
830
+ return self.rename(textDocument["uri"], position, newName)
831
+
832
+ def m_text_document__folding_range(self, textDocument=None, **_kwargs):
833
+ return self.folding(textDocument["uri"])
834
+
835
+ def m_text_document__range_formatting(
836
+ self, textDocument=None, range=None, options=None, **_kwargs
837
+ ):
838
+ return self.format_range(textDocument["uri"], range, options)
839
+
840
+ def m_text_document__references(
841
+ self, textDocument=None, position=None, context=None, **_kwargs
842
+ ):
843
+ exclude_declaration = not context["includeDeclaration"]
844
+ return self.references(textDocument["uri"], position, exclude_declaration)
845
+
846
+ def m_text_document__signature_help(
847
+ self, textDocument=None, position=None, **_kwargs
848
+ ):
849
+ return self.signature_help(textDocument["uri"], position)
850
+
851
+ def m_workspace__did_change_configuration(self, settings=None) -> None:
852
+ if self.config is not None:
853
+ self.config.update((settings or {}).get("pylsp", {}))
854
+ for workspace in self.workspaces.values():
855
+ workspace.update_config(settings)
856
+ self._hook("pylsp_workspace_configuration_changed")
857
+ for doc_uri in workspace.documents:
858
+ self.lint(doc_uri, is_saved=False)
859
+
860
+ def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
861
+ if event is None:
862
+ return
863
+ added = event.get("added", [])
864
+ removed = event.get("removed", [])
865
+
866
+ for removed_info in removed:
867
+ if "uri" in removed_info:
868
+ removed_uri = removed_info["uri"]
869
+ self.workspaces.pop(removed_uri, None)
870
+
871
+ for added_info in added:
872
+ if "uri" in added_info:
873
+ added_uri = added_info["uri"]
874
+ workspace_config = config.Config(
875
+ added_uri,
876
+ self.config._init_opts,
877
+ self.config._process_id,
878
+ self.config._capabilities,
879
+ )
880
+ workspace_config.update(self.config._settings)
881
+ self.workspaces[added_uri] = Workspace(
882
+ added_uri, self._endpoint, workspace_config
883
+ )
884
+
885
+ root_workspace_removed = any(
886
+ removed_info["uri"] == self.root_uri for removed_info in removed
887
+ )
888
+ workspace_added = len(added) > 0 and "uri" in added[0]
889
+ if root_workspace_removed and workspace_added:
890
+ added_uri = added[0]["uri"]
891
+ self.root_uri = added_uri
892
+ new_root_workspace = self.workspaces[added_uri]
893
+ self.config = new_root_workspace._config
894
+ self.workspace = new_root_workspace
895
+ elif root_workspace_removed:
896
+ # NOTE: Removing the root workspace can only happen when the server
897
+ # is closed, thus the else condition of this if can never happen.
898
+ if self.workspaces:
899
+ log.debug("Root workspace deleted!")
900
+ available_workspaces = sorted(self.workspaces)
901
+ first_workspace = available_workspaces[0]
902
+ new_root_workspace = self.workspaces[first_workspace]
903
+ self.root_uri = first_workspace
904
+ self.config = new_root_workspace._config
905
+ self.workspace = new_root_workspace
906
+
907
+ # Migrate documents that are on the root workspace and have a better
908
+ # match now
909
+ doc_uris = list(self.workspace._docs.keys())
910
+ for uri in doc_uris:
911
+ doc = self.workspace._docs.pop(uri)
912
+ new_workspace = self._match_uri_to_workspace(uri)
913
+ new_workspace._docs[uri] = doc
914
+
915
+ def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
916
+ changed_py_files = set()
917
+ config_changed = False
918
+ for d in changes or []:
919
+ if d["uri"].endswith(PYTHON_FILE_EXTENSIONS):
920
+ changed_py_files.add(d["uri"])
921
+ elif d["uri"].endswith(CONFIG_FILEs):
922
+ config_changed = True
923
+
924
+ if config_changed:
925
+ self.config.settings.cache_clear()
926
+ elif not changed_py_files:
927
+ # Only externally changed python files and lint configs may result in changed diagnostics.
928
+ return
929
+
930
+ for workspace in self.workspaces.values():
931
+ for doc_uri in workspace.documents:
932
+ # Changes in doc_uri are already handled by m_text_document__did_save
933
+ if doc_uri not in changed_py_files:
934
+ self.lint(doc_uri, is_saved=False)
935
+
936
+ def m_workspace__execute_command(self, command=None, arguments=None):
937
+ return self.execute_command(command, arguments)
938
+
939
+
940
+ def flatten(list_of_lists):
941
+ return [item for lst in list_of_lists for item in lst]
942
+
943
+
944
+ def merge(list_of_dicts):
945
+ return {k: v for dictionary in list_of_dicts for k, v in dictionary.items()}