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/workspace.py ADDED
@@ -0,0 +1,791 @@
1
+ # Copyright 2017-2020 Palantir Technologies, Inc.
2
+ # Copyright 2021- Python Language Server Contributors.
3
+
4
+ import functools
5
+ import importlib.metadata
6
+ import io
7
+ import logging
8
+ import os
9
+ import re
10
+ import uuid
11
+ from collections.abc import Generator
12
+ from contextlib import contextmanager
13
+ from threading import RLock
14
+ from typing import Callable, Optional
15
+
16
+ import jedi
17
+
18
+ from . import _utils, lsp, uris
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+ DEFAULT_AUTO_IMPORT_MODULES = ["numpy"]
23
+
24
+ # TODO: this is not the best e.g. we capture numbers
25
+ RE_START_WORD = re.compile("[A-Za-z_0-9]*$")
26
+ RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
27
+
28
+
29
+ def lock(method):
30
+ """Define an atomic region over a method."""
31
+
32
+ @functools.wraps(method)
33
+ def wrapper(self, *args, **kwargs):
34
+ with self._lock:
35
+ return method(self, *args, **kwargs)
36
+
37
+ return wrapper
38
+
39
+
40
+ class Workspace:
41
+ M_PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics"
42
+ M_PROGRESS = "$/progress"
43
+ M_INITIALIZE_PROGRESS = "window/workDoneProgress/create"
44
+ M_APPLY_EDIT = "workspace/applyEdit"
45
+ M_SHOW_MESSAGE = "window/showMessage"
46
+ M_LOG_MESSAGE = "window/logMessage"
47
+
48
+ def __init__(self, root_uri, endpoint, config=None) -> None:
49
+ self._config = config
50
+ self._root_uri = root_uri
51
+ self._endpoint = endpoint
52
+ self._root_uri_scheme = uris.urlparse(self._root_uri)[0]
53
+ self._root_path = uris.to_fs_path(self._root_uri)
54
+ self._docs = {}
55
+
56
+ # Cache jedi environments
57
+ self._environments = {}
58
+
59
+ # Whilst incubating, keep rope private
60
+ self.__rope = None
61
+ self.__rope_config = None
62
+ self.__rope_autoimport = None
63
+
64
+ def _rope_autoimport(
65
+ self,
66
+ rope_config: Optional,
67
+ memory: bool = False,
68
+ ):
69
+ from rope.contrib.autoimport.sqlite import AutoImport
70
+
71
+ if self.__rope_autoimport is None:
72
+ project = self._rope_project_builder(rope_config)
73
+ self.__rope_autoimport = AutoImport(project, memory=memory)
74
+ return self.__rope_autoimport
75
+
76
+ def _rope_project_builder(self, rope_config):
77
+ from rope.base.project import Project
78
+
79
+ # TODO: we could keep track of dirty files and validate only those
80
+ if self.__rope is None or self.__rope_config != rope_config:
81
+ rope_folder = rope_config.get("ropeFolder")
82
+ if "ropeFolder" in rope_config:
83
+ self.__rope = Project(self._root_path, ropefolder=rope_folder)
84
+ else:
85
+ self.__rope = Project(self._root_path)
86
+ self.__rope.prefs.set(
87
+ "extension_modules", rope_config.get("extensionModules", [])
88
+ )
89
+ self.__rope.prefs.set("ignore_syntax_errors", True)
90
+ self.__rope.prefs.set("ignore_bad_imports", True)
91
+ self.__rope.validate()
92
+ return self.__rope
93
+
94
+ @property
95
+ def documents(self):
96
+ return self._docs
97
+
98
+ @property
99
+ def root_path(self):
100
+ return self._root_path
101
+
102
+ @property
103
+ def root_uri(self):
104
+ return self._root_uri
105
+
106
+ def is_local(self):
107
+ return (self._root_uri_scheme in ["", "file"]) and os.path.exists(
108
+ self._root_path
109
+ )
110
+
111
+ def get_document(self, doc_uri):
112
+ """Return a managed document if-present, else create one pointing at disk.
113
+
114
+ See https://github.com/Microsoft/language-server-protocol/issues/177
115
+ """
116
+ return self._docs.get(doc_uri) or self._create_document(doc_uri)
117
+
118
+ def get_cell_document(self, doc_uri):
119
+ return self._docs.get(doc_uri)
120
+
121
+ def get_maybe_document(self, doc_uri):
122
+ return self._docs.get(doc_uri)
123
+
124
+ def put_document(self, doc_uri, source, version=None) -> None:
125
+ self._docs[doc_uri] = self._create_document(
126
+ doc_uri, source=source, version=version
127
+ )
128
+
129
+ def put_notebook_document(
130
+ self, doc_uri, notebook_type, cells, version=None, metadata=None
131
+ ) -> None:
132
+ self._docs[doc_uri] = self._create_notebook_document(
133
+ doc_uri, notebook_type, cells, version, metadata
134
+ )
135
+
136
+ @contextmanager
137
+ def temp_document(self, source, path=None) -> None:
138
+ if path is None:
139
+ path = self.root_path
140
+ uri = uris.from_fs_path(os.path.join(path, str(uuid.uuid4())))
141
+ try:
142
+ self.put_document(uri, source)
143
+ yield uri
144
+ finally:
145
+ self.rm_document(uri)
146
+
147
+ def add_notebook_cells(self, doc_uri, cells, start) -> None:
148
+ self._docs[doc_uri].add_cells(cells, start)
149
+
150
+ def remove_notebook_cells(self, doc_uri, start, delete_count) -> None:
151
+ self._docs[doc_uri].remove_cells(start, delete_count)
152
+
153
+ def update_notebook_metadata(self, doc_uri, metadata) -> None:
154
+ self._docs[doc_uri].metadata = metadata
155
+
156
+ def put_cell_document(
157
+ self, doc_uri, notebook_uri, language_id, source, version=None
158
+ ) -> None:
159
+ self._docs[doc_uri] = self._create_cell_document(
160
+ doc_uri, notebook_uri, language_id, source, version
161
+ )
162
+
163
+ def rm_document(self, doc_uri) -> None:
164
+ self._docs.pop(doc_uri)
165
+
166
+ def update_document(self, doc_uri, change, version=None) -> None:
167
+ self._docs[doc_uri].apply_change(change)
168
+ self._docs[doc_uri].version = version
169
+
170
+ def update_config(self, settings):
171
+ self._config.update((settings or {}).get("pylsp", {}))
172
+ for doc_uri in self.documents:
173
+ if isinstance(document := self.get_document(doc_uri), Notebook):
174
+ # Notebook documents don't have a config. The config is
175
+ # handled at the cell level.
176
+ return
177
+ document.update_config(settings)
178
+
179
+ def apply_edit(self, edit):
180
+ return self._endpoint.request(self.M_APPLY_EDIT, {"edit": edit})
181
+
182
+ def publish_diagnostics(self, doc_uri, diagnostics, doc_version=None) -> None:
183
+ params = {
184
+ "uri": doc_uri,
185
+ "diagnostics": diagnostics,
186
+ }
187
+
188
+ if doc_version:
189
+ params["version"] = doc_version
190
+
191
+ self._endpoint.notify(
192
+ self.M_PUBLISH_DIAGNOSTICS,
193
+ params=params,
194
+ )
195
+
196
+ @contextmanager
197
+ def report_progress(
198
+ self,
199
+ title: str,
200
+ message: Optional[str] = None,
201
+ percentage: Optional[int] = None,
202
+ skip_token_initialization: bool = False,
203
+ ) -> Generator[Callable[[str, Optional[int]], None], None, None]:
204
+ """
205
+ Report progress to the editor / client.
206
+
207
+ ``skip_token_initialization` is necessary due to some current
208
+ limitations of our LSP implementation. When `report_progress`
209
+ is used from a synchronous LSP handler, the token initialization
210
+ will time out because we can't receive the response.
211
+
212
+ Many editors will still correctly show the progress messages though, which
213
+ is why we are giving progress users the option to skip the initialization
214
+ of the progress token.
215
+ """
216
+ if self._config:
217
+ client_supports_progress_reporting = self._config.capabilities.get(
218
+ "window", {}
219
+ ).get("workDoneProgress", False)
220
+ else:
221
+ client_supports_progress_reporting = False
222
+
223
+ if client_supports_progress_reporting:
224
+ token = self._progress_begin(
225
+ title, message, percentage, skip_token_initialization
226
+ )
227
+
228
+ def progress_message(
229
+ message: str, percentage: Optional[int] = None
230
+ ) -> None:
231
+ self._progress_report(token, message, percentage)
232
+
233
+ try:
234
+ yield progress_message
235
+ finally:
236
+ self._progress_end(token)
237
+
238
+ return
239
+
240
+ # FALLBACK:
241
+ # If the client doesn't support progress reporting, we have a dummy method
242
+ # for the caller to use.
243
+ def dummy_progress_message(
244
+ message: str, percentage: Optional[int] = None
245
+ ) -> None:
246
+ pass
247
+
248
+ yield dummy_progress_message
249
+
250
+ def _progress_begin(
251
+ self,
252
+ title: str,
253
+ message: Optional[str] = None,
254
+ percentage: Optional[int] = None,
255
+ skip_token_initialization: bool = False,
256
+ ) -> str:
257
+ token = str(uuid.uuid4())
258
+
259
+ if not skip_token_initialization:
260
+ try:
261
+ self._endpoint.request(
262
+ self.M_INITIALIZE_PROGRESS, {"token": token}
263
+ ).result(timeout=1.0)
264
+ except Exception:
265
+ log.warning(
266
+ "There was an error while trying to initialize progress reporting."
267
+ "Likely progress reporting was used in a synchronous LSP handler, "
268
+ "which is not supported by progress reporting yet. "
269
+ "To prevent waiting for the timeout you can set "
270
+ "`skip_token_initialization=True`. "
271
+ "Not every editor will show progress then, but many will.",
272
+ exc_info=True,
273
+ )
274
+
275
+ value = {
276
+ "kind": "begin",
277
+ "title": title,
278
+ }
279
+ if message is not None:
280
+ value["message"] = message
281
+ if percentage is not None:
282
+ value["percentage"] = percentage
283
+
284
+ self._endpoint.notify(
285
+ self.M_PROGRESS,
286
+ params={
287
+ "token": token,
288
+ "value": value,
289
+ },
290
+ )
291
+ return token
292
+
293
+ def _progress_report(
294
+ self,
295
+ token: str,
296
+ message: Optional[str] = None,
297
+ percentage: Optional[int] = None,
298
+ ) -> None:
299
+ value = {
300
+ "kind": "report",
301
+ }
302
+ if message:
303
+ value["message"] = message
304
+ if percentage:
305
+ value["percentage"] = percentage
306
+
307
+ self._endpoint.notify(
308
+ self.M_PROGRESS,
309
+ params={
310
+ "token": token,
311
+ "value": value,
312
+ },
313
+ )
314
+
315
+ def _progress_end(self, token: str, message: Optional[str] = None) -> None:
316
+ value = {
317
+ "kind": "end",
318
+ }
319
+ if message:
320
+ value["message"] = message
321
+
322
+ self._endpoint.notify(
323
+ self.M_PROGRESS,
324
+ params={
325
+ "token": token,
326
+ "value": value,
327
+ },
328
+ )
329
+
330
+ def log_message(self, message, msg_type=lsp.MessageType.Info):
331
+ self._endpoint.notify(
332
+ self.M_LOG_MESSAGE, params={"type": msg_type, "message": message}
333
+ )
334
+
335
+ def show_message(self, message, msg_type=lsp.MessageType.Info) -> None:
336
+ self._endpoint.notify(
337
+ self.M_SHOW_MESSAGE, params={"type": msg_type, "message": message}
338
+ )
339
+
340
+ def source_roots(self, document_path):
341
+ """Return the source roots for the given document."""
342
+ files = (
343
+ _utils.find_parents(
344
+ self._root_path, document_path, ["setup.py", "pyproject.toml"]
345
+ )
346
+ or []
347
+ )
348
+ return list({os.path.dirname(project_file) for project_file in files}) or [
349
+ self._root_path
350
+ ]
351
+
352
+ def _create_document(self, doc_uri, source=None, version=None):
353
+ path = uris.to_fs_path(doc_uri)
354
+ return Document(
355
+ doc_uri,
356
+ self,
357
+ source=source,
358
+ version=version,
359
+ extra_sys_path=self.source_roots(path),
360
+ rope_project_builder=self._rope_project_builder,
361
+ )
362
+
363
+ def _create_notebook_document(
364
+ self, doc_uri, notebook_type, cells, version=None, metadata=None
365
+ ):
366
+ return Notebook(
367
+ doc_uri,
368
+ notebook_type,
369
+ self,
370
+ cells=cells,
371
+ version=version,
372
+ metadata=metadata,
373
+ )
374
+
375
+ def _create_cell_document(
376
+ self, doc_uri, notebook_uri, language_id, source=None, version=None
377
+ ):
378
+ # TODO: remove what is unnecessary here.
379
+ path = uris.to_fs_path(doc_uri)
380
+ return Cell(
381
+ doc_uri,
382
+ notebook_uri=notebook_uri,
383
+ language_id=language_id,
384
+ workspace=self,
385
+ source=source,
386
+ version=version,
387
+ extra_sys_path=self.source_roots(path),
388
+ rope_project_builder=self._rope_project_builder,
389
+ )
390
+
391
+ def close(self) -> None:
392
+ if self.__rope_autoimport:
393
+ self.__rope_autoimport.close()
394
+
395
+
396
+ class Document:
397
+ DO_NOT_PRELOAD_MODULES = [
398
+ "attrs",
399
+ "backcall",
400
+ "bleach",
401
+ "certifi",
402
+ "chardet",
403
+ "cycler",
404
+ "decorator",
405
+ "defusedxml",
406
+ "docopt",
407
+ "entrypoints",
408
+ "idna",
409
+ "importlib-metadata",
410
+ "ipykernel",
411
+ "ipython-genutils",
412
+ "ipython",
413
+ "ipywidgets",
414
+ "jedi",
415
+ "jinja2",
416
+ "joblib",
417
+ "jsonschema",
418
+ "jupyter-client",
419
+ "jupyter-core",
420
+ "markupsafe",
421
+ "mistune",
422
+ "nbconvert",
423
+ "nbformat",
424
+ "notebook",
425
+ "packaging",
426
+ "pandocfilters",
427
+ "parso",
428
+ "pexpect",
429
+ "pickleshare",
430
+ "pip",
431
+ "pipreqs",
432
+ "pluggy",
433
+ "prometheus-client",
434
+ "prompt-toolkit",
435
+ "ptyprocess",
436
+ "pygments",
437
+ "pyparsing",
438
+ "pyrsistent",
439
+ "python-dateutil",
440
+ "python-jsonrpc-server",
441
+ "python-language-server",
442
+ "pytz",
443
+ "pyzmq",
444
+ "send2trash",
445
+ "setuptools",
446
+ "six",
447
+ "terminado",
448
+ "testpath",
449
+ "threadpoolctl",
450
+ "tornado",
451
+ "traitlets",
452
+ "ujson",
453
+ "wcwidth",
454
+ "webencodings",
455
+ "wheel",
456
+ "widgetsnbextension",
457
+ "yarg",
458
+ "zipp",
459
+ ]
460
+
461
+ def __init__(
462
+ self,
463
+ uri,
464
+ workspace,
465
+ source=None,
466
+ version=None,
467
+ local=True,
468
+ extra_sys_path=None,
469
+ rope_project_builder=None,
470
+ ) -> None:
471
+ self.uri = uri
472
+ self.version = version
473
+ self.path = uris.to_fs_path(uri)
474
+ self.dot_path = _utils.path_to_dot_name(self.path)
475
+ self.filename = os.path.basename(self.path)
476
+ self.shared_data = {}
477
+
478
+ self._config = workspace._config
479
+ self._workspace = workspace
480
+ self._local = local
481
+ self._source = source
482
+ self._extra_sys_path = extra_sys_path or []
483
+ self._rope_project_builder = rope_project_builder
484
+ self._lock = RLock()
485
+
486
+ jedi.settings.cache_directory = ".cache/jedi/"
487
+ jedi.settings.use_filesystem_cache = True
488
+ jedi.settings.auto_import_modules = self._get_auto_import_modules()
489
+
490
+ def _get_auto_import_modules(self):
491
+ installed_packages_list = [
492
+ dist.metadata["Name"] for dist in importlib.metadata.distributions()
493
+ ]
494
+ auto_import_modules = [
495
+ pkg
496
+ for pkg in installed_packages_list
497
+ if pkg not in self.DO_NOT_PRELOAD_MODULES
498
+ ]
499
+ return auto_import_modules
500
+
501
+ def __str__(self):
502
+ return str(self.uri)
503
+
504
+ def _rope_resource(self, rope_config):
505
+ from rope.base import libutils
506
+
507
+ return libutils.path_to_resource(
508
+ self._rope_project_builder(rope_config), self.path
509
+ )
510
+
511
+ @property
512
+ @lock
513
+ def lines(self):
514
+ return self.source.splitlines(True)
515
+
516
+ @property
517
+ @lock
518
+ def source(self):
519
+ if self._source is None:
520
+ with open(self.path, encoding="utf-8") as f:
521
+ return f.read()
522
+ return self._source
523
+
524
+ def update_config(self, settings) -> None:
525
+ self._config.update((settings or {}).get("pylsp", {}))
526
+
527
+ @lock
528
+ def apply_change(self, change):
529
+ """Apply a change to the document."""
530
+ text = change["text"]
531
+ change_range = change.get("range")
532
+
533
+ if not change_range:
534
+ # The whole file has changed
535
+ self._source = text
536
+ return
537
+
538
+ start_line = change_range["start"]["line"]
539
+ start_col = change_range["start"]["character"]
540
+ end_line = change_range["end"]["line"]
541
+ end_col = change_range["end"]["character"]
542
+
543
+ # Check for an edit occuring at the very end of the file
544
+ lines = self.lines
545
+ if start_line == len(lines):
546
+ self._source = self.source + text
547
+ return
548
+
549
+ new = io.StringIO()
550
+
551
+ # Iterate over the existing document until we hit the edit range,
552
+ # at which point we write the new text, then loop until we hit
553
+ # the end of the range and continue writing.
554
+ for i, line in enumerate(lines):
555
+ if i < start_line:
556
+ new.write(line)
557
+ continue
558
+
559
+ if i > end_line:
560
+ new.write(line)
561
+ continue
562
+
563
+ if i == start_line:
564
+ new.write(line[:start_col])
565
+ new.write(text)
566
+
567
+ if i == end_line:
568
+ new.write(line[end_col:])
569
+
570
+ self._source = new.getvalue()
571
+
572
+ def offset_at_position(self, position):
573
+ """Return the byte-offset pointed at by the given position."""
574
+ return position["character"] + len("".join(self.lines[: position["line"]]))
575
+
576
+ def word_at_position(self, position):
577
+ """Get the word under the cursor returning the start and end positions."""
578
+ lines = self.lines
579
+ if position["line"] >= len(lines):
580
+ return ""
581
+
582
+ line = lines[position["line"]]
583
+ i = position["character"]
584
+ # Split word in two
585
+ start = line[:i]
586
+ end = line[i:]
587
+
588
+ # Take end of start and start of end to find word
589
+ # These are guaranteed to match, even if they match the empty string
590
+ m_start = RE_START_WORD.findall(start)
591
+ m_end = RE_END_WORD.findall(end)
592
+
593
+ return m_start[0] + m_end[-1]
594
+
595
+ @lock
596
+ def jedi_names(self, all_scopes=False, definitions=True, references=False):
597
+ script = self.jedi_script()
598
+ return script.get_names(
599
+ all_scopes=all_scopes, definitions=definitions, references=references
600
+ )
601
+
602
+ @lock
603
+ def jedi_script(self, position=None, use_document_path=False):
604
+ extra_paths = []
605
+ environment_path = None
606
+ env_vars = None
607
+ prioritize_extra_paths = False
608
+
609
+ if self._config:
610
+ jedi_settings = self._config.plugin_settings(
611
+ "jedi", document_path=self.path
612
+ )
613
+ jedi.settings.auto_import_modules = jedi_settings.get(
614
+ "auto_import_modules", DEFAULT_AUTO_IMPORT_MODULES
615
+ )
616
+ environment_path = jedi_settings.get("environment")
617
+ # Jedi itself cannot deal with homedir-relative paths.
618
+ # On systems, where it is expected, expand the home directory.
619
+ if environment_path and os.name != "nt":
620
+ environment_path = os.path.expanduser(environment_path)
621
+
622
+ extra_paths = jedi_settings.get("extra_paths") or []
623
+ env_vars = jedi_settings.get("env_vars")
624
+ prioritize_extra_paths = jedi_settings.get("prioritize_extra_paths")
625
+
626
+ # Drop PYTHONPATH from env_vars before creating the environment to
627
+ # ensure that Jedi can startup properly without module name collision.
628
+ if env_vars is None:
629
+ env_vars = os.environ.copy()
630
+ env_vars.pop("PYTHONPATH", None)
631
+
632
+ sys_path = self.sys_path(
633
+ environment_path, env_vars, prioritize_extra_paths, extra_paths
634
+ )
635
+
636
+ import __main__
637
+
638
+ # Extend sys_path with document's path if requested
639
+ if use_document_path:
640
+ sys_path += [os.path.normpath(os.path.dirname(self.path))]
641
+
642
+ kwargs = {
643
+ "code": self.source,
644
+ "path": self.path,
645
+ "namespaces": [__main__.__dict__],
646
+ }
647
+
648
+ if position:
649
+ # Deprecated by Jedi to use in Script() constructor
650
+ kwargs += _utils.position_to_jedi_linecolumn(self, position)
651
+
652
+ return jedi.Interpreter(**kwargs)
653
+
654
+ def get_enviroment(self, environment_path=None, env_vars=None):
655
+ # TODO(gatesn): #339 - make better use of jedi environments, they seem pretty powerful
656
+ if environment_path is None:
657
+ environment = jedi.api.environment.get_cached_default_environment()
658
+ else:
659
+ if environment_path in self._workspace._environments:
660
+ environment = self._workspace._environments[environment_path]
661
+ else:
662
+ environment = jedi.api.environment.create_environment(
663
+ path=environment_path, safe=False, env_vars=env_vars
664
+ )
665
+ self._workspace._environments[environment_path] = environment
666
+
667
+ return environment
668
+
669
+ def sys_path(
670
+ self,
671
+ environment_path=None,
672
+ env_vars=None,
673
+ prioritize_extra_paths=False,
674
+ extra_paths=[],
675
+ ):
676
+ # Copy our extra sys path
677
+ path = list(self._extra_sys_path)
678
+ environment = self.get_enviroment(
679
+ environment_path=environment_path, env_vars=env_vars
680
+ )
681
+ path.extend(environment.get_sys_path())
682
+ if prioritize_extra_paths:
683
+ path = extra_paths + path
684
+ else:
685
+ path = path + extra_paths
686
+
687
+ return path
688
+
689
+
690
+ class Notebook:
691
+ """Represents a notebook."""
692
+
693
+ def __init__(
694
+ self, uri, notebook_type, workspace, cells=None, version=None, metadata=None
695
+ ) -> None:
696
+ self.uri = uri
697
+ self.notebook_type = notebook_type
698
+ self.workspace = workspace
699
+ self.version = version
700
+ self.cells = cells or []
701
+ self.metadata = metadata or {}
702
+ self._lock = RLock()
703
+
704
+ def __str__(self):
705
+ return "Notebook with URI '%s'" % str(self.uri)
706
+
707
+ def add_cells(self, new_cells: list, start: int) -> None:
708
+ self.cells[start:start] = new_cells
709
+
710
+ def remove_cells(self, start: int, delete_count: int) -> None:
711
+ del self.cells[start : start + delete_count]
712
+
713
+ def cell_data(self):
714
+ """Extract current cell data.
715
+
716
+ Returns a dict (ordered by cell position) where the key is the cell uri and the
717
+ value is a dict with line_start, line_end, and source attributes.
718
+ """
719
+ cell_data = {}
720
+ offset = 0
721
+ for cell in self.cells:
722
+ cell_uri = cell["document"]
723
+ cell_document = self.workspace.get_cell_document(cell_uri)
724
+ num_lines = cell_document.line_count
725
+ cell_data[cell_uri] = {
726
+ "line_start": offset,
727
+ "line_end": offset + num_lines - 1,
728
+ "source": cell_document.source,
729
+ }
730
+ offset += num_lines
731
+ return cell_data
732
+
733
+ @lock
734
+ def jedi_names(
735
+ self,
736
+ up_to_cell_uri: Optional[str] = None,
737
+ all_scopes=False,
738
+ definitions=True,
739
+ references=False,
740
+ ):
741
+ """
742
+ Get the names in the notebook up to a certain cell.
743
+
744
+ Parameters
745
+ ----------
746
+ up_to_cell_uri: str, optional
747
+ The cell uri to stop at. If None, all cells are considered.
748
+ """
749
+ names = set()
750
+ for cell in self.cells:
751
+ cell_uri = cell["document"]
752
+ cell_document = self.workspace.get_cell_document(cell_uri)
753
+ names.update(cell_document.jedi_names(all_scopes, definitions, references))
754
+ if cell_uri == up_to_cell_uri:
755
+ break
756
+ return {name.name for name in names}
757
+
758
+
759
+ class Cell(Document):
760
+ """
761
+ Represents a cell in a notebook.
762
+
763
+ Notes
764
+ -----
765
+ We inherit from Document for now to get the same API. However, a cell document differs from text documents in that
766
+ they have a language id.
767
+ """
768
+
769
+ def __init__(
770
+ self,
771
+ uri,
772
+ notebook_uri,
773
+ language_id,
774
+ workspace,
775
+ source=None,
776
+ version=None,
777
+ local=True,
778
+ extra_sys_path=None,
779
+ rope_project_builder=None,
780
+ ) -> None:
781
+ super().__init__(
782
+ uri, workspace, source, version, local, extra_sys_path, rope_project_builder
783
+ )
784
+ self.language_id = language_id
785
+ self.notebook_uri = notebook_uri
786
+
787
+ @property
788
+ @lock
789
+ def line_count(self):
790
+ """ "Return the number of lines in the cell document."""
791
+ return len(self.source.split("\n"))