robotcode-robot 0.68.2__py3-none-any.whl → 0.68.5__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.
@@ -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
+ )