robotcode-robot 2.6.2__py3-none-any.whl → 2.8.0.dev548__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.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/config/loader.py +18 -5
- robotcode/robot/config/model.py +258 -61
- robotcode/robot/diagnostics/analyzer_result.py +1 -0
- robotcode/robot/diagnostics/data_cache.py +12 -1
- robotcode/robot/diagnostics/diagnostic_rules.py +75 -0
- robotcode/robot/diagnostics/diagnostics_modifier.py +83 -43
- robotcode/robot/diagnostics/document_cache_helper.py +158 -17
- robotcode/robot/diagnostics/errors.py +7 -0
- robotcode/robot/diagnostics/import_resolver.py +178 -12
- robotcode/robot/diagnostics/imports_manager.py +556 -220
- robotcode/robot/diagnostics/library_doc.py +874 -162
- robotcode/robot/diagnostics/namespace.py +84 -18
- robotcode/robot/diagnostics/namespace_analyzer.py +59 -16
- robotcode/robot/diagnostics/project_index.py +88 -13
- robotcode/robot/diagnostics/scope_tree.py +2 -2
- robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +790 -201
- robotcode/robot/diagnostics/semantic_analyzer/enums.py +39 -2
- robotcode/robot/diagnostics/semantic_analyzer/json_dump.py +364 -0
- robotcode/robot/diagnostics/semantic_analyzer/model.py +72 -11
- robotcode/robot/diagnostics/semantic_analyzer/nodes.py +7 -1
- robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py +218 -112
- robotcode/robot/diagnostics/workspace_config.py +3 -0
- robotcode/robot/utils/markdown_docs.py +733 -0
- robotcode/robot/utils/markdownformatter.py +57 -12
- robotcode/robot/utils/match.py +13 -0
- robotcode/robot/utils/robot_patching.py +4 -0
- robotcode/robot/utils/variables.py +3 -0
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/METADATA +4 -3
- robotcode_robot-2.8.0.dev548.dist-info/RECORD +48 -0
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/WHEEL +1 -1
- robotcode_robot-2.6.2.dist-info/RECORD +0 -46
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import multiprocessing as mp
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import threading
|
|
6
|
+
import time
|
|
5
7
|
import weakref
|
|
6
8
|
from abc import ABC, abstractmethod
|
|
7
|
-
from concurrent.futures import ProcessPoolExecutor
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
10
|
+
from dataclasses import dataclass, field, replace
|
|
11
|
+
from multiprocessing.connection import Connection, wait
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import (
|
|
11
14
|
TYPE_CHECKING,
|
|
12
15
|
Any,
|
|
16
|
+
Callable,
|
|
13
17
|
Dict,
|
|
14
18
|
List,
|
|
15
19
|
Mapping,
|
|
16
20
|
Optional,
|
|
17
21
|
Set,
|
|
18
22
|
Tuple,
|
|
23
|
+
Union,
|
|
19
24
|
final,
|
|
20
25
|
)
|
|
21
26
|
|
|
@@ -42,21 +47,34 @@ from robotcode.core.uri import Uri
|
|
|
42
47
|
from robotcode.core.utils.caching import SimpleLRUCache
|
|
43
48
|
from robotcode.core.utils.glob_path import Pattern
|
|
44
49
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
45
|
-
from robotcode.core.utils.path import
|
|
50
|
+
from robotcode.core.utils.path import (
|
|
51
|
+
DiskInfo,
|
|
52
|
+
disk_info_from_stat,
|
|
53
|
+
normalized_path,
|
|
54
|
+
path_is_relative_to,
|
|
55
|
+
probe_disk_info,
|
|
56
|
+
)
|
|
46
57
|
|
|
47
58
|
from ..__version__ import __version__
|
|
48
59
|
from ..utils import RF_VERSION
|
|
49
60
|
from ..utils.robot_path import find_file_ex
|
|
61
|
+
from ..utils.stubs import Languages
|
|
50
62
|
from ..utils.variables import contains_variable
|
|
51
63
|
from .data_cache import CACHE_DIR_NAME, CacheSection, build_cache_dir
|
|
52
64
|
from .data_cache import SqliteDataCache as DefaultDataCache
|
|
53
65
|
from .entities import (
|
|
54
66
|
CommandLineVariableDefinition,
|
|
67
|
+
Import,
|
|
68
|
+
LibraryImport,
|
|
69
|
+
ResourceImport,
|
|
55
70
|
VariableDefinition,
|
|
71
|
+
VariablesImport,
|
|
56
72
|
)
|
|
73
|
+
from .errors import Error as DiagnosticError
|
|
57
74
|
from .library_doc import (
|
|
58
75
|
ROBOT_LIBRARY_PACKAGE,
|
|
59
76
|
CompleteResult,
|
|
77
|
+
Error,
|
|
60
78
|
LibraryDoc,
|
|
61
79
|
ModuleSpec,
|
|
62
80
|
ResourceDoc,
|
|
@@ -88,12 +106,49 @@ RESOURCE_EXTENSIONS = (
|
|
|
88
106
|
if RF_VERSION >= (6, 1)
|
|
89
107
|
else {".resource", ".robot", ".txt", ".tsv", ".rst", ".rest"}
|
|
90
108
|
)
|
|
109
|
+
if RF_VERSION >= (7, 5):
|
|
110
|
+
RESOURCE_EXTENSIONS |= {".md", ".markdown"}
|
|
91
111
|
REST_EXTENSIONS = (".rst", ".rest")
|
|
92
112
|
|
|
93
113
|
|
|
94
114
|
DEFAULT_LOAD_LIBRARY_TIMEOUT: int = 10
|
|
95
115
|
ENV_LOAD_LIBRARY_TIMEOUT_VAR = "ROBOTCODE_LOAD_LIBRARY_TIMEOUT"
|
|
96
116
|
COMPLETE_LIBRARY_IMPORT_TIMEOUT = COMPLETE_RESOURCE_IMPORT_TIMEOUT = COMPLETE_VARIABLES_IMPORT_TIMEOUT = 5
|
|
117
|
+
# waiting on a pipe takes the timeout in milliseconds as a C int on POSIX
|
|
118
|
+
MAX_LOAD_WAIT_SECONDS = 2_000_000
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class LoadTimeoutError(RuntimeError):
|
|
122
|
+
"""Loading a library or variable file took longer than the load library timeout."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class LoadExitError(RuntimeError):
|
|
126
|
+
"""The import of a library or variable file ended before it finished, e.g. through sys.exit()."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _run_import_worker(connection: Connection, func: Callable[..., Any], func_args: Tuple[Any, ...]) -> None:
|
|
130
|
+
"""Process target of `ImportsManager._run_in_subprocess`: sends `(ok, value)` back to the parent."""
|
|
131
|
+
try:
|
|
132
|
+
result: Tuple[bool, Any] = (True, func(*func_args))
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# a SystemExit ends only the worker, the parent reports its exit code
|
|
135
|
+
result = (False, e)
|
|
136
|
+
|
|
137
|
+
connection.send(result)
|
|
138
|
+
connection.close()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _end_process(process: mp.process.BaseProcess) -> None:
|
|
142
|
+
if process.pid is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if process.is_alive():
|
|
146
|
+
process.terminate()
|
|
147
|
+
process.join(1)
|
|
148
|
+
if process.is_alive():
|
|
149
|
+
process.kill()
|
|
150
|
+
|
|
151
|
+
process.join()
|
|
97
152
|
|
|
98
153
|
|
|
99
154
|
@dataclass(frozen=True, slots=True)
|
|
@@ -272,14 +327,23 @@ class _LibrariesEntry(_ImportEntry):
|
|
|
272
327
|
with self._lock:
|
|
273
328
|
return self._lib_doc is not None
|
|
274
329
|
|
|
275
|
-
def
|
|
330
|
+
def get_libdoc_with_meta(self) -> Tuple[LibraryDoc, Optional["LibraryMetaData"]]:
|
|
331
|
+
"""Return the LibraryDoc together with the meta of the state it was built from.
|
|
332
|
+
|
|
333
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
334
|
+
the same state — reading ``get_libdoc()`` and ``meta`` separately can
|
|
335
|
+
interleave with an invalidation and pair a doc with a newer meta.
|
|
336
|
+
"""
|
|
276
337
|
with self._lock:
|
|
277
338
|
if self._lib_doc is None:
|
|
278
339
|
self._update()
|
|
279
340
|
|
|
280
341
|
assert self._lib_doc is not None
|
|
281
342
|
|
|
282
|
-
return self._lib_doc
|
|
343
|
+
return self._lib_doc, self._meta
|
|
344
|
+
|
|
345
|
+
def get_libdoc(self) -> LibraryDoc:
|
|
346
|
+
return self.get_libdoc_with_meta()[0]
|
|
283
347
|
|
|
284
348
|
|
|
285
349
|
@dataclass(frozen=True, slots=True)
|
|
@@ -343,7 +407,11 @@ class _ResourcesEntry(_ImportEntry):
|
|
|
343
407
|
)
|
|
344
408
|
|
|
345
409
|
self._document = self.parent.documents_manager.get_or_open_document(self.source_path)
|
|
346
|
-
|
|
410
|
+
# Bind the meta to the document's disk state instead of a fresh stat, so
|
|
411
|
+
# it describes exactly the content this entry's ResourceDoc is built
|
|
412
|
+
# from. A dirty/editor-open document yields no meta — its state must
|
|
413
|
+
# never be persisted.
|
|
414
|
+
self._meta = RobotFileMeta.from_document(self._document)
|
|
347
415
|
|
|
348
416
|
if self._document._version is None:
|
|
349
417
|
self.file_watchers.append(
|
|
@@ -390,15 +458,21 @@ class _ResourcesEntry(_ImportEntry):
|
|
|
390
458
|
def _get_namespace(self) -> "Namespace":
|
|
391
459
|
return self.parent.get_namespace_for_resource(self._get_document())
|
|
392
460
|
|
|
393
|
-
def
|
|
461
|
+
def get_resource_doc_with_meta(self) -> Tuple[ResourceDoc, Optional["RobotFileMeta"]]:
|
|
462
|
+
"""Return the ResourceDoc together with the meta of the content it was built from.
|
|
463
|
+
|
|
464
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
465
|
+
the same state — reading ``get_libdoc()`` and ``meta`` separately can
|
|
466
|
+
interleave with an invalidation and pair a doc with a newer meta.
|
|
467
|
+
"""
|
|
394
468
|
with self._lock:
|
|
395
469
|
if self._lib_doc is None:
|
|
396
470
|
self._lib_doc = self.parent.get_resource_doc_from_document(self._get_document())
|
|
397
471
|
|
|
398
|
-
return self._lib_doc
|
|
472
|
+
return self._lib_doc, self._meta
|
|
399
473
|
|
|
400
474
|
def get_libdoc(self) -> ResourceDoc:
|
|
401
|
-
return self.
|
|
475
|
+
return self.get_resource_doc_with_meta()[0]
|
|
402
476
|
|
|
403
477
|
|
|
404
478
|
@dataclass(frozen=True, slots=True)
|
|
@@ -474,7 +548,7 @@ class _VariablesEntry(_ImportEntry):
|
|
|
474
548
|
self.resolve_command_line_vars,
|
|
475
549
|
)
|
|
476
550
|
|
|
477
|
-
if self._lib_doc is not None:
|
|
551
|
+
if self._lib_doc is not None and self._lib_doc.source:
|
|
478
552
|
self.file_watchers.append(
|
|
479
553
|
self.parent.file_watcher_manager.add_file_watchers(
|
|
480
554
|
self.parent.did_change_watched_files,
|
|
@@ -495,14 +569,22 @@ class _VariablesEntry(_ImportEntry):
|
|
|
495
569
|
with self._lock:
|
|
496
570
|
return self._lib_doc is not None
|
|
497
571
|
|
|
498
|
-
def
|
|
572
|
+
def get_libdoc_with_meta(self) -> Tuple[VariablesDoc, Optional["LibraryMetaData"]]:
|
|
573
|
+
"""Return the VariablesDoc together with the meta of the state it was built from.
|
|
574
|
+
|
|
575
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
576
|
+
the same state (see ``_LibrariesEntry.get_libdoc_with_meta``).
|
|
577
|
+
"""
|
|
499
578
|
with self._lock:
|
|
500
579
|
if self._lib_doc is None:
|
|
501
580
|
self._update()
|
|
502
581
|
|
|
503
582
|
assert self._lib_doc is not None
|
|
504
583
|
|
|
505
|
-
return self._lib_doc
|
|
584
|
+
return self._lib_doc, self._meta
|
|
585
|
+
|
|
586
|
+
def get_libdoc(self) -> VariablesDoc:
|
|
587
|
+
return self.get_libdoc_with_meta()[0]
|
|
506
588
|
|
|
507
589
|
|
|
508
590
|
@dataclass(slots=True)
|
|
@@ -513,7 +595,7 @@ class LibraryMetaData:
|
|
|
513
595
|
submodule_search_locations: Optional[List[str]]
|
|
514
596
|
by_path: bool
|
|
515
597
|
|
|
516
|
-
|
|
598
|
+
file_infos: Optional[Dict[str, DiskInfo]] = None
|
|
517
599
|
|
|
518
600
|
has_errors: bool = False
|
|
519
601
|
|
|
@@ -528,16 +610,38 @@ class LibraryMetaData:
|
|
|
528
610
|
|
|
529
611
|
raise ValueError("Cannot determine cache key.")
|
|
530
612
|
|
|
613
|
+
def cache_key_for_args(self, args: Tuple[Any, ...]) -> str:
|
|
614
|
+
# the documentation can depend on the import arguments, so each argument set gets its own entry
|
|
615
|
+
return f"{self.cache_key}\n{args!r}" if args else self.cache_key
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def trusted(self) -> bool:
|
|
619
|
+
return self.file_infos is None or all(info.trusted for info in self.file_infos.values())
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _stat_library_file(path: str) -> os.stat_result:
|
|
623
|
+
# Follow symlinks so target changes behind an unchanged link are detected
|
|
624
|
+
# (editable installs); for a broken link fall back to the link's own state
|
|
625
|
+
# so a single dangling .py file doesn't make the whole library uncacheable.
|
|
626
|
+
try:
|
|
627
|
+
return os.stat(path)
|
|
628
|
+
except OSError:
|
|
629
|
+
return os.stat(path, follow_symlinks=False)
|
|
630
|
+
|
|
531
631
|
|
|
532
|
-
def
|
|
632
|
+
def _collect_library_file_infos(
|
|
533
633
|
origin: Optional[str],
|
|
534
634
|
submodule_search_locations: Optional[List[str]],
|
|
535
|
-
) -> Optional[Dict[str,
|
|
536
|
-
"""Collect
|
|
537
|
-
|
|
635
|
+
) -> Optional[Dict[str, DiskInfo]]:
|
|
636
|
+
"""Collect DiskInfo snapshots from origin and submodule_search_locations."""
|
|
637
|
+
file_infos: Dict[str, DiskInfo] = {}
|
|
638
|
+
|
|
639
|
+
# one capture moment for the whole collection, so all snapshots share the
|
|
640
|
+
# same racy-mtime reference point
|
|
641
|
+
now_ns = time.time_ns()
|
|
538
642
|
|
|
539
643
|
if origin is not None:
|
|
540
|
-
|
|
644
|
+
file_infos[origin] = disk_info_from_stat(_stat_library_file(origin), now_ns)
|
|
541
645
|
|
|
542
646
|
if submodule_search_locations:
|
|
543
647
|
for loc in submodule_search_locations:
|
|
@@ -545,9 +649,9 @@ def _collect_library_mtimes(
|
|
|
545
649
|
for filename in filenames:
|
|
546
650
|
if filename.endswith(".py"):
|
|
547
651
|
filepath = os.path.join(dirpath, filename)
|
|
548
|
-
|
|
652
|
+
file_infos[filepath] = disk_info_from_stat(_stat_library_file(filepath), now_ns)
|
|
549
653
|
|
|
550
|
-
return
|
|
654
|
+
return file_infos or None
|
|
551
655
|
|
|
552
656
|
|
|
553
657
|
def _matches_any_pattern(
|
|
@@ -564,7 +668,45 @@ def _matches_any_pattern(
|
|
|
564
668
|
@dataclass(slots=True)
|
|
565
669
|
class RobotFileMeta:
|
|
566
670
|
source: str
|
|
567
|
-
|
|
671
|
+
info: DiskInfo
|
|
672
|
+
# the language settings the file was parsed with; only the cached docs of files carry them
|
|
673
|
+
languages: Any = None
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def trusted(self) -> bool:
|
|
677
|
+
return self.info.trusted
|
|
678
|
+
|
|
679
|
+
@classmethod
|
|
680
|
+
def from_document(cls, document: TextDocument) -> Optional["RobotFileMeta"]:
|
|
681
|
+
"""Meta describing exactly the disk state the document's text was read from.
|
|
682
|
+
|
|
683
|
+
``None`` when the document has no disk info, i.e. its text is not known
|
|
684
|
+
to match any on-disk state (editor buffer, read race).
|
|
685
|
+
"""
|
|
686
|
+
info = document.disk_info
|
|
687
|
+
if info is None:
|
|
688
|
+
return None
|
|
689
|
+
return cls(str(normalized_path(document.uri.to_path())), info)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _languages_fingerprint(languages: Optional[Languages]) -> Any:
|
|
693
|
+
"""What the parsing of a Robot Framework file takes from the language settings, comparable and picklable."""
|
|
694
|
+
if languages is None:
|
|
695
|
+
return None
|
|
696
|
+
return (
|
|
697
|
+
tuple(sorted(languages.bdd_prefixes)),
|
|
698
|
+
tuple(sorted(languages.headers.items())),
|
|
699
|
+
tuple(sorted(languages.settings.items())),
|
|
700
|
+
tuple(sorted(languages.true_strings)),
|
|
701
|
+
tuple(sorted(languages.false_strings)),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _has_cache_key(meta: LibraryMetaData, cache_key: str) -> bool:
|
|
706
|
+
try:
|
|
707
|
+
return meta.cache_key == cache_key
|
|
708
|
+
except ValueError:
|
|
709
|
+
return False
|
|
568
710
|
|
|
569
711
|
|
|
570
712
|
@dataclass(slots=True)
|
|
@@ -576,7 +718,7 @@ class NamespaceMetaData:
|
|
|
576
718
|
"""
|
|
577
719
|
|
|
578
720
|
source: str
|
|
579
|
-
|
|
721
|
+
source_info: DiskInfo
|
|
580
722
|
config_fingerprint: Any
|
|
581
723
|
dependency_fingerprints: Dict[str, Any] = field(default_factory=dict)
|
|
582
724
|
semantic_model_enabled: bool = False
|
|
@@ -585,6 +727,9 @@ class NamespaceMetaData:
|
|
|
585
727
|
class ImportsManager:
|
|
586
728
|
_logger = LoggingDescriptor()
|
|
587
729
|
|
|
730
|
+
# Seconds that changes of resource documents are gathered before their importers are analyzed again.
|
|
731
|
+
_resource_document_changed_timer_interval: float = 1
|
|
732
|
+
|
|
588
733
|
def __init__(
|
|
589
734
|
self,
|
|
590
735
|
documents_manager: DocumentsManager,
|
|
@@ -656,24 +801,16 @@ class ImportsManager:
|
|
|
656
801
|
if environment:
|
|
657
802
|
self._environment.update(environment)
|
|
658
803
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
tuple(sorted(workspace_langs.bdd_prefixes)),
|
|
663
|
-
tuple(sorted(workspace_langs.headers.items())),
|
|
664
|
-
tuple(sorted(workspace_langs.settings.items())),
|
|
665
|
-
tuple(sorted(workspace_langs.true_strings)),
|
|
666
|
-
tuple(sorted(workspace_langs.false_strings)),
|
|
667
|
-
)
|
|
668
|
-
else:
|
|
669
|
-
languages_fingerprint = None
|
|
804
|
+
self._languages_fingerprint = _languages_fingerprint(
|
|
805
|
+
self.document_cache_helper.get_languages_for_document(Uri.from_path(self.root_folder))
|
|
806
|
+
)
|
|
670
807
|
|
|
671
808
|
self._config_fingerprint: Any = (
|
|
672
809
|
tuple(sorted(self.cmd_variables.items())),
|
|
673
810
|
tuple(self.cmd_variable_files),
|
|
674
811
|
tuple(sorted((k, v) for k, v in self._environment.items() if k not in os.environ)),
|
|
675
812
|
tuple(self.global_library_search_order),
|
|
676
|
-
|
|
813
|
+
self._languages_fingerprint,
|
|
677
814
|
)
|
|
678
815
|
|
|
679
816
|
self._library_files_cache = SimpleLRUCache(2048)
|
|
@@ -688,12 +825,14 @@ class ImportsManager:
|
|
|
688
825
|
default_timeout=120, name="ImportsManager._resource_document_changed_timer_lock"
|
|
689
826
|
)
|
|
690
827
|
self._resource_document_changed_timer: Optional[threading.Timer] = None
|
|
691
|
-
self._resource_document_changed_timer_interval = 1
|
|
692
828
|
self._resource_document_changed_documents: Set[TextDocument] = set()
|
|
693
829
|
|
|
694
830
|
self._resource_libdoc_cache: "weakref.WeakKeyDictionary[ast.AST, Dict[str, ResourceDoc]]" = (
|
|
695
831
|
weakref.WeakKeyDictionary()
|
|
696
832
|
)
|
|
833
|
+
# the doc of each file with the disk state it was read at: while the file keeps that state, analyzing it
|
|
834
|
+
# again gets the same doc instead of a new copy from the disk cache
|
|
835
|
+
self._model_docs_by_source: Dict[str, Tuple[RobotFileMeta, ResourceDoc]] = {}
|
|
697
836
|
|
|
698
837
|
self._diagnostics: List[Diagnostic] = []
|
|
699
838
|
|
|
@@ -761,101 +900,84 @@ class ImportsManager:
|
|
|
761
900
|
"""
|
|
762
901
|
return self._config_fingerprint
|
|
763
902
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
903
|
+
@staticmethod
|
|
904
|
+
def _is_dependency_meta_trusted(value: Any) -> bool:
|
|
905
|
+
# fail closed: a meta type this dispatch does not know must never be
|
|
906
|
+
# persisted — silently treating it as trusted is the exact failure
|
|
907
|
+
# class the freshness system exists to prevent
|
|
908
|
+
return isinstance(value, (RobotFileMeta, LibraryMetaData)) and value.trusted
|
|
770
909
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
910
|
+
def build_namespace_meta(
|
|
911
|
+
self,
|
|
912
|
+
source: str,
|
|
913
|
+
namespace: "Namespace",
|
|
914
|
+
disk_info: DiskInfo,
|
|
915
|
+
*,
|
|
916
|
+
semantic_model_enabled: bool = False,
|
|
917
|
+
) -> Optional[NamespaceMetaData]:
|
|
918
|
+
"""Build a NamespaceMetaData for the given namespace and its dependencies.
|
|
919
|
+
|
|
920
|
+
``disk_info`` must be the snapshot of the content the namespace was
|
|
921
|
+
built from; the dependency fingerprints are the metas recorded during
|
|
922
|
+
import resolution. Returns ``None`` when any of that state is missing
|
|
923
|
+
or untrusted — such a namespace must not be persisted, because its
|
|
924
|
+
fingerprints could describe newer file states than the analyzed
|
|
925
|
+
content and would validate as fresh forever.
|
|
926
|
+
|
|
927
|
+
A namespace with failed imports is not persisted either: the validation
|
|
928
|
+
cannot tell that a missing file appeared since, or that a library that
|
|
929
|
+
failed to load would load now, and the cached diagnostics would keep
|
|
930
|
+
reporting the old error.
|
|
774
931
|
"""
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
for entry in namespace.libraries.values():
|
|
778
|
-
try:
|
|
779
|
-
lib_meta = self.get_cached_library_meta(entry.import_name, entry.args)
|
|
780
|
-
if lib_meta is None:
|
|
781
|
-
lib_meta, _, _ = self.get_library_meta(entry.import_name)
|
|
782
|
-
if lib_meta is not None:
|
|
783
|
-
fingerprints[f"lib:{entry.import_name}"] = lib_meta
|
|
784
|
-
except (SystemExit, KeyboardInterrupt):
|
|
785
|
-
raise
|
|
786
|
-
except BaseException:
|
|
787
|
-
pass
|
|
788
|
-
|
|
789
|
-
for entry in namespace.resources.values():
|
|
790
|
-
source = entry.library_doc.source
|
|
791
|
-
if source:
|
|
792
|
-
try:
|
|
793
|
-
res_meta = self.get_cached_resource_meta(source)
|
|
794
|
-
if res_meta is None:
|
|
795
|
-
res_meta = self.get_resource_meta(source)
|
|
796
|
-
if res_meta is not None:
|
|
797
|
-
fingerprints[f"res:{source}"] = res_meta
|
|
798
|
-
else:
|
|
799
|
-
mtime = os.stat(source, follow_symlinks=False).st_mtime_ns
|
|
800
|
-
fingerprints[f"res:{source}"] = mtime
|
|
801
|
-
except OSError:
|
|
802
|
-
pass
|
|
932
|
+
if not disk_info.trusted:
|
|
933
|
+
return None
|
|
803
934
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
var_meta = self.get_cached_variables_meta(entry.import_name, entry.args)
|
|
807
|
-
if var_meta is None:
|
|
808
|
-
var_meta, _ = self.get_variables_meta(entry.import_name)
|
|
809
|
-
if var_meta is not None:
|
|
810
|
-
fingerprints[f"var:{entry.import_name}"] = var_meta
|
|
811
|
-
except (SystemExit, KeyboardInterrupt):
|
|
812
|
-
raise
|
|
813
|
-
except BaseException:
|
|
814
|
-
pass
|
|
935
|
+
if namespace.failed_imports:
|
|
936
|
+
return None
|
|
815
937
|
|
|
816
|
-
|
|
938
|
+
dependency_metas = namespace.dependency_metas
|
|
939
|
+
if dependency_metas is None:
|
|
940
|
+
return None
|
|
817
941
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
"""Build a NamespaceMetaData for the given namespace and its dependencies."""
|
|
822
|
-
try:
|
|
823
|
-
source_mtime_ns = os.stat(source, follow_symlinks=False).st_mtime_ns
|
|
824
|
-
except OSError:
|
|
825
|
-
source_mtime_ns = 0
|
|
942
|
+
for value in dependency_metas.values():
|
|
943
|
+
if value is None or not self._is_dependency_meta_trusted(value):
|
|
944
|
+
return None
|
|
826
945
|
|
|
827
946
|
return NamespaceMetaData(
|
|
828
947
|
source=source,
|
|
829
|
-
|
|
948
|
+
source_info=disk_info,
|
|
830
949
|
config_fingerprint=self.config_fingerprint,
|
|
831
|
-
dependency_fingerprints=
|
|
950
|
+
dependency_fingerprints=dict(dependency_metas),
|
|
832
951
|
semantic_model_enabled=semantic_model_enabled,
|
|
833
952
|
)
|
|
834
953
|
|
|
835
|
-
def validate_namespace_meta(self, meta: NamespaceMetaData) -> bool:
|
|
836
|
-
"""Check whether a cached NamespaceMetaData is
|
|
954
|
+
def validate_namespace_meta(self, meta: NamespaceMetaData, source_disk_info: Optional[DiskInfo]) -> bool:
|
|
955
|
+
"""Check whether a cached NamespaceMetaData is fresh for the given content.
|
|
837
956
|
|
|
838
957
|
Performs a 2-level validation:
|
|
839
|
-
Level 1 (fast):
|
|
958
|
+
Level 1 (fast): source_disk_info, config_fingerprint
|
|
840
959
|
Level 2 (dependency check): each dependency fingerprint
|
|
960
|
+
|
|
961
|
+
Validation is against the *effective* content: the source is compared
|
|
962
|
+
to the snapshot of the document the namespace would represent, and
|
|
963
|
+
resource dependencies are checked document-first — a dirty editor
|
|
964
|
+
buffer fails validation so the namespace is rebuilt against the buffer
|
|
965
|
+
instead of being served from disk state.
|
|
841
966
|
"""
|
|
842
967
|
source = meta.source
|
|
843
968
|
|
|
844
969
|
# Level 1: fast checks
|
|
845
|
-
|
|
846
|
-
current_mtime = os.stat(source, follow_symlinks=False).st_mtime_ns
|
|
847
|
-
except OSError:
|
|
970
|
+
if source_disk_info is None:
|
|
848
971
|
self._logger.debug(
|
|
849
|
-
lambda: f"Cache miss for {source}: source
|
|
972
|
+
lambda: f"Cache miss for {source}: source has no trustworthy disk state",
|
|
850
973
|
context_name="cache",
|
|
851
974
|
)
|
|
852
975
|
return False
|
|
853
976
|
|
|
854
|
-
if meta.
|
|
977
|
+
if meta.source_info != source_disk_info:
|
|
855
978
|
self._logger.debug(
|
|
856
979
|
lambda: (
|
|
857
|
-
f"Cache miss for {source}: source
|
|
858
|
-
f" (cached={meta.source_mtime_ns}, current={current_mtime})"
|
|
980
|
+
f"Cache miss for {source}: source changed (cached={meta.source_info}, current={source_disk_info})"
|
|
859
981
|
),
|
|
860
982
|
context_name="cache",
|
|
861
983
|
)
|
|
@@ -869,14 +991,14 @@ class ImportsManager:
|
|
|
869
991
|
return False
|
|
870
992
|
|
|
871
993
|
# Level 2: dependency checks — direct comparison, no hashing
|
|
872
|
-
base_dir = os.path.dirname(source)
|
|
873
994
|
for key, saved_value in meta.dependency_fingerprints.items():
|
|
874
995
|
if key.startswith("lib:"):
|
|
996
|
+
# the cache key of the library's meta: its file, or its module name
|
|
875
997
|
lib_name = key[4:]
|
|
876
998
|
try:
|
|
877
999
|
lib_meta = self.get_cached_library_meta(lib_name)
|
|
878
1000
|
if lib_meta is None:
|
|
879
|
-
lib_meta, _, _ = self.get_library_meta(lib_name
|
|
1001
|
+
lib_meta, _, _ = self.get_library_meta(lib_name)
|
|
880
1002
|
if lib_meta is None or lib_meta != saved_value:
|
|
881
1003
|
self._logger.debug(
|
|
882
1004
|
lambda: (
|
|
@@ -898,8 +1020,15 @@ class ImportsManager:
|
|
|
898
1020
|
elif key.startswith("res:"):
|
|
899
1021
|
res_source = key[4:]
|
|
900
1022
|
try:
|
|
901
|
-
|
|
902
|
-
|
|
1023
|
+
# Document-first: an open document is the effective content
|
|
1024
|
+
# of this dependency. A dirty buffer (no disk snapshot)
|
|
1025
|
+
# must fail validation so dependents are rebuilt against
|
|
1026
|
+
# the buffer instead of being served from cached disk
|
|
1027
|
+
# state. Only unopened resources are probed on disk.
|
|
1028
|
+
res_document = self.documents_manager.get(Uri.from_path(res_source).normalized())
|
|
1029
|
+
if res_document is not None:
|
|
1030
|
+
res_meta = RobotFileMeta.from_document(res_document)
|
|
1031
|
+
else:
|
|
903
1032
|
res_meta = self.get_resource_meta(res_source)
|
|
904
1033
|
if res_meta is None or res_meta != saved_value:
|
|
905
1034
|
self._logger.debug(
|
|
@@ -920,11 +1049,12 @@ class ImportsManager:
|
|
|
920
1049
|
)
|
|
921
1050
|
return False
|
|
922
1051
|
elif key.startswith("var:"):
|
|
1052
|
+
# the cache key of the variable file's meta: its file, or its module name
|
|
923
1053
|
var_name = key[4:]
|
|
924
1054
|
try:
|
|
925
1055
|
var_meta = self.get_cached_variables_meta(var_name)
|
|
926
1056
|
if var_meta is None:
|
|
927
|
-
var_meta, _ = self.get_variables_meta(var_name
|
|
1057
|
+
var_meta, _ = self.get_variables_meta(var_name)
|
|
928
1058
|
if var_meta is None or var_meta != saved_value:
|
|
929
1059
|
self._logger.debug(
|
|
930
1060
|
lambda: (
|
|
@@ -952,18 +1082,39 @@ class ImportsManager:
|
|
|
952
1082
|
def get_resource_doc_from_document(self, document: TextDocument) -> ResourceDoc:
|
|
953
1083
|
source = str(document.uri.to_path())
|
|
954
1084
|
|
|
955
|
-
|
|
956
|
-
|
|
1085
|
+
# Capture the meta before parsing, so it can only be older than the
|
|
1086
|
+
# parsed content, never newer (stale metas cause a harmless cache
|
|
1087
|
+
# miss, newer ones would poison the cache).
|
|
1088
|
+
meta = RobotFileMeta.from_document(document)
|
|
1089
|
+
|
|
1090
|
+
if meta is not None and meta.trusted:
|
|
1091
|
+
cached = self._get_model_doc_cached(source, meta)
|
|
957
1092
|
if cached is not None:
|
|
958
1093
|
return cached
|
|
959
1094
|
|
|
960
1095
|
model = self.document_cache_helper.get_resource_model(document)
|
|
961
|
-
|
|
1096
|
+
|
|
1097
|
+
# The document may have been re-read from disk while parsing; in that
|
|
1098
|
+
# case the model does not necessarily belong to the captured meta.
|
|
1099
|
+
if meta is not None and document.disk_info != meta.info:
|
|
1100
|
+
meta = None
|
|
1101
|
+
|
|
1102
|
+
return self.get_libdoc_from_model(model, source, meta)
|
|
962
1103
|
|
|
963
1104
|
def clear_cache(self) -> None:
|
|
1105
|
+
self._model_docs_by_source.clear()
|
|
964
1106
|
count = self.data_cache.clear_all()
|
|
965
1107
|
self._logger.debug(lambda: f"Cleared {count} cache entries from {self.cache_path}")
|
|
966
1108
|
|
|
1109
|
+
def remove_cached_resource_docs(self, sources: List[str]) -> None:
|
|
1110
|
+
"""Remove the docs of deleted files from the analysis cache and from memory.
|
|
1111
|
+
|
|
1112
|
+
``sources`` are normalized paths, as the entries of the cache name them.
|
|
1113
|
+
"""
|
|
1114
|
+
for source in sources:
|
|
1115
|
+
self._model_docs_by_source.pop(source, None)
|
|
1116
|
+
self.data_cache.remove_entries(CacheSection.RESOURCE, sources)
|
|
1117
|
+
|
|
967
1118
|
@_logger.call
|
|
968
1119
|
def get_command_line_variables(self) -> List[VariableDefinition]:
|
|
969
1120
|
with self._command_line_variables_lock:
|
|
@@ -1153,6 +1304,23 @@ class ImportsManager:
|
|
|
1153
1304
|
if resource_changed:
|
|
1154
1305
|
self.resources_changed(self, resource_changed)
|
|
1155
1306
|
|
|
1307
|
+
def changed_import_files(self) -> List[str]:
|
|
1308
|
+
"""The files of the loaded libraries and variable files that changed since they were loaded."""
|
|
1309
|
+
with self._libaries_lock:
|
|
1310
|
+
entries: List[Union[_LibrariesEntry, _VariablesEntry]] = list(self._libaries.values())
|
|
1311
|
+
with self._variables_lock:
|
|
1312
|
+
entries.extend(self._variables.values())
|
|
1313
|
+
|
|
1314
|
+
result: List[str] = []
|
|
1315
|
+
for entry in entries:
|
|
1316
|
+
meta = entry.meta
|
|
1317
|
+
if meta is None or not meta.file_infos:
|
|
1318
|
+
continue
|
|
1319
|
+
for path, info in meta.file_infos.items():
|
|
1320
|
+
if path not in result and probe_disk_info(path) != info:
|
|
1321
|
+
result.append(path)
|
|
1322
|
+
return result
|
|
1323
|
+
|
|
1156
1324
|
@_logger.call
|
|
1157
1325
|
def did_change_watched_files(self, sender: Any, changes: List[FileEvent]) -> None:
|
|
1158
1326
|
libraries_changed: List[Tuple[_LibrariesEntryKey, FileChangeType, Optional[LibraryDoc]]] = []
|
|
@@ -1302,7 +1470,7 @@ class ImportsManager:
|
|
|
1302
1470
|
import_name,
|
|
1303
1471
|
None,
|
|
1304
1472
|
True,
|
|
1305
|
-
|
|
1473
|
+
file_infos=_collect_library_file_infos(import_name, None),
|
|
1306
1474
|
)
|
|
1307
1475
|
else:
|
|
1308
1476
|
module_spec = self._get_module_spec_cached(import_name)
|
|
@@ -1313,7 +1481,9 @@ class ImportsManager:
|
|
|
1313
1481
|
module_spec.origin,
|
|
1314
1482
|
module_spec.submodule_search_locations,
|
|
1315
1483
|
False,
|
|
1316
|
-
|
|
1484
|
+
file_infos=_collect_library_file_infos(
|
|
1485
|
+
module_spec.origin, module_spec.submodule_search_locations
|
|
1486
|
+
),
|
|
1317
1487
|
)
|
|
1318
1488
|
|
|
1319
1489
|
if result is not None:
|
|
@@ -1367,7 +1537,7 @@ class ImportsManager:
|
|
|
1367
1537
|
import_name,
|
|
1368
1538
|
None,
|
|
1369
1539
|
True,
|
|
1370
|
-
|
|
1540
|
+
file_infos=_collect_library_file_infos(import_name, None),
|
|
1371
1541
|
)
|
|
1372
1542
|
else:
|
|
1373
1543
|
module_spec = self._get_module_spec_cached(import_name)
|
|
@@ -1378,7 +1548,9 @@ class ImportsManager:
|
|
|
1378
1548
|
module_spec.origin,
|
|
1379
1549
|
module_spec.submodule_search_locations,
|
|
1380
1550
|
False,
|
|
1381
|
-
|
|
1551
|
+
file_infos=_collect_library_file_infos(
|
|
1552
|
+
module_spec.origin, module_spec.submodule_search_locations
|
|
1553
|
+
),
|
|
1382
1554
|
)
|
|
1383
1555
|
|
|
1384
1556
|
if result is not None:
|
|
@@ -1399,38 +1571,28 @@ class ImportsManager:
|
|
|
1399
1571
|
|
|
1400
1572
|
return None, name
|
|
1401
1573
|
|
|
1402
|
-
def get_cached_library_meta(self,
|
|
1403
|
-
"""Return
|
|
1574
|
+
def get_cached_library_meta(self, cache_key: str) -> Optional[LibraryMetaData]:
|
|
1575
|
+
"""Return the meta of a loaded library whose meta has the cache key `cache_key`, or None.
|
|
1404
1576
|
|
|
1405
|
-
|
|
1406
|
-
the same
|
|
1577
|
+
The cache key names the library's file (or module), so a library with
|
|
1578
|
+
the same import name from another folder is not found.
|
|
1407
1579
|
"""
|
|
1408
1580
|
with self._libaries_lock:
|
|
1409
|
-
|
|
1410
|
-
if
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
def get_cached_resource_meta(self, source: str) -> Optional[RobotFileMeta]:
|
|
1415
|
-
"""Return already-computed RobotFileMeta from the internal cache, or None."""
|
|
1416
|
-
entry_key = _ResourcesEntryKey(str(normalized_path(Path(source))))
|
|
1417
|
-
with self._resources_lock:
|
|
1418
|
-
entry = self._resources.get(entry_key)
|
|
1419
|
-
if entry is not None:
|
|
1420
|
-
return entry.meta
|
|
1421
|
-
return None
|
|
1581
|
+
return next(
|
|
1582
|
+
(e.meta for e in self._libaries.values() if e.meta is not None and _has_cache_key(e.meta, cache_key)),
|
|
1583
|
+
None,
|
|
1584
|
+
)
|
|
1422
1585
|
|
|
1423
|
-
def get_cached_variables_meta(self,
|
|
1424
|
-
"""Return
|
|
1586
|
+
def get_cached_variables_meta(self, cache_key: str) -> Optional[LibraryMetaData]:
|
|
1587
|
+
"""Return the meta of a loaded variable file whose meta has the cache key `cache_key`, or None.
|
|
1425
1588
|
|
|
1426
|
-
|
|
1427
|
-
different content depending on the arguments.
|
|
1589
|
+
See ``get_cached_library_meta``.
|
|
1428
1590
|
"""
|
|
1429
1591
|
with self._variables_lock:
|
|
1430
|
-
|
|
1431
|
-
if
|
|
1432
|
-
|
|
1433
|
-
|
|
1592
|
+
return next(
|
|
1593
|
+
(e.meta for e in self._variables.values() if e.meta is not None and _has_cache_key(e.meta, cache_key)),
|
|
1594
|
+
None,
|
|
1595
|
+
)
|
|
1434
1596
|
|
|
1435
1597
|
def find_library(
|
|
1436
1598
|
self,
|
|
@@ -1554,6 +1716,41 @@ class ImportsManager:
|
|
|
1554
1716
|
|
|
1555
1717
|
return name
|
|
1556
1718
|
|
|
1719
|
+
def find_import_source(
|
|
1720
|
+
self, imp: Import, base_dir: str, variables: Optional[Dict[str, Any]] = None
|
|
1721
|
+
) -> Optional[str]:
|
|
1722
|
+
"""The file an import refers to, or `None` when its file or module is not found. Nothing is loaded."""
|
|
1723
|
+
if imp.name is None:
|
|
1724
|
+
return None
|
|
1725
|
+
|
|
1726
|
+
try:
|
|
1727
|
+
if isinstance(imp, ResourceImport):
|
|
1728
|
+
return self.find_resource(imp.name, base_dir, variables=variables)
|
|
1729
|
+
|
|
1730
|
+
if isinstance(imp, LibraryImport):
|
|
1731
|
+
name = self.find_library(imp.name, base_dir, variables)
|
|
1732
|
+
by_path = is_library_by_path(name)
|
|
1733
|
+
elif isinstance(imp, VariablesImport):
|
|
1734
|
+
name = self.find_variables(imp.name, base_dir, variables)
|
|
1735
|
+
by_path = is_variables_by_path(name)
|
|
1736
|
+
else:
|
|
1737
|
+
return None
|
|
1738
|
+
|
|
1739
|
+
# a file imported by path is looked up with its name, and the lookup fails when it is missing
|
|
1740
|
+
if by_path:
|
|
1741
|
+
return name
|
|
1742
|
+
|
|
1743
|
+
module_spec = self._get_module_spec_cached(name)
|
|
1744
|
+
if module_spec is None:
|
|
1745
|
+
return None
|
|
1746
|
+
if module_spec.origin is not None:
|
|
1747
|
+
return module_spec.origin
|
|
1748
|
+
return module_spec.submodule_search_locations[0] if module_spec.submodule_search_locations else None
|
|
1749
|
+
except (SystemExit, KeyboardInterrupt):
|
|
1750
|
+
raise
|
|
1751
|
+
except BaseException:
|
|
1752
|
+
return None
|
|
1753
|
+
|
|
1557
1754
|
@property
|
|
1558
1755
|
def executor(self) -> ProcessPoolExecutor:
|
|
1559
1756
|
with self._executor_lock:
|
|
@@ -1569,26 +1766,61 @@ class ImportsManager:
|
|
|
1569
1766
|
A fresh process per import is intentional: libraries and variable files
|
|
1570
1767
|
can pollute the interpreter (e.g. via sys.modules, global state, native
|
|
1571
1768
|
extensions) and cannot be safely re-imported after on-disk changes.
|
|
1769
|
+
|
|
1770
|
+
The process is ended when the load library timeout expires, so a library
|
|
1771
|
+
that blocks cannot hold up the caller beyond the timeout.
|
|
1572
1772
|
"""
|
|
1573
|
-
|
|
1773
|
+
context = mp.get_context("spawn")
|
|
1774
|
+
receiver, sender = context.Pipe(duplex=False)
|
|
1775
|
+
process = context.Process(target=_run_import_worker, args=(sender, func, func_args))
|
|
1574
1776
|
try:
|
|
1575
1777
|
try:
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1778
|
+
process.start()
|
|
1779
|
+
finally:
|
|
1780
|
+
# the worker keeps the sending end (on Windows once it has unpickled its arguments)
|
|
1781
|
+
sender.close()
|
|
1782
|
+
|
|
1783
|
+
# the sentinel also reports a worker that ends before it has taken over the sending end
|
|
1784
|
+
if not wait([receiver, process.sentinel], min(self.load_library_timeout, MAX_LOAD_WAIT_SECONDS)):
|
|
1785
|
+
raise LoadTimeoutError(
|
|
1579
1786
|
f"{timeout_msg} "
|
|
1580
|
-
f"timed out after {self.load_library_timeout}
|
|
1787
|
+
f"timed out after {self.load_library_timeout} "
|
|
1788
|
+
f"{'second' if self.load_library_timeout == 1 else 'seconds'}. "
|
|
1581
1789
|
"The import may be slow or blocked. "
|
|
1582
1790
|
"If required, increase the timeout by setting the ROBOTCODE_LOAD_LIBRARY_TIMEOUT "
|
|
1583
1791
|
"environment variable."
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
try:
|
|
1795
|
+
if not receiver.poll(0):
|
|
1796
|
+
# only the sentinel is ready: the worker ended before it took over the sending end
|
|
1797
|
+
raise EOFError
|
|
1798
|
+
ok, value = receiver.recv()
|
|
1799
|
+
except (EOFError, BrokenPipeError) as e:
|
|
1800
|
+
process.join(1)
|
|
1801
|
+
raise LoadExitError(
|
|
1802
|
+
f"{timeout_msg} failed: the import ended with exit code {process.exitcode} before it finished, "
|
|
1803
|
+
"for example through sys.exit() or a crash in the imported code."
|
|
1584
1804
|
) from e
|
|
1805
|
+
|
|
1806
|
+
# give the worker the chance to end on its own, the finally block ends it otherwise
|
|
1807
|
+
process.join(1)
|
|
1808
|
+
|
|
1809
|
+
if not ok:
|
|
1810
|
+
raise value
|
|
1811
|
+
|
|
1812
|
+
return value
|
|
1585
1813
|
except (SystemExit, KeyboardInterrupt):
|
|
1586
1814
|
raise
|
|
1815
|
+
except (LoadTimeoutError, LoadExitError):
|
|
1816
|
+
# the caller reports a timeout or an import that ended early as a diagnostic, so no traceback
|
|
1817
|
+
raise
|
|
1587
1818
|
except BaseException as e:
|
|
1588
1819
|
self._logger.exception(e)
|
|
1589
1820
|
raise
|
|
1590
1821
|
finally:
|
|
1591
|
-
|
|
1822
|
+
receiver.close()
|
|
1823
|
+
_end_process(process)
|
|
1592
1824
|
|
|
1593
1825
|
def _save_import_cache(
|
|
1594
1826
|
self,
|
|
@@ -1599,11 +1831,16 @@ class ImportsManager:
|
|
|
1599
1831
|
name: str,
|
|
1600
1832
|
args: Tuple[Any, ...],
|
|
1601
1833
|
) -> None:
|
|
1602
|
-
"""Save an import result to the disk cache, or log skip if no meta."""
|
|
1834
|
+
"""Save an import result to the disk cache under its argument set, or log skip if no meta."""
|
|
1603
1835
|
try:
|
|
1604
|
-
if meta is not None:
|
|
1836
|
+
if meta is not None and not meta.trusted:
|
|
1837
|
+
self._logger.debug(
|
|
1838
|
+
lambda: f"Skip caching {kind} {name}{args!r}: file state too fresh to trust",
|
|
1839
|
+
context_name="import",
|
|
1840
|
+
)
|
|
1841
|
+
elif meta is not None:
|
|
1605
1842
|
try:
|
|
1606
|
-
self.data_cache.save_entry(section, meta.
|
|
1843
|
+
self.data_cache.save_entry(section, meta.cache_key_for_args(args), meta, result)
|
|
1607
1844
|
except (SystemExit, KeyboardInterrupt):
|
|
1608
1845
|
raise
|
|
1609
1846
|
except BaseException as e:
|
|
@@ -1623,11 +1860,18 @@ class ImportsManager:
|
|
|
1623
1860
|
base_dir: str,
|
|
1624
1861
|
variables: Optional[Dict[str, Any]] = None,
|
|
1625
1862
|
) -> Tuple[LibraryDoc, Optional[LibraryMetaData]]:
|
|
1626
|
-
meta,
|
|
1863
|
+
meta, import_name, ignore_arguments = self.get_library_meta(name, base_dir, variables)
|
|
1864
|
+
cache_args = (
|
|
1865
|
+
()
|
|
1866
|
+
if ignore_arguments
|
|
1867
|
+
else resolve_args(args, working_dir, base_dir, self.get_resolvable_command_line_variables(), variables)
|
|
1868
|
+
)
|
|
1627
1869
|
|
|
1628
1870
|
if meta is not None and not meta.has_errors:
|
|
1629
1871
|
try:
|
|
1630
|
-
entry = self.data_cache.read_entry(
|
|
1872
|
+
entry = self.data_cache.read_entry(
|
|
1873
|
+
CacheSection.LIBRARY, meta.cache_key_for_args(cache_args), LibraryMetaData, LibraryDoc
|
|
1874
|
+
)
|
|
1631
1875
|
if entry is not None and entry.meta is not None:
|
|
1632
1876
|
if entry.meta.has_errors:
|
|
1633
1877
|
self._logger.debug(
|
|
@@ -1644,35 +1888,59 @@ class ImportsManager:
|
|
|
1644
1888
|
|
|
1645
1889
|
self._logger.debug(lambda: f"Load library in process {name}{args!r}", context_name="import")
|
|
1646
1890
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1891
|
+
try:
|
|
1892
|
+
result = self._run_in_subprocess(
|
|
1893
|
+
get_library_doc,
|
|
1894
|
+
(
|
|
1895
|
+
name,
|
|
1896
|
+
args if not ignore_arguments else (),
|
|
1897
|
+
working_dir,
|
|
1898
|
+
base_dir,
|
|
1899
|
+
self.get_resolvable_command_line_variables(),
|
|
1900
|
+
variables,
|
|
1901
|
+
),
|
|
1902
|
+
f"Loading library {name!r} with args {args!r}",
|
|
1903
|
+
)
|
|
1904
|
+
except (LoadTimeoutError, LoadExitError) as e:
|
|
1905
|
+
# kept by the import entry until a file changes, but neither written to the disk cache
|
|
1906
|
+
# nor usable as a namespace dependency
|
|
1907
|
+
return (
|
|
1908
|
+
LibraryDoc(
|
|
1909
|
+
name=name,
|
|
1910
|
+
source=(
|
|
1911
|
+
meta.origin if meta is not None else import_name if is_library_by_path(import_name) else None
|
|
1912
|
+
),
|
|
1913
|
+
errors=[
|
|
1914
|
+
Error(
|
|
1915
|
+
message=str(e),
|
|
1916
|
+
type_name=(
|
|
1917
|
+
DiagnosticError.LIBRARY_TIMEOUT_ERROR
|
|
1918
|
+
if isinstance(e, LoadTimeoutError)
|
|
1919
|
+
else DiagnosticError.LIBRARY_EXIT_ERROR
|
|
1920
|
+
),
|
|
1921
|
+
)
|
|
1922
|
+
],
|
|
1923
|
+
python_path=list(sys.path),
|
|
1924
|
+
),
|
|
1925
|
+
None,
|
|
1926
|
+
)
|
|
1659
1927
|
|
|
1660
1928
|
if meta is not None:
|
|
1661
1929
|
meta.has_errors = bool(result.errors)
|
|
1662
1930
|
|
|
1663
|
-
self._save_import_cache(CacheSection.LIBRARY, meta, result, "library", name,
|
|
1931
|
+
self._save_import_cache(CacheSection.LIBRARY, meta, result, "library", name, cache_args)
|
|
1664
1932
|
|
|
1665
1933
|
return result, meta
|
|
1666
1934
|
|
|
1667
1935
|
@_logger.call
|
|
1668
|
-
def
|
|
1936
|
+
def get_libdoc_for_library_import_with_meta(
|
|
1669
1937
|
self,
|
|
1670
1938
|
name: str,
|
|
1671
1939
|
args: Tuple[Any, ...],
|
|
1672
1940
|
base_dir: str,
|
|
1673
1941
|
sentinel: Any = None,
|
|
1674
1942
|
variables: Optional[Dict[str, Any]] = None,
|
|
1675
|
-
) -> LibraryDoc:
|
|
1943
|
+
) -> Tuple[LibraryDoc, Optional[LibraryMetaData]]:
|
|
1676
1944
|
with self._logger.measure_time(lambda: f"loading library {name}{args!r}", context_name="import"):
|
|
1677
1945
|
source = self.find_library(name, base_dir, variables)
|
|
1678
1946
|
|
|
@@ -1701,39 +1969,48 @@ class ImportsManager:
|
|
|
1701
1969
|
|
|
1702
1970
|
if not entry.ignore_reference and sentinel is not None and sentinel not in entry.references:
|
|
1703
1971
|
fin = weakref.finalize(sentinel, self.__remove_library_entry, entry_key, entry)
|
|
1704
|
-
fin.atexit = False
|
|
1972
|
+
fin.atexit = False
|
|
1705
1973
|
entry.references.add(sentinel)
|
|
1706
1974
|
|
|
1707
|
-
return entry.
|
|
1975
|
+
return entry.get_libdoc_with_meta()
|
|
1708
1976
|
|
|
1709
|
-
def
|
|
1710
|
-
|
|
1711
|
-
|
|
1977
|
+
def get_libdoc_for_library_import(
|
|
1978
|
+
self,
|
|
1979
|
+
name: str,
|
|
1980
|
+
args: Tuple[Any, ...],
|
|
1981
|
+
base_dir: str,
|
|
1982
|
+
sentinel: Any = None,
|
|
1983
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
1984
|
+
) -> LibraryDoc:
|
|
1985
|
+
return self.get_libdoc_for_library_import_with_meta(name, args, base_dir, sentinel, variables)[0]
|
|
1712
1986
|
|
|
1713
1987
|
@_logger.call
|
|
1714
1988
|
def get_libdoc_from_model(
|
|
1715
1989
|
self,
|
|
1716
1990
|
model: ast.AST,
|
|
1717
1991
|
source: str,
|
|
1992
|
+
meta: Optional[RobotFileMeta] = None,
|
|
1718
1993
|
) -> ResourceDoc:
|
|
1719
|
-
|
|
1994
|
+
"""Build (or load from cache) the ResourceDoc for a parsed model.
|
|
1995
|
+
|
|
1996
|
+
``meta`` must describe the disk state of exactly the content the model
|
|
1997
|
+
was parsed from; the disk cache is only consulted and written when such
|
|
1998
|
+
a trusted meta is available. There is deliberately no fallback to a
|
|
1999
|
+
fresh stat here — the file may have changed since the model's text was
|
|
2000
|
+
read, and labeling old content with a newer state would make the cache
|
|
2001
|
+
entry validate as fresh forever.
|
|
2002
|
+
"""
|
|
1720
2003
|
entry = self._resource_libdoc_cache.get(model)
|
|
1721
2004
|
if entry is not None and source in entry:
|
|
1722
2005
|
return entry[source]
|
|
1723
2006
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
if result is None:
|
|
2007
|
+
if meta is not None and meta.trusted:
|
|
2008
|
+
result = self._get_model_doc_cached(source, meta)
|
|
2009
|
+
if result is None:
|
|
2010
|
+
result = get_model_doc(model=model, source=source)
|
|
2011
|
+
self._save_model_doc_cache(source, result, meta)
|
|
2012
|
+
else:
|
|
1731
2013
|
result = get_model_doc(model=model, source=source)
|
|
1732
|
-
if use_disk_cache:
|
|
1733
|
-
if meta is None:
|
|
1734
|
-
meta = self.get_resource_meta(source)
|
|
1735
|
-
if meta is not None:
|
|
1736
|
-
self._save_model_doc_cache(source, result, meta)
|
|
1737
2014
|
|
|
1738
2015
|
if entry is None:
|
|
1739
2016
|
entry = {}
|
|
@@ -1745,22 +2022,23 @@ class ImportsManager:
|
|
|
1745
2022
|
|
|
1746
2023
|
@staticmethod
|
|
1747
2024
|
def get_resource_meta(source: str) -> Optional[RobotFileMeta]:
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
return RobotFileMeta(normalized, mtime_ns)
|
|
1752
|
-
except OSError:
|
|
2025
|
+
normalized = str(normalized_path(source))
|
|
2026
|
+
info = probe_disk_info(normalized)
|
|
2027
|
+
if info is None:
|
|
1753
2028
|
return None
|
|
2029
|
+
return RobotFileMeta(normalized, info)
|
|
1754
2030
|
|
|
1755
|
-
def _get_model_doc_cached(self, source: str) ->
|
|
1756
|
-
|
|
1757
|
-
if
|
|
1758
|
-
return
|
|
2031
|
+
def _get_model_doc_cached(self, source: str, meta: RobotFileMeta) -> Optional[ResourceDoc]:
|
|
2032
|
+
known = self._model_docs_by_source.get(meta.source)
|
|
2033
|
+
if known is not None and known[0] == meta:
|
|
2034
|
+
return known[1]
|
|
1759
2035
|
|
|
1760
2036
|
try:
|
|
1761
2037
|
entry = self.data_cache.read_entry(CacheSection.RESOURCE, meta.source, RobotFileMeta, ResourceDoc)
|
|
1762
|
-
|
|
1763
|
-
|
|
2038
|
+
# a doc read with other language settings would hold what the file defines under those settings
|
|
2039
|
+
if entry is not None and entry.meta == replace(meta, languages=self._languages_fingerprint):
|
|
2040
|
+
self._model_docs_by_source[meta.source] = (meta, entry.data)
|
|
2041
|
+
return entry.data
|
|
1764
2042
|
except (SystemExit, KeyboardInterrupt):
|
|
1765
2043
|
raise
|
|
1766
2044
|
except BaseException as e:
|
|
@@ -1770,12 +2048,15 @@ class ImportsManager:
|
|
|
1770
2048
|
context_name="import",
|
|
1771
2049
|
)
|
|
1772
2050
|
|
|
1773
|
-
return None
|
|
2051
|
+
return None
|
|
1774
2052
|
|
|
1775
2053
|
def _save_model_doc_cache(self, source: str, result: ResourceDoc, meta: "RobotFileMeta") -> None:
|
|
2054
|
+
self._model_docs_by_source[meta.source] = (meta, result)
|
|
1776
2055
|
|
|
1777
2056
|
try:
|
|
1778
|
-
self.data_cache.save_entry(
|
|
2057
|
+
self.data_cache.save_entry(
|
|
2058
|
+
CacheSection.RESOURCE, meta.source, replace(meta, languages=self._languages_fingerprint), result
|
|
2059
|
+
)
|
|
1779
2060
|
except (SystemExit, KeyboardInterrupt):
|
|
1780
2061
|
raise
|
|
1781
2062
|
except BaseException as e:
|
|
@@ -1795,7 +2076,7 @@ class ImportsManager:
|
|
|
1795
2076
|
resolve_variables: bool = True,
|
|
1796
2077
|
resolve_command_line_vars: bool = True,
|
|
1797
2078
|
) -> Tuple[VariablesDoc, Optional[LibraryMetaData]]:
|
|
1798
|
-
meta,
|
|
2079
|
+
meta, import_name = self.get_variables_meta(
|
|
1799
2080
|
name,
|
|
1800
2081
|
base_dir,
|
|
1801
2082
|
variables,
|
|
@@ -1806,7 +2087,7 @@ class ImportsManager:
|
|
|
1806
2087
|
if meta is not None:
|
|
1807
2088
|
try:
|
|
1808
2089
|
entry = self.data_cache.read_entry(
|
|
1809
|
-
CacheSection.VARIABLES, meta.
|
|
2090
|
+
CacheSection.VARIABLES, meta.cache_key_for_args(args), LibraryMetaData, VariablesDoc
|
|
1810
2091
|
)
|
|
1811
2092
|
if entry is not None and entry.meta == meta:
|
|
1812
2093
|
return entry.data, meta
|
|
@@ -1815,25 +2096,47 @@ class ImportsManager:
|
|
|
1815
2096
|
except BaseException as e:
|
|
1816
2097
|
self._logger.exception(e)
|
|
1817
2098
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2099
|
+
try:
|
|
2100
|
+
result = self._run_in_subprocess(
|
|
2101
|
+
get_variables_doc,
|
|
2102
|
+
(
|
|
2103
|
+
name,
|
|
2104
|
+
args,
|
|
2105
|
+
working_dir,
|
|
2106
|
+
base_dir,
|
|
2107
|
+
self.get_resolvable_command_line_variables() if resolve_command_line_vars else None,
|
|
2108
|
+
variables,
|
|
2109
|
+
),
|
|
2110
|
+
f"Loading variables {name!r} with args {args!r}",
|
|
2111
|
+
)
|
|
2112
|
+
except (LoadTimeoutError, LoadExitError) as e:
|
|
2113
|
+
# see _get_library_libdoc
|
|
2114
|
+
return (
|
|
2115
|
+
VariablesDoc(
|
|
2116
|
+
name=name,
|
|
2117
|
+
source=(
|
|
2118
|
+
meta.origin if meta is not None else import_name if is_variables_by_path(import_name) else None
|
|
2119
|
+
),
|
|
2120
|
+
errors=[
|
|
2121
|
+
Error(
|
|
2122
|
+
message=str(e),
|
|
2123
|
+
type_name=(
|
|
2124
|
+
DiagnosticError.VARIABLES_TIMEOUT_ERROR
|
|
2125
|
+
if isinstance(e, LoadTimeoutError)
|
|
2126
|
+
else DiagnosticError.VARIABLES_EXIT_ERROR
|
|
2127
|
+
),
|
|
2128
|
+
)
|
|
2129
|
+
],
|
|
2130
|
+
),
|
|
2131
|
+
None,
|
|
2132
|
+
)
|
|
1830
2133
|
|
|
1831
2134
|
self._save_import_cache(CacheSection.VARIABLES, meta, result, "variables", name, args)
|
|
1832
2135
|
|
|
1833
2136
|
return result, meta
|
|
1834
2137
|
|
|
1835
2138
|
@_logger.call
|
|
1836
|
-
def
|
|
2139
|
+
def get_libdoc_for_variables_import_with_meta(
|
|
1837
2140
|
self,
|
|
1838
2141
|
name: str,
|
|
1839
2142
|
args: Tuple[Any, ...],
|
|
@@ -1842,7 +2145,7 @@ class ImportsManager:
|
|
|
1842
2145
|
variables: Optional[Dict[str, Any]] = None,
|
|
1843
2146
|
resolve_variables: bool = True,
|
|
1844
2147
|
resolve_command_line_vars: bool = True,
|
|
1845
|
-
) -> VariablesDoc:
|
|
2148
|
+
) -> Tuple[VariablesDoc, Optional[LibraryMetaData]]:
|
|
1846
2149
|
with self._logger.measure_time(lambda: f"getting libdoc for variables import {name}", context_name="import"):
|
|
1847
2150
|
source = self.find_variables(name, base_dir, variables, resolve_variables, resolve_command_line_vars)
|
|
1848
2151
|
|
|
@@ -1877,9 +2180,29 @@ class ImportsManager:
|
|
|
1877
2180
|
if sentinel is not None and sentinel not in entry.references:
|
|
1878
2181
|
entry.references.add(sentinel)
|
|
1879
2182
|
fin = weakref.finalize(sentinel, self.__remove_variables_entry, entry_key, entry)
|
|
1880
|
-
fin.atexit = False
|
|
2183
|
+
fin.atexit = False
|
|
1881
2184
|
|
|
1882
|
-
return entry.
|
|
2185
|
+
return entry.get_libdoc_with_meta()
|
|
2186
|
+
|
|
2187
|
+
def get_libdoc_for_variables_import(
|
|
2188
|
+
self,
|
|
2189
|
+
name: str,
|
|
2190
|
+
args: Tuple[Any, ...],
|
|
2191
|
+
base_dir: str,
|
|
2192
|
+
sentinel: Any = None,
|
|
2193
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
2194
|
+
resolve_variables: bool = True,
|
|
2195
|
+
resolve_command_line_vars: bool = True,
|
|
2196
|
+
) -> VariablesDoc:
|
|
2197
|
+
return self.get_libdoc_for_variables_import_with_meta(
|
|
2198
|
+
name,
|
|
2199
|
+
args,
|
|
2200
|
+
base_dir,
|
|
2201
|
+
sentinel,
|
|
2202
|
+
variables,
|
|
2203
|
+
resolve_variables,
|
|
2204
|
+
resolve_command_line_vars,
|
|
2205
|
+
)[0]
|
|
1883
2206
|
|
|
1884
2207
|
@_logger.call
|
|
1885
2208
|
def _get_entry_for_resource_import(
|
|
@@ -1905,7 +2228,7 @@ class ImportsManager:
|
|
|
1905
2228
|
if sentinel is not None and sentinel not in entry.references:
|
|
1906
2229
|
entry.references.add(sentinel)
|
|
1907
2230
|
fin = weakref.finalize(sentinel, self.__remove_resource_entry, entry_key, entry)
|
|
1908
|
-
fin.atexit = False
|
|
2231
|
+
fin.atexit = False
|
|
1909
2232
|
|
|
1910
2233
|
return entry
|
|
1911
2234
|
|
|
@@ -1929,7 +2252,7 @@ class ImportsManager:
|
|
|
1929
2252
|
|
|
1930
2253
|
return namespace, libdoc
|
|
1931
2254
|
|
|
1932
|
-
def
|
|
2255
|
+
def get_resource_doc_for_resource_import_with_meta(
|
|
1933
2256
|
self,
|
|
1934
2257
|
name: str,
|
|
1935
2258
|
base_dir: str,
|
|
@@ -1937,11 +2260,24 @@ class ImportsManager:
|
|
|
1937
2260
|
variables: Optional[Dict[str, Any]] = None,
|
|
1938
2261
|
*,
|
|
1939
2262
|
source: Optional[str] = None,
|
|
1940
|
-
) -> ResourceDoc:
|
|
2263
|
+
) -> Tuple[ResourceDoc, Optional[RobotFileMeta]]:
|
|
1941
2264
|
with self._logger.measure_time(lambda: f"getting resource doc for {name}", context_name="import"):
|
|
1942
2265
|
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
|
|
1943
2266
|
|
|
1944
|
-
return entry.
|
|
2267
|
+
return entry.get_resource_doc_with_meta()
|
|
2268
|
+
|
|
2269
|
+
def get_resource_doc_for_resource_import(
|
|
2270
|
+
self,
|
|
2271
|
+
name: str,
|
|
2272
|
+
base_dir: str,
|
|
2273
|
+
sentinel: Any = None,
|
|
2274
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
2275
|
+
*,
|
|
2276
|
+
source: Optional[str] = None,
|
|
2277
|
+
) -> ResourceDoc:
|
|
2278
|
+
return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables, source=source)[
|
|
2279
|
+
0
|
|
2280
|
+
]
|
|
1945
2281
|
|
|
1946
2282
|
def complete_library_import(
|
|
1947
2283
|
self,
|