robotcode-robot 0.68.3__py3-none-any.whl → 0.68.5__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1499 @@
1
+ import ast
2
+ import itertools
3
+ import multiprocessing as mp
4
+ import os
5
+ import shutil
6
+ import sys
7
+ import threading
8
+ import weakref
9
+ import zlib
10
+ from abc import ABC, abstractmethod
11
+ from collections import OrderedDict
12
+ from concurrent.futures import ProcessPoolExecutor
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import (
16
+ TYPE_CHECKING,
17
+ Any,
18
+ Callable,
19
+ Dict,
20
+ List,
21
+ Mapping,
22
+ Optional,
23
+ Set,
24
+ Tuple,
25
+ final,
26
+ )
27
+
28
+ from robotcode.core.concurrent import run_as_task
29
+ from robotcode.core.documents_manager import DocumentsManager
30
+ from robotcode.core.event import event
31
+ from robotcode.core.filewatcher import FileWatcherEntry, FileWatcherManagerBase, FileWatcherManagerDummy
32
+ from robotcode.core.language import language_id
33
+ from robotcode.core.lsp.types import DocumentUri, FileChangeType, FileEvent
34
+ from robotcode.core.text_document import TextDocument
35
+ from robotcode.core.uri import Uri
36
+ from robotcode.core.utils.caching import SimpleLRUCache
37
+ from robotcode.core.utils.dataclasses import as_json, from_json
38
+ from robotcode.core.utils.glob_path import Pattern, iter_files
39
+ from robotcode.core.utils.logging import LoggingDescriptor
40
+ from robotcode.core.utils.path import path_is_relative_to
41
+
42
+ from ..__version__ import __version__
43
+ from ..utils import get_robot_version, get_robot_version_str
44
+ from ..utils.robot_path import find_file_ex
45
+ from .entities import (
46
+ CommandLineVariableDefinition,
47
+ VariableDefinition,
48
+ )
49
+ from .library_doc import (
50
+ ROBOT_LIBRARY_PACKAGE,
51
+ CompleteResult,
52
+ LibraryDoc,
53
+ ModuleSpec,
54
+ VariablesDoc,
55
+ complete_library_import,
56
+ complete_resource_import,
57
+ complete_variables_import,
58
+ find_file,
59
+ find_library,
60
+ find_variables,
61
+ get_library_doc,
62
+ get_model_doc,
63
+ get_module_spec,
64
+ get_variables_doc,
65
+ is_library_by_path,
66
+ is_variables_by_path,
67
+ resolve_args,
68
+ resolve_variable,
69
+ )
70
+
71
+ if TYPE_CHECKING:
72
+ from .document_cache_helper import DocumentsCacheHelper
73
+ from .namespace import Namespace
74
+
75
+
76
+ RESOURCE_EXTENSIONS = (
77
+ {".resource", ".robot", ".txt", ".tsv", ".rst", ".rest", ".json", ".rsrc"}
78
+ if get_robot_version() >= (6, 1)
79
+ else {".resource", ".robot", ".txt", ".tsv", ".rst", ".rest"}
80
+ )
81
+ REST_EXTENSIONS = (".rst", ".rest")
82
+
83
+
84
+ LOAD_LIBRARY_TIME_OUT = 30
85
+ FIND_FILE_TIME_OUT = 10
86
+ COMPLETE_LIBRARY_IMPORT_TIME_OUT = COMPLETE_RESOURCE_IMPORT_TIME_OUT = COMPLETE_VARIABLES_IMPORT_TIME_OUT = 10
87
+
88
+
89
+ class _EntryKey:
90
+ pass
91
+
92
+
93
+ @dataclass()
94
+ class _LibrariesEntryKey(_EntryKey):
95
+ name: str
96
+ args: Tuple[Any, ...]
97
+
98
+ def __hash__(self) -> int:
99
+ return hash((self.name, self.args))
100
+
101
+
102
+ class _ImportEntry(ABC):
103
+ def __init__(self, parent: "ImportsManager") -> None:
104
+ self.parent = parent
105
+ self.references: weakref.WeakSet[Any] = weakref.WeakSet()
106
+ self.file_watchers: List[FileWatcherEntry] = []
107
+ self._lock = threading.RLock()
108
+
109
+ def __del__(self) -> None:
110
+ try:
111
+ self._remove_file_watcher()
112
+ except RuntimeError:
113
+ pass
114
+
115
+ def _remove_file_watcher(self) -> None:
116
+ if self.file_watchers:
117
+ for watcher in self.file_watchers:
118
+ self.parent.file_watcher_manager.remove_file_watcher_entry(watcher)
119
+ self.file_watchers = []
120
+
121
+ @abstractmethod
122
+ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeType]:
123
+ ...
124
+
125
+ @final
126
+ def invalidate(self) -> None:
127
+ with self._lock:
128
+ self._invalidate()
129
+
130
+ @abstractmethod
131
+ def _invalidate(self) -> None:
132
+ ...
133
+
134
+ @abstractmethod
135
+ def _update(self) -> None:
136
+ ...
137
+
138
+ @abstractmethod
139
+ def is_valid(self) -> bool:
140
+ ...
141
+
142
+
143
+ class _LibrariesEntry(_ImportEntry):
144
+ def __init__(
145
+ self,
146
+ parent: "ImportsManager",
147
+ name: str,
148
+ args: Tuple[Any, ...],
149
+ working_dir: str,
150
+ base_dir: str,
151
+ get_libdoc_coroutine: Callable[[str, Tuple[Any, ...], str, str], LibraryDoc],
152
+ ignore_reference: bool = False,
153
+ ) -> None:
154
+ super().__init__(parent)
155
+ self.name = name
156
+ self.args = args
157
+ self.working_dir = working_dir
158
+ self.base_dir = base_dir
159
+ self._get_libdoc_coroutine = get_libdoc_coroutine
160
+ self._lib_doc: Optional[LibraryDoc] = None
161
+ self.ignore_reference = ignore_reference
162
+
163
+ def __repr__(self) -> str:
164
+ return (
165
+ f"{type(self).__qualname__}(name={self.name!r}, "
166
+ f"args={self.args!r}, file_watchers={self.file_watchers!r}, id={id(self)!r}"
167
+ )
168
+
169
+ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeType]:
170
+ with self._lock:
171
+ if self._lib_doc is None:
172
+ return None
173
+
174
+ for change in changes:
175
+ uri = Uri(change.uri)
176
+ if uri.scheme != "file":
177
+ continue
178
+
179
+ path = uri.to_path()
180
+ if self._lib_doc is not None and (
181
+ (
182
+ self._lib_doc.module_spec is not None
183
+ and self._lib_doc.module_spec.submodule_search_locations is not None
184
+ and any(
185
+ path_is_relative_to(path, Path(e).resolve())
186
+ for e in self._lib_doc.module_spec.submodule_search_locations
187
+ )
188
+ )
189
+ or (
190
+ self._lib_doc.module_spec is not None
191
+ and self._lib_doc.module_spec.origin is not None
192
+ and path_is_relative_to(path, Path(self._lib_doc.module_spec.origin).parent)
193
+ )
194
+ or (self._lib_doc.source and path_is_relative_to(path, Path(self._lib_doc.source).parent))
195
+ or (
196
+ self._lib_doc.module_spec is None
197
+ and not self._lib_doc.source
198
+ and self._lib_doc.python_path
199
+ and any(path_is_relative_to(path, Path(e).resolve()) for e in self._lib_doc.python_path)
200
+ )
201
+ ):
202
+ self._invalidate()
203
+
204
+ return change.type
205
+
206
+ return None
207
+
208
+ def _update(self) -> None:
209
+ self._lib_doc = self._get_libdoc_coroutine(self.name, self.args, self.working_dir, self.base_dir)
210
+
211
+ source_or_origin = (
212
+ self._lib_doc.source
213
+ if self._lib_doc.source is not None
214
+ else self._lib_doc.module_spec.origin
215
+ if self._lib_doc.module_spec is not None
216
+ else None
217
+ )
218
+
219
+ # we are a module, so add the module path into file watchers
220
+ if self._lib_doc.module_spec is not None and self._lib_doc.module_spec.submodule_search_locations is not None:
221
+ self.file_watchers.append(
222
+ self.parent.file_watcher_manager.add_file_watchers(
223
+ self.parent.did_change_watched_files,
224
+ [
225
+ str(Path(location).resolve().joinpath("**"))
226
+ for location in self._lib_doc.module_spec.submodule_search_locations
227
+ ],
228
+ )
229
+ )
230
+
231
+ if source_or_origin is not None and Path(source_or_origin).parent in [
232
+ Path(loc).resolve() for loc in self._lib_doc.module_spec.submodule_search_locations
233
+ ]:
234
+ return
235
+
236
+ # we are a file, so put the parent path to filewatchers
237
+ if source_or_origin is not None:
238
+ self.file_watchers.append(
239
+ self.parent.file_watcher_manager.add_file_watchers(
240
+ self.parent.did_change_watched_files,
241
+ [str(Path(source_or_origin).parent.joinpath("**"))],
242
+ )
243
+ )
244
+
245
+ return
246
+
247
+ # we are not found, so put the pythonpath to filewatchers
248
+ if self._lib_doc.python_path is not None:
249
+ self.file_watchers.append(
250
+ self.parent.file_watcher_manager.add_file_watchers(
251
+ self.parent.did_change_watched_files,
252
+ [str(Path(s).joinpath("**")) for s in self._lib_doc.python_path],
253
+ )
254
+ )
255
+
256
+ def _invalidate(self) -> None:
257
+ if self._lib_doc is None and len(self.file_watchers) == 0:
258
+ return
259
+
260
+ self._remove_file_watcher()
261
+ self._lib_doc = None
262
+
263
+ def is_valid(self) -> bool:
264
+ with self._lock:
265
+ return self._lib_doc is not None
266
+
267
+ def get_libdoc(self) -> LibraryDoc:
268
+ with self._lock:
269
+ if self._lib_doc is None:
270
+ self._update()
271
+
272
+ assert self._lib_doc is not None
273
+
274
+ return self._lib_doc
275
+
276
+
277
+ @dataclass()
278
+ class _ResourcesEntryKey(_EntryKey):
279
+ name: str
280
+
281
+ def __hash__(self) -> int:
282
+ return hash(self.name)
283
+
284
+
285
+ class _ResourcesEntry(_ImportEntry):
286
+ def __init__(
287
+ self,
288
+ name: str,
289
+ parent: "ImportsManager",
290
+ get_document_callback: Callable[[], TextDocument],
291
+ ) -> None:
292
+ super().__init__(parent)
293
+ self.name = name
294
+ self._get_document_callback = get_document_callback
295
+ self._document: Optional[TextDocument] = None
296
+ self._lib_doc: Optional[LibraryDoc] = None
297
+
298
+ def __repr__(self) -> str:
299
+ return f"{type(self).__qualname__}(name={self.name!r}, file_watchers={self.file_watchers!r}, id={id(self)!r}"
300
+
301
+ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeType]:
302
+ with self._lock:
303
+ for change in changes:
304
+ uri = Uri(change.uri)
305
+ if uri.scheme != "file":
306
+ continue
307
+
308
+ path = uri.to_path()
309
+ if (
310
+ self._document is not None
311
+ and (path.resolve() == self._document.uri.to_path().resolve())
312
+ or self._document is None
313
+ ):
314
+ self._invalidate()
315
+
316
+ return change.type
317
+
318
+ return None
319
+
320
+ def _update(self) -> None:
321
+ self._document = self._get_document_callback()
322
+
323
+ if self._document._version is None:
324
+ self.file_watchers.append(
325
+ self.parent.file_watcher_manager.add_file_watchers(
326
+ self.parent.did_change_watched_files,
327
+ [str(self._document.uri.to_path())],
328
+ )
329
+ )
330
+
331
+ def _invalidate(self) -> None:
332
+ if self._document is None and len(self.file_watchers) == 0:
333
+ return
334
+
335
+ self._remove_file_watcher()
336
+
337
+ self._document = None
338
+ self._lib_doc = None
339
+
340
+ def is_valid(self) -> bool:
341
+ with self._lock:
342
+ return self._document is not None
343
+
344
+ def get_document(self) -> TextDocument:
345
+ with self._lock:
346
+ self._get_document()
347
+
348
+ assert self._document is not None
349
+
350
+ return self._document
351
+
352
+ def _get_document(self) -> TextDocument:
353
+ if self._document is None:
354
+ self._update()
355
+
356
+ assert self._document is not None
357
+
358
+ return self._document
359
+
360
+ def get_namespace(self) -> "Namespace":
361
+ with self._lock:
362
+ return self._get_namespace()
363
+
364
+ def _get_namespace(self) -> "Namespace":
365
+ return self.parent.get_namespace_for_resource(self._get_document())
366
+
367
+ def get_libdoc(self) -> LibraryDoc:
368
+ with self._lock:
369
+ if self._lib_doc is None:
370
+ self._lib_doc = self._get_namespace().get_library_doc()
371
+
372
+ return self._lib_doc
373
+
374
+
375
+ @dataclass()
376
+ class _VariablesEntryKey(_EntryKey):
377
+ name: str
378
+ args: Tuple[Any, ...]
379
+
380
+ def __hash__(self) -> int:
381
+ return hash((self.name, self.args))
382
+
383
+
384
+ class _VariablesEntry(_ImportEntry):
385
+ def __init__(
386
+ self,
387
+ name: str,
388
+ args: Tuple[Any, ...],
389
+ working_dir: str,
390
+ base_dir: str,
391
+ parent: "ImportsManager",
392
+ get_variables_doc_coroutine: Callable[[str, Tuple[Any, ...], str, str], VariablesDoc],
393
+ ) -> None:
394
+ super().__init__(parent)
395
+ self.name = name
396
+ self.args = args
397
+ self.working_dir = working_dir
398
+ self.base_dir = base_dir
399
+ self._get_variables_doc_coroutine = get_variables_doc_coroutine
400
+ self._lib_doc: Optional[VariablesDoc] = None
401
+
402
+ def __repr__(self) -> str:
403
+ return (
404
+ f"{type(self).__qualname__}(name={self.name!r}, "
405
+ f"args={self.args!r}, file_watchers={self.file_watchers!r}, id={id(self)!r}"
406
+ )
407
+
408
+ def check_file_changed(self, changes: List[FileEvent]) -> Optional[FileChangeType]:
409
+ with self._lock:
410
+ if self._lib_doc is None:
411
+ return None
412
+
413
+ for change in changes:
414
+ uri = Uri(change.uri)
415
+ if uri.scheme != "file":
416
+ continue
417
+
418
+ path = uri.to_path()
419
+ if self._lib_doc.source and path.exists() and path.samefile(Path(self._lib_doc.source)):
420
+ self._invalidate()
421
+
422
+ return change.type
423
+
424
+ return None
425
+
426
+ def _update(self) -> None:
427
+ self._lib_doc = self._get_variables_doc_coroutine(self.name, self.args, self.working_dir, self.base_dir)
428
+
429
+ if self._lib_doc is not None:
430
+ self.file_watchers.append(
431
+ self.parent.file_watcher_manager.add_file_watchers(
432
+ self.parent.did_change_watched_files,
433
+ [str(self._lib_doc.source)],
434
+ )
435
+ )
436
+
437
+ def _invalidate(self) -> None:
438
+ if self._lib_doc is None and len(self.file_watchers) == 0:
439
+ return
440
+
441
+ self._remove_file_watcher()
442
+
443
+ self._lib_doc = None
444
+
445
+ def is_valid(self) -> bool:
446
+ with self._lock:
447
+ return self._lib_doc is not None
448
+
449
+ def get_libdoc(self) -> VariablesDoc:
450
+ with self._lock:
451
+ if self._lib_doc is None:
452
+ self._update()
453
+
454
+ assert self._lib_doc is not None
455
+
456
+ return self._lib_doc
457
+
458
+
459
+ @dataclass
460
+ class LibraryMetaData:
461
+ meta_version: str
462
+ name: Optional[str]
463
+ member_name: Optional[str]
464
+ origin: Optional[str]
465
+ submodule_search_locations: Optional[List[str]]
466
+ by_path: bool
467
+
468
+ mtimes: Optional[Dict[str, int]] = None
469
+
470
+ @property
471
+ def filepath_base(self) -> str:
472
+ if self.by_path:
473
+ if self.origin is not None:
474
+ p = Path(self.origin)
475
+
476
+ return f"{zlib.adler32(str(p.parent).encode('utf-8')):08x}_{p.stem}"
477
+ else:
478
+ if self.name is not None:
479
+ return self.name.replace(".", "/") + (f".{self.member_name}" if self.member_name else "")
480
+
481
+ raise ValueError("Cannot determine filepath base.")
482
+
483
+
484
+ class ImportsManager:
485
+ _logger = LoggingDescriptor()
486
+
487
+ def __init__(
488
+ self,
489
+ documents_manager: DocumentsManager,
490
+ file_watcher_manager: Optional[FileWatcherManagerBase],
491
+ document_cache_helper: "DocumentsCacheHelper",
492
+ root_folder: Path,
493
+ variables: Dict[str, str],
494
+ variable_files: List[str],
495
+ environment: Optional[Dict[str, str]],
496
+ ignored_libraries: List[str],
497
+ ignored_variables: List[str],
498
+ cache_base_path: Optional[Path],
499
+ ) -> None:
500
+ super().__init__()
501
+
502
+ self.documents_manager = documents_manager
503
+ self.documents_manager.did_create_uri.add(self.possible_imports_modified)
504
+ self.documents_manager.did_change.add(self.possible_resource_document_modified)
505
+
506
+ self.file_watcher_manager: FileWatcherManagerBase = (
507
+ file_watcher_manager if file_watcher_manager is not None else FileWatcherManagerDummy()
508
+ )
509
+
510
+ self.document_cache_helper = document_cache_helper
511
+
512
+ self.root_folder = root_folder
513
+
514
+ if cache_base_path is None:
515
+ cache_base_path = root_folder
516
+
517
+ self._logger.trace(lambda: f"use {cache_base_path} as base for caching")
518
+
519
+ self.cache_path = cache_base_path / ".robotcode_cache"
520
+
521
+ self.lib_doc_cache_path = (
522
+ self.cache_path
523
+ / f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
524
+ / get_robot_version_str()
525
+ / "libdoc"
526
+ )
527
+ self.variables_doc_cache_path = (
528
+ self.cache_path
529
+ / f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
530
+ / get_robot_version_str()
531
+ / "variables"
532
+ )
533
+
534
+ self.cmd_variables = variables
535
+ self.cmd_variable_files = variable_files
536
+
537
+ self.ignored_libraries_patters = [Pattern(s) for s in ignored_libraries]
538
+ self.ignored_variables_patters = [Pattern(s) for s in ignored_variables]
539
+ self._libaries_lock = threading.RLock()
540
+ self._libaries: OrderedDict[_LibrariesEntryKey, _LibrariesEntry] = OrderedDict()
541
+ self._resources_lock = threading.RLock()
542
+ self._resources: OrderedDict[_ResourcesEntryKey, _ResourcesEntry] = OrderedDict()
543
+ self._variables_lock = threading.RLock()
544
+ self._variables: OrderedDict[_VariablesEntryKey, _VariablesEntry] = OrderedDict()
545
+ self.file_watchers: List[FileWatcherEntry] = []
546
+ self._command_line_variables: Optional[List[VariableDefinition]] = None
547
+ self._command_line_variables_lock = threading.RLock()
548
+ self._resolvable_command_line_variables: Optional[Dict[str, Any]] = None
549
+ self._resolvable_command_line_variables_lock = threading.RLock()
550
+
551
+ self._environment = dict(os.environ)
552
+ if environment:
553
+ self._environment.update(environment)
554
+
555
+ self._library_files_cache = SimpleLRUCache()
556
+ self._resource_files_cache = SimpleLRUCache()
557
+ self._variables_files_cache = SimpleLRUCache()
558
+
559
+ self._executor_lock = threading.RLock()
560
+ self._executor: Optional[ProcessPoolExecutor] = None
561
+
562
+ self._resource_document_changed_timer_lock = threading.RLock()
563
+ self._resource_document_changed_timer: Optional[threading.Timer] = None
564
+ self._resource_document_changed_timer_interval = 1
565
+ self._resource_document_changed_documents: Set[TextDocument] = set()
566
+
567
+ def __del__(self) -> None:
568
+ try:
569
+ if self._executor is not None:
570
+ self._executor.shutdown(wait=False)
571
+ except RuntimeError:
572
+ pass
573
+
574
+ @property
575
+ def environment(self) -> Mapping[str, str]:
576
+ return self._environment
577
+
578
+ def get_namespace_for_resource(self, document: TextDocument) -> "Namespace":
579
+ return self.document_cache_helper.get_resource_namespace(document)
580
+
581
+ def clear_cache(self) -> None:
582
+ if self.cache_path.exists():
583
+ shutil.rmtree(self.cache_path)
584
+ self._logger.debug(lambda: f"Cleared cache {self.cache_path}")
585
+
586
+ @_logger.call
587
+ def get_command_line_variables(self) -> List[VariableDefinition]:
588
+ from robot.utils.text import split_args_from_name_or_path
589
+
590
+ with self._command_line_variables_lock:
591
+ if self._command_line_variables is None:
592
+ command_line_vars: List[VariableDefinition] = []
593
+
594
+ command_line_vars += [
595
+ CommandLineVariableDefinition(
596
+ 0,
597
+ 0,
598
+ 0,
599
+ 0,
600
+ "",
601
+ f"${{{k}}}",
602
+ None,
603
+ has_value=True,
604
+ value=v,
605
+ )
606
+ for k, v in self.cmd_variables.items()
607
+ ]
608
+
609
+ for variable_file in self.cmd_variable_files:
610
+ name, args = split_args_from_name_or_path(str(variable_file))
611
+ try:
612
+ lib_doc = self.get_libdoc_for_variables_import(
613
+ name.replace("\\", "\\\\"),
614
+ tuple(args),
615
+ str(self.root_folder),
616
+ self,
617
+ resolve_variables=False,
618
+ resolve_command_line_vars=False,
619
+ )
620
+ if lib_doc is not None:
621
+ command_line_vars += [
622
+ CommandLineVariableDefinition(
623
+ line_no=e.line_no,
624
+ col_offset=e.col_offset,
625
+ end_line_no=e.end_line_no,
626
+ end_col_offset=e.end_col_offset,
627
+ source=e.source,
628
+ name=e.name,
629
+ name_token=e.name_token,
630
+ has_value=e.has_value,
631
+ resolvable=e.resolvable,
632
+ value=e.value,
633
+ value_is_native=e.value_is_native,
634
+ )
635
+ for e in lib_doc.variables
636
+ ]
637
+
638
+ if lib_doc.errors:
639
+ # TODO add diagnostics
640
+ for error in lib_doc.errors:
641
+ self._logger.error(
642
+ lambda: f"{error.type_name}: {error.message} in {error.source}:{error.line_no}"
643
+ )
644
+ except (SystemExit, KeyboardInterrupt):
645
+ raise
646
+ except BaseException as e:
647
+ # TODO add diagnostics
648
+ self._logger.exception(e)
649
+
650
+ self._command_line_variables = command_line_vars
651
+
652
+ return self._command_line_variables or []
653
+
654
+ def get_resolvable_command_line_variables(self) -> Dict[str, Any]:
655
+ with self._resolvable_command_line_variables_lock:
656
+ if self._resolvable_command_line_variables is None:
657
+ self._resolvable_command_line_variables = {
658
+ v.name: v.value for v in (self.get_command_line_variables()) if v.has_value
659
+ }
660
+
661
+ return self._resolvable_command_line_variables
662
+
663
+ @event
664
+ def libraries_changed(sender, libraries: List[LibraryDoc]) -> None:
665
+ ...
666
+
667
+ @event
668
+ def resources_changed(sender, resources: List[LibraryDoc]) -> None:
669
+ ...
670
+
671
+ @event
672
+ def variables_changed(sender, variables: List[LibraryDoc]) -> None:
673
+ ...
674
+
675
+ @event
676
+ def imports_changed(sender, uri: DocumentUri) -> None:
677
+ ...
678
+
679
+ def possible_imports_modified(self, sender: Any, uri: DocumentUri) -> None:
680
+ self.imports_changed(self, uri)
681
+
682
+ @language_id("robotframework")
683
+ def possible_resource_document_modified(self, sender: Any, document: TextDocument) -> None:
684
+ with self._resource_document_changed_timer_lock:
685
+ if document in self._resource_document_changed_documents:
686
+ return
687
+
688
+ if self._resource_document_changed_timer is not None:
689
+ self._resource_document_changed_timer.cancel()
690
+ self._resource_document_changed_timer = None
691
+
692
+ self._resource_document_changed_documents.add(document)
693
+
694
+ self._resource_document_changed_timer = threading.Timer(
695
+ self._resource_document_changed_timer_interval, self.__resource_documents_changed
696
+ )
697
+ self._resource_document_changed_timer.start()
698
+
699
+ def __resource_documents_changed(self) -> None:
700
+ with self._resource_document_changed_timer_lock:
701
+ self._resource_document_changed_timer = None
702
+
703
+ documents = self._resource_document_changed_documents
704
+ self._resource_document_changed_documents = set()
705
+
706
+ for document in documents:
707
+ run_as_task(self.__resource_document_changed, document).result()
708
+
709
+ def __resource_document_changed(self, document: TextDocument) -> None:
710
+ resource_changed: List[LibraryDoc] = []
711
+
712
+ with self._resources_lock:
713
+ for r_entry in self._resources.values():
714
+ lib_doc: Optional[LibraryDoc] = None
715
+ try:
716
+ if not r_entry.is_valid():
717
+ continue
718
+
719
+ uri = r_entry.get_document().uri
720
+ result = uri == document.uri
721
+ if result:
722
+ lib_doc = r_entry.get_libdoc()
723
+ r_entry.invalidate()
724
+
725
+ except (SystemExit, KeyboardInterrupt):
726
+ raise
727
+ except BaseException:
728
+ result = True
729
+
730
+ if result and lib_doc is not None:
731
+ resource_changed.append(lib_doc)
732
+
733
+ if resource_changed:
734
+ self.resources_changed(self, resource_changed)
735
+
736
+ @_logger.call
737
+ def did_change_watched_files(self, sender: Any, changes: List[FileEvent]) -> None:
738
+ libraries_changed: List[Tuple[_LibrariesEntryKey, FileChangeType, Optional[LibraryDoc]]] = []
739
+ resource_changed: List[Tuple[_ResourcesEntryKey, FileChangeType, Optional[LibraryDoc]]] = []
740
+ variables_changed: List[Tuple[_VariablesEntryKey, FileChangeType, Optional[LibraryDoc]]] = []
741
+
742
+ lib_doc: Optional[LibraryDoc]
743
+
744
+ with self._libaries_lock:
745
+ for l_key, l_entry in self._libaries.items():
746
+ lib_doc = None
747
+ if l_entry.is_valid():
748
+ lib_doc = l_entry.get_libdoc()
749
+ result = l_entry.check_file_changed(changes)
750
+ if result is not None:
751
+ libraries_changed.append((l_key, result, lib_doc))
752
+
753
+ try:
754
+ with self._resources_lock:
755
+ for r_key, r_entry in self._resources.items():
756
+ lib_doc = None
757
+ if r_entry.is_valid():
758
+ lib_doc = r_entry.get_libdoc()
759
+ result = r_entry.check_file_changed(changes)
760
+ if result is not None:
761
+ resource_changed.append((r_key, result, lib_doc))
762
+ except BaseException as e:
763
+ self._logger.exception(e)
764
+ raise
765
+
766
+ with self._variables_lock:
767
+ for v_key, v_entry in self._variables.items():
768
+ lib_doc = None
769
+ if v_entry.is_valid():
770
+ lib_doc = v_entry.get_libdoc()
771
+ result = v_entry.check_file_changed(changes)
772
+ if result is not None:
773
+ variables_changed.append((v_key, result, lib_doc))
774
+
775
+ if libraries_changed:
776
+ for l, t, _ in libraries_changed:
777
+ if t == FileChangeType.DELETED:
778
+ self.__remove_library_entry(l, self._libaries[l], True)
779
+
780
+ self.libraries_changed(self, [v for (_, _, v) in libraries_changed if v is not None])
781
+
782
+ if resource_changed:
783
+ for r, t, _ in resource_changed:
784
+ if t == FileChangeType.DELETED:
785
+ self.__remove_resource_entry(r, self._resources[r], True)
786
+
787
+ self.resources_changed(self, [v for (_, _, v) in resource_changed if v is not None])
788
+
789
+ if variables_changed:
790
+ for v, t, _ in variables_changed:
791
+ if t == FileChangeType.DELETED:
792
+ self.__remove_variables_entry(v, self._variables[v], True)
793
+
794
+ self.variables_changed(self, [v for (_, _, v) in variables_changed if v is not None])
795
+
796
+ def __remove_library_entry(
797
+ self,
798
+ entry_key: _LibrariesEntryKey,
799
+ entry: _LibrariesEntry,
800
+ now: bool = False,
801
+ ) -> None:
802
+ try:
803
+ if len(entry.references) == 0 or now:
804
+ self._logger.debug(lambda: f"Remove Library Entry {entry_key}")
805
+ with self._libaries_lock:
806
+ if len(entry.references) == 0:
807
+ e1 = self._libaries.get(entry_key, None)
808
+ if e1 == entry:
809
+ self._libaries.pop(entry_key, None)
810
+
811
+ entry.invalidate()
812
+ self._logger.debug(lambda: f"Library Entry {entry_key} removed")
813
+ finally:
814
+ self._library_files_cache.clear()
815
+
816
+ def __remove_resource_entry(
817
+ self,
818
+ entry_key: _ResourcesEntryKey,
819
+ entry: _ResourcesEntry,
820
+ now: bool = False,
821
+ ) -> None:
822
+ try:
823
+ if len(entry.references) == 0 or now:
824
+ self._logger.debug(lambda: f"Remove Resource Entry {entry_key}")
825
+ with self._resources_lock:
826
+ if len(entry.references) == 0 or now:
827
+ e1 = self._resources.get(entry_key, None)
828
+ if e1 == entry:
829
+ self._resources.pop(entry_key, None)
830
+
831
+ entry.invalidate()
832
+ self._logger.debug(lambda: f"Resource Entry {entry_key} removed")
833
+ finally:
834
+ self._resource_files_cache.clear()
835
+
836
+ def __remove_variables_entry(
837
+ self,
838
+ entry_key: _VariablesEntryKey,
839
+ entry: _VariablesEntry,
840
+ now: bool = False,
841
+ ) -> None:
842
+ try:
843
+ if len(entry.references) == 0 or now:
844
+ self._logger.debug(lambda: f"Remove Variables Entry {entry_key}")
845
+ with self._variables_lock:
846
+ if len(entry.references) == 0:
847
+ e1 = self._variables.get(entry_key, None)
848
+ if e1 == entry:
849
+ self._variables.pop(entry_key, None)
850
+
851
+ entry.invalidate()
852
+ self._logger.debug(lambda: f"Variables Entry {entry_key} removed")
853
+ finally:
854
+ self._variables_files_cache.clear()
855
+
856
+ def get_library_meta(
857
+ self,
858
+ name: str,
859
+ base_dir: str = ".",
860
+ variables: Optional[Dict[str, Optional[Any]]] = None,
861
+ ) -> Tuple[Optional[LibraryMetaData], str]:
862
+ try:
863
+ import_name = self.find_library(name, base_dir=base_dir, variables=variables)
864
+
865
+ result: Optional[LibraryMetaData] = None
866
+ module_spec: Optional[ModuleSpec] = None
867
+ if is_library_by_path(import_name):
868
+ if (p := Path(import_name)).exists():
869
+ result = LibraryMetaData(__version__, p.stem, None, import_name, None, True)
870
+ else:
871
+ module_spec = get_module_spec(import_name)
872
+ if module_spec is not None and module_spec.origin is not None:
873
+ result = LibraryMetaData(
874
+ __version__,
875
+ module_spec.name,
876
+ module_spec.member_name,
877
+ module_spec.origin,
878
+ module_spec.submodule_search_locations,
879
+ False,
880
+ )
881
+
882
+ if result is not None:
883
+ if any(
884
+ (p.matches(result.name) if result.name is not None else False)
885
+ or (p.matches(result.origin) if result.origin is not None else False)
886
+ for p in self.ignored_libraries_patters
887
+ ):
888
+ self._logger.debug(
889
+ lambda: f"Ignore library {result.name or '' if result is not None else ''}"
890
+ f" {result.origin or '' if result is not None else ''} for caching."
891
+ )
892
+ return None, import_name
893
+
894
+ if result.origin is not None:
895
+ result.mtimes = {result.origin: Path(result.origin).stat().st_mtime_ns}
896
+
897
+ if result.submodule_search_locations:
898
+ if result.mtimes is None:
899
+ result.mtimes = {}
900
+ result.mtimes.update(
901
+ {
902
+ str(f): f.stat().st_mtime_ns
903
+ for f in itertools.chain(
904
+ *(iter_files(loc, "**/*.py") for loc in result.submodule_search_locations)
905
+ )
906
+ }
907
+ )
908
+
909
+ return result, import_name
910
+ except (SystemExit, KeyboardInterrupt):
911
+ raise
912
+ except BaseException:
913
+ pass
914
+
915
+ return None, import_name
916
+
917
+ def get_variables_meta(
918
+ self,
919
+ name: str,
920
+ base_dir: str = ".",
921
+ variables: Optional[Dict[str, Optional[Any]]] = None,
922
+ resolve_variables: bool = True,
923
+ resolve_command_line_vars: bool = True,
924
+ ) -> Tuple[Optional[LibraryMetaData], str]:
925
+ try:
926
+ import_name = self.find_variables(
927
+ name,
928
+ base_dir=base_dir,
929
+ variables=variables,
930
+ resolve_variables=resolve_variables,
931
+ resolve_command_line_vars=resolve_command_line_vars,
932
+ )
933
+
934
+ result: Optional[LibraryMetaData] = None
935
+ module_spec: Optional[ModuleSpec] = None
936
+ if is_variables_by_path(import_name):
937
+ if (p := Path(import_name)).exists():
938
+ result = LibraryMetaData(__version__, p.stem, None, import_name, None, True)
939
+ else:
940
+ module_spec = get_module_spec(import_name)
941
+ if module_spec is not None and module_spec.origin is not None:
942
+ result = LibraryMetaData(
943
+ __version__,
944
+ module_spec.name,
945
+ module_spec.member_name,
946
+ module_spec.origin,
947
+ module_spec.submodule_search_locations,
948
+ False,
949
+ )
950
+
951
+ if result is not None:
952
+ if any(
953
+ (p.matches(result.name) if result.name is not None else False)
954
+ or (p.matches(result.origin) if result.origin is not None else False)
955
+ for p in self.ignored_variables_patters
956
+ ):
957
+ self._logger.debug(
958
+ lambda: f"Ignore Variables {result.name or '' if result is not None else ''}"
959
+ f" {result.origin or '' if result is not None else ''} for caching."
960
+ )
961
+ return None, import_name
962
+
963
+ if result.origin is not None:
964
+ result.mtimes = {result.origin: Path(result.origin).stat().st_mtime_ns}
965
+
966
+ if result.submodule_search_locations:
967
+ if result.mtimes is None:
968
+ result.mtimes = {}
969
+ result.mtimes.update(
970
+ {
971
+ str(f): f.stat().st_mtime_ns
972
+ for f in itertools.chain(
973
+ *(iter_files(loc, "**/*.py") for loc in result.submodule_search_locations)
974
+ )
975
+ }
976
+ )
977
+
978
+ return result, import_name
979
+ except (SystemExit, KeyboardInterrupt):
980
+ raise
981
+ except BaseException:
982
+ pass
983
+
984
+ return None, name
985
+
986
+ def find_library(
987
+ self,
988
+ name: str,
989
+ base_dir: str,
990
+ variables: Optional[Dict[str, Any]] = None,
991
+ ) -> str:
992
+ return self._library_files_cache.get(self._find_library, name, base_dir, variables)
993
+
994
+ def _find_library(
995
+ self,
996
+ name: str,
997
+ base_dir: str,
998
+ variables: Optional[Dict[str, Any]] = None,
999
+ ) -> str:
1000
+ from robot.libraries import STDLIBS
1001
+ from robot.variables.search import contains_variable
1002
+
1003
+ if contains_variable(name, "$@&%"):
1004
+ return find_library(
1005
+ name,
1006
+ str(self.root_folder),
1007
+ base_dir,
1008
+ self.get_resolvable_command_line_variables(),
1009
+ variables,
1010
+ )
1011
+
1012
+ if name in STDLIBS:
1013
+ result = ROBOT_LIBRARY_PACKAGE + "." + name
1014
+ else:
1015
+ result = name
1016
+
1017
+ if is_library_by_path(result):
1018
+ return find_file_ex(result, base_dir, "Library")
1019
+
1020
+ return result
1021
+
1022
+ def find_resource(
1023
+ self,
1024
+ name: str,
1025
+ base_dir: str,
1026
+ file_type: str = "Resource",
1027
+ variables: Optional[Dict[str, Any]] = None,
1028
+ ) -> str:
1029
+ return self._resource_files_cache.get(self.__find_resource, name, base_dir, file_type, variables)
1030
+
1031
+ @_logger.call
1032
+ def __find_resource(
1033
+ self,
1034
+ name: str,
1035
+ base_dir: str,
1036
+ file_type: str = "Resource",
1037
+ variables: Optional[Dict[str, Any]] = None,
1038
+ ) -> str:
1039
+ from robot.variables.search import contains_variable
1040
+
1041
+ if contains_variable(name, "$@&%"):
1042
+ return find_file(
1043
+ name,
1044
+ str(self.root_folder),
1045
+ base_dir,
1046
+ self.get_resolvable_command_line_variables(),
1047
+ variables,
1048
+ file_type,
1049
+ )
1050
+
1051
+ return str(find_file_ex(name, base_dir, file_type))
1052
+
1053
+ def find_variables(
1054
+ self,
1055
+ name: str,
1056
+ base_dir: str,
1057
+ variables: Optional[Dict[str, Any]] = None,
1058
+ resolve_variables: bool = True,
1059
+ resolve_command_line_vars: bool = True,
1060
+ ) -> str:
1061
+ return self._variables_files_cache.get(
1062
+ self.__find_variables,
1063
+ name,
1064
+ base_dir,
1065
+ variables,
1066
+ resolve_command_line_vars,
1067
+ )
1068
+
1069
+ @_logger.call
1070
+ def __find_variables(
1071
+ self,
1072
+ name: str,
1073
+ base_dir: str,
1074
+ variables: Optional[Dict[str, Any]] = None,
1075
+ resolve_variables: bool = True,
1076
+ resolve_command_line_vars: bool = True,
1077
+ ) -> str:
1078
+ from robot.variables.search import contains_variable
1079
+
1080
+ if resolve_variables and contains_variable(name, "$@&%"):
1081
+ return find_variables(
1082
+ name,
1083
+ str(self.root_folder),
1084
+ base_dir,
1085
+ self.get_resolvable_command_line_variables() if resolve_command_line_vars else None,
1086
+ variables,
1087
+ )
1088
+
1089
+ if get_robot_version() >= (5, 0):
1090
+ if is_variables_by_path(name):
1091
+ return str(find_file_ex(name, base_dir, "Variables"))
1092
+
1093
+ return name
1094
+
1095
+ return str(find_file_ex(name, base_dir, "Variables"))
1096
+
1097
+ @property
1098
+ def executor(self) -> ProcessPoolExecutor:
1099
+ with self._executor_lock:
1100
+ if self._executor is None:
1101
+ self._executor = ProcessPoolExecutor(mp_context=mp.get_context("spawn"))
1102
+
1103
+ return self._executor
1104
+
1105
+ @_logger.call
1106
+ def get_libdoc_for_library_import(
1107
+ self,
1108
+ name: str,
1109
+ args: Tuple[Any, ...],
1110
+ base_dir: str,
1111
+ sentinel: Any = None,
1112
+ variables: Optional[Dict[str, Any]] = None,
1113
+ ) -> LibraryDoc:
1114
+ source = self.find_library(name, base_dir, variables)
1115
+
1116
+ def _get_libdoc(name: str, args: Tuple[Any, ...], working_dir: str, base_dir: str) -> LibraryDoc:
1117
+ meta, source = self.get_library_meta(name, base_dir, variables)
1118
+
1119
+ self._logger.debug(lambda: f"Load Library {source}{args!r}")
1120
+
1121
+ if meta is not None:
1122
+ meta_file = Path(self.lib_doc_cache_path, meta.filepath_base + ".meta.json")
1123
+ if meta_file.exists():
1124
+ try:
1125
+ spec_path = None
1126
+ try:
1127
+ saved_meta = from_json(meta_file.read_text("utf-8"), LibraryMetaData)
1128
+ if saved_meta == meta:
1129
+ spec_path = Path(
1130
+ self.lib_doc_cache_path,
1131
+ meta.filepath_base + ".spec.json",
1132
+ )
1133
+ return from_json(spec_path.read_text("utf-8"), LibraryDoc)
1134
+ except (SystemExit, KeyboardInterrupt):
1135
+ raise
1136
+ except BaseException as e:
1137
+ raise RuntimeError(
1138
+ f"Failed to load library meta data for library {name} from {spec_path}"
1139
+ ) from e
1140
+ except (SystemExit, KeyboardInterrupt):
1141
+ raise
1142
+ except BaseException as e:
1143
+ self._logger.exception(e)
1144
+
1145
+ executor = ProcessPoolExecutor(max_workers=1, mp_context=mp.get_context("spawn"))
1146
+ try:
1147
+ result = executor.submit(
1148
+ get_library_doc,
1149
+ name,
1150
+ args,
1151
+ working_dir,
1152
+ base_dir,
1153
+ self.get_resolvable_command_line_variables(),
1154
+ variables,
1155
+ ).result(LOAD_LIBRARY_TIME_OUT)
1156
+
1157
+ except (SystemExit, KeyboardInterrupt):
1158
+ raise
1159
+ except BaseException as e:
1160
+ self._logger.exception(e)
1161
+ raise
1162
+ finally:
1163
+ executor.shutdown(wait=True)
1164
+
1165
+ if result.stdout:
1166
+ self._logger.warning(lambda: f"stdout captured at loading library {name}{args!r}:\n{result.stdout}")
1167
+ try:
1168
+ if meta is not None:
1169
+ meta_file = Path(
1170
+ self.lib_doc_cache_path,
1171
+ meta.filepath_base + ".meta.json",
1172
+ )
1173
+ spec_file = Path(
1174
+ self.lib_doc_cache_path,
1175
+ meta.filepath_base + ".spec.json",
1176
+ )
1177
+ spec_file.parent.mkdir(parents=True, exist_ok=True)
1178
+
1179
+ try:
1180
+ spec_file.write_text(as_json(result), "utf-8")
1181
+ except (SystemExit, KeyboardInterrupt):
1182
+ raise
1183
+ except BaseException as e:
1184
+ raise RuntimeError(f"Cannot write spec file for library '{name}' to '{spec_file}'") from e
1185
+
1186
+ meta_file.write_text(as_json(meta), "utf-8")
1187
+ else:
1188
+ self._logger.debug(lambda: f"Skip caching library {name}{args!r}")
1189
+ except (SystemExit, KeyboardInterrupt):
1190
+ raise
1191
+ except BaseException as e:
1192
+ self._logger.exception(e)
1193
+
1194
+ return result
1195
+
1196
+ resolved_args = resolve_args(
1197
+ args,
1198
+ str(self.root_folder),
1199
+ base_dir,
1200
+ self.get_resolvable_command_line_variables(),
1201
+ variables,
1202
+ )
1203
+ entry_key = _LibrariesEntryKey(source, resolved_args)
1204
+
1205
+ with self._libaries_lock:
1206
+ if entry_key not in self._libaries:
1207
+ self._libaries[entry_key] = _LibrariesEntry(
1208
+ self,
1209
+ name,
1210
+ args,
1211
+ str(self.root_folder),
1212
+ base_dir,
1213
+ _get_libdoc,
1214
+ ignore_reference=sentinel is None,
1215
+ )
1216
+
1217
+ entry = self._libaries[entry_key]
1218
+
1219
+ if not entry.ignore_reference and sentinel is not None and sentinel not in entry.references:
1220
+ entry.references.add(sentinel)
1221
+ weakref.finalize(sentinel, self.__remove_library_entry, entry_key, entry)
1222
+
1223
+ return entry.get_libdoc()
1224
+
1225
+ @_logger.call
1226
+ def get_libdoc_from_model(
1227
+ self,
1228
+ model: ast.AST,
1229
+ source: str,
1230
+ model_type: str = "RESOURCE",
1231
+ scope: str = "GLOBAL",
1232
+ append_model_errors: bool = True,
1233
+ ) -> LibraryDoc:
1234
+ return get_model_doc(
1235
+ model=model,
1236
+ source=source,
1237
+ model_type=model_type,
1238
+ scope=scope,
1239
+ append_model_errors=append_model_errors,
1240
+ )
1241
+
1242
+ @_logger.call
1243
+ def get_libdoc_for_variables_import(
1244
+ self,
1245
+ name: str,
1246
+ args: Tuple[Any, ...],
1247
+ base_dir: str,
1248
+ sentinel: Any = None,
1249
+ variables: Optional[Dict[str, Any]] = None,
1250
+ resolve_variables: bool = True,
1251
+ resolve_command_line_vars: bool = True,
1252
+ ) -> VariablesDoc:
1253
+ source = self.find_variables(
1254
+ name,
1255
+ base_dir,
1256
+ variables,
1257
+ resolve_variables=resolve_variables,
1258
+ resolve_command_line_vars=resolve_command_line_vars,
1259
+ )
1260
+
1261
+ def _get_libdoc(name: str, args: Tuple[Any, ...], working_dir: str, base_dir: str) -> VariablesDoc:
1262
+ meta, source = self.get_variables_meta(
1263
+ name,
1264
+ base_dir,
1265
+ variables,
1266
+ resolve_command_line_vars=resolve_command_line_vars,
1267
+ )
1268
+
1269
+ self._logger.debug(lambda: f"Load variables {source}{args!r}")
1270
+ if meta is not None:
1271
+ meta_file = Path(
1272
+ self.variables_doc_cache_path,
1273
+ meta.filepath_base + ".meta.json",
1274
+ )
1275
+ if meta_file.exists():
1276
+ try:
1277
+ spec_path = None
1278
+ try:
1279
+ saved_meta = from_json(meta_file.read_text("utf-8"), LibraryMetaData)
1280
+ if saved_meta == meta:
1281
+ spec_path = Path(
1282
+ self.variables_doc_cache_path,
1283
+ meta.filepath_base + ".spec.json",
1284
+ )
1285
+ return from_json(spec_path.read_text("utf-8"), VariablesDoc)
1286
+ except (SystemExit, KeyboardInterrupt):
1287
+ raise
1288
+ except BaseException as e:
1289
+ raise RuntimeError(
1290
+ f"Failed to load library meta data for library {name} from {spec_path}"
1291
+ ) from e
1292
+ except (SystemExit, KeyboardInterrupt):
1293
+ raise
1294
+ except BaseException as e:
1295
+ self._logger.exception(e)
1296
+
1297
+ executor = ProcessPoolExecutor(max_workers=1, mp_context=mp.get_context("spawn"))
1298
+ try:
1299
+ result = executor.submit(
1300
+ get_variables_doc,
1301
+ name,
1302
+ args,
1303
+ str(self.root_folder),
1304
+ base_dir,
1305
+ self.get_resolvable_command_line_variables() if resolve_command_line_vars else None,
1306
+ variables,
1307
+ ).result(LOAD_LIBRARY_TIME_OUT)
1308
+ except (SystemExit, KeyboardInterrupt):
1309
+ raise
1310
+ except BaseException as e:
1311
+ self._logger.exception(e)
1312
+ raise
1313
+ finally:
1314
+ executor.shutdown(True)
1315
+
1316
+ if result.stdout:
1317
+ self._logger.warning(lambda: f"stdout captured at loading variables {name}{args!r}:\n{result.stdout}")
1318
+
1319
+ try:
1320
+ if meta is not None:
1321
+ meta_file = Path(
1322
+ self.variables_doc_cache_path,
1323
+ meta.filepath_base + ".meta.json",
1324
+ )
1325
+ spec_file = Path(
1326
+ self.variables_doc_cache_path,
1327
+ meta.filepath_base + ".spec.json",
1328
+ )
1329
+ spec_file.parent.mkdir(parents=True, exist_ok=True)
1330
+
1331
+ try:
1332
+ spec_file.write_text(as_json(result), "utf-8")
1333
+ except (SystemExit, KeyboardInterrupt):
1334
+ raise
1335
+ except BaseException as e:
1336
+ raise RuntimeError(f"Cannot write spec file for variables '{name}' to '{spec_file}'") from e
1337
+ meta_file.write_text(as_json(meta), "utf-8")
1338
+ else:
1339
+ self._logger.debug(lambda: f"Skip caching variables {name}{args!r}")
1340
+ except (SystemExit, KeyboardInterrupt):
1341
+ raise
1342
+ except BaseException as e:
1343
+ self._logger.exception(e)
1344
+
1345
+ return result
1346
+
1347
+ resolved_args = resolve_args(
1348
+ args,
1349
+ str(self.root_folder),
1350
+ base_dir,
1351
+ self.get_resolvable_command_line_variables() if resolve_command_line_vars else None,
1352
+ variables,
1353
+ )
1354
+ entry_key = _VariablesEntryKey(source, resolved_args)
1355
+
1356
+ with self._variables_lock:
1357
+ if entry_key not in self._variables:
1358
+ self._variables[entry_key] = _VariablesEntry(
1359
+ name,
1360
+ resolved_args,
1361
+ str(self.root_folder),
1362
+ base_dir,
1363
+ self,
1364
+ _get_libdoc,
1365
+ )
1366
+
1367
+ entry = self._variables[entry_key]
1368
+
1369
+ if sentinel is not None and sentinel not in entry.references:
1370
+ entry.references.add(sentinel)
1371
+ weakref.finalize(sentinel, self.__remove_variables_entry, entry_key, entry)
1372
+
1373
+ return entry.get_libdoc()
1374
+
1375
+ @_logger.call
1376
+ def _get_entry_for_resource_import(
1377
+ self,
1378
+ name: str,
1379
+ base_dir: str,
1380
+ sentinel: Any = None,
1381
+ variables: Optional[Dict[str, Any]] = None,
1382
+ ) -> _ResourcesEntry:
1383
+ source = self.find_resource(name, base_dir, variables=variables)
1384
+
1385
+ def _get_document() -> TextDocument:
1386
+ self._logger.debug(lambda: f"Load resource {name} from source {source}")
1387
+
1388
+ source_path = Path(source).resolve()
1389
+ extension = source_path.suffix
1390
+ if extension.lower() not in RESOURCE_EXTENSIONS:
1391
+ raise ImportError(
1392
+ f"Invalid resource file extension '{extension}'. "
1393
+ f"Supported extensions are {', '.join(repr(s) for s in RESOURCE_EXTENSIONS)}."
1394
+ )
1395
+
1396
+ return self.documents_manager.get_or_open_document(source_path)
1397
+
1398
+ entry_key = _ResourcesEntryKey(source)
1399
+
1400
+ with self._resources_lock:
1401
+ if entry_key not in self._resources:
1402
+ self._resources[entry_key] = _ResourcesEntry(name, self, _get_document)
1403
+
1404
+ entry = self._resources[entry_key]
1405
+
1406
+ if sentinel is not None and sentinel not in entry.references:
1407
+ entry.references.add(sentinel)
1408
+ weakref.finalize(sentinel, self.__remove_resource_entry, entry_key, entry)
1409
+
1410
+ return entry
1411
+
1412
+ def get_namespace_and_libdoc_for_resource_import(
1413
+ self,
1414
+ name: str,
1415
+ base_dir: str,
1416
+ sentinel: Any = None,
1417
+ variables: Optional[Dict[str, Any]] = None,
1418
+ ) -> Tuple["Namespace", LibraryDoc]:
1419
+ entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
1420
+
1421
+ return entry.get_namespace(), entry.get_libdoc()
1422
+
1423
+ def get_namespace_for_resource_import(
1424
+ self,
1425
+ name: str,
1426
+ base_dir: str,
1427
+ sentinel: Any = None,
1428
+ variables: Optional[Dict[str, Any]] = None,
1429
+ ) -> "Namespace":
1430
+ entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
1431
+
1432
+ return entry.get_namespace()
1433
+
1434
+ def get_libdoc_for_resource_import(
1435
+ self,
1436
+ name: str,
1437
+ base_dir: str,
1438
+ sentinel: Any = None,
1439
+ variables: Optional[Dict[str, Any]] = None,
1440
+ ) -> LibraryDoc:
1441
+ entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
1442
+
1443
+ return entry.get_libdoc()
1444
+
1445
+ def complete_library_import(
1446
+ self,
1447
+ name: Optional[str],
1448
+ base_dir: str = ".",
1449
+ variables: Optional[Dict[str, Any]] = None,
1450
+ ) -> List[CompleteResult]:
1451
+ return complete_library_import(
1452
+ name,
1453
+ str(self.root_folder),
1454
+ base_dir,
1455
+ self.get_resolvable_command_line_variables(),
1456
+ variables,
1457
+ )
1458
+
1459
+ def complete_resource_import(
1460
+ self,
1461
+ name: Optional[str],
1462
+ base_dir: str = ".",
1463
+ variables: Optional[Dict[str, Any]] = None,
1464
+ ) -> Optional[List[CompleteResult]]:
1465
+ return complete_resource_import(
1466
+ name,
1467
+ str(self.root_folder),
1468
+ base_dir,
1469
+ self.get_resolvable_command_line_variables(),
1470
+ variables,
1471
+ )
1472
+
1473
+ def complete_variables_import(
1474
+ self,
1475
+ name: Optional[str],
1476
+ base_dir: str = ".",
1477
+ variables: Optional[Dict[str, Any]] = None,
1478
+ ) -> Optional[List[CompleteResult]]:
1479
+ return complete_variables_import(
1480
+ name,
1481
+ str(self.root_folder),
1482
+ base_dir,
1483
+ self.get_resolvable_command_line_variables(),
1484
+ variables,
1485
+ )
1486
+
1487
+ def resolve_variable(
1488
+ self,
1489
+ name: str,
1490
+ base_dir: str = ".",
1491
+ variables: Optional[Dict[str, Any]] = None,
1492
+ ) -> Any:
1493
+ return resolve_variable(
1494
+ name,
1495
+ str(self.root_folder),
1496
+ base_dir,
1497
+ self.get_resolvable_command_line_variables(),
1498
+ variables,
1499
+ )