robotcode-robot 2.6.2__py3-none-any.whl → 2.7.0__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/model.py +29 -0
- robotcode/robot/diagnostics/document_cache_helper.py +84 -11
- robotcode/robot/diagnostics/import_resolver.py +20 -4
- robotcode/robot/diagnostics/imports_manager.py +269 -154
- robotcode/robot/diagnostics/namespace.py +24 -3
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.7.0.dist-info}/METADATA +2 -2
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.7.0.dist-info}/RECORD +9 -9
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.7.0.dist-info}/WHEEL +1 -1
robotcode/robot/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.7.0"
|
robotcode/robot/config/model.py
CHANGED
|
@@ -2912,6 +2912,23 @@ class RobotBaseProfile(CommonOptions, CommonExtendOptions, RobotOptions, RobotEx
|
|
|
2912
2912
|
"""
|
|
2913
2913
|
)
|
|
2914
2914
|
|
|
2915
|
+
wrapper: Optional[List[Union[str, StringExpression]]] = field(
|
|
2916
|
+
description="""\
|
|
2917
|
+
Command prefix the test run is executed through (the actual
|
|
2918
|
+
`robotcode` command line is appended to it), e.g. to bring up the
|
|
2919
|
+
services, servers, mocks or session the tests need around them. For
|
|
2920
|
+
plain environment variables use `env` instead. The selected
|
|
2921
|
+
profile's `env` is applied first, so the wrapper can rely on it.
|
|
2922
|
+
|
|
2923
|
+
Examples:
|
|
2924
|
+
```toml
|
|
2925
|
+
[profiles.integration]
|
|
2926
|
+
wrapper = ["./with-test-services.sh"]
|
|
2927
|
+
# or a ready-made tool, e.g. ["xvfb-run", "-a"] for headless UI tests
|
|
2928
|
+
```
|
|
2929
|
+
""",
|
|
2930
|
+
)
|
|
2931
|
+
|
|
2915
2932
|
rebot: Optional[RebotProfile] = field(
|
|
2916
2933
|
description="""\
|
|
2917
2934
|
Options to be passed to _rebot_.
|
|
@@ -2979,6 +2996,18 @@ class RobotExtendBaseProfile(RobotBaseProfile):
|
|
|
2979
2996
|
alias="extend-env",
|
|
2980
2997
|
)
|
|
2981
2998
|
|
|
2999
|
+
extend_wrapper: Optional[List[Union[str, StringExpression]]] = field(
|
|
3000
|
+
description="""\
|
|
3001
|
+
Append extra entries to the `wrapper` command prefix.
|
|
3002
|
+
|
|
3003
|
+
Examples:
|
|
3004
|
+
```toml
|
|
3005
|
+
extend-wrapper = ["--some-flag"]
|
|
3006
|
+
```
|
|
3007
|
+
""",
|
|
3008
|
+
alias="extend-wrapper",
|
|
3009
|
+
)
|
|
3010
|
+
|
|
2982
3011
|
extend_paths: Union[str, List[str], None] = field(
|
|
2983
3012
|
description="""\
|
|
2984
3013
|
Append extra entries to the paths argument.
|
|
@@ -24,6 +24,7 @@ from robotcode.core.filewatcher import FileWatcherManagerBase
|
|
|
24
24
|
from robotcode.core.text_document import TextDocument
|
|
25
25
|
from robotcode.core.uri import Uri
|
|
26
26
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
27
|
+
from robotcode.core.utils.path import DiskInfo
|
|
27
28
|
from robotcode.core.workspace import Workspace, WorkspaceFolder
|
|
28
29
|
from robotcode.robot.diagnostics.diagnostics_modifier import (
|
|
29
30
|
DiagnosticModifiersConfig,
|
|
@@ -58,6 +59,18 @@ class UnknownFileTypeError(Exception):
|
|
|
58
59
|
pass
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def _namespace_cache_key(source: str, document_type: Optional[DocumentType]) -> str:
|
|
63
|
+
"""Disk-cache entry name for a namespace.
|
|
64
|
+
|
|
65
|
+
Includes the document type because the same file can be analyzed both as
|
|
66
|
+
a suite (GENERAL) and as an imported resource (RESOURCE) — the results
|
|
67
|
+
must not overwrite each other. "\\n" as separator is impossible in Windows
|
|
68
|
+
paths and never appears in real-world POSIX paths (where it is technically
|
|
69
|
+
legal); the key is an opaque entry name and is never parsed back.
|
|
70
|
+
"""
|
|
71
|
+
return f"{source}\n{document_type.value if document_type is not None else ''}"
|
|
72
|
+
|
|
73
|
+
|
|
61
74
|
class _CacheEntry:
|
|
62
75
|
pass
|
|
63
76
|
|
|
@@ -438,11 +451,16 @@ class DocumentsCacheHelper:
|
|
|
438
451
|
imports_manager = self.get_imports_manager(document)
|
|
439
452
|
semantic_model_enabled = self._is_semantic_model_enabled(document)
|
|
440
453
|
|
|
454
|
+
# Capture the disk state before the model is parsed: this snapshot can
|
|
455
|
+
# only be older than the analyzed content, never newer, so anything
|
|
456
|
+
# persisted under it self-heals with a cache miss.
|
|
457
|
+
disk_info = document.disk_info
|
|
458
|
+
|
|
441
459
|
# --- Try disk cache (cold-start acceleration) ---
|
|
442
460
|
cache_namespaces = self.analysis_config.cache.cache_namespaces
|
|
443
|
-
if cache_namespaces and document.version is None:
|
|
461
|
+
if cache_namespaces and document.version is None and disk_info is not None:
|
|
444
462
|
result = self._try_load_cached_namespace(
|
|
445
|
-
source, document, document_type, imports_manager, semantic_model_enabled
|
|
463
|
+
source, document, document_type, imports_manager, semantic_model_enabled, disk_info
|
|
446
464
|
)
|
|
447
465
|
if result is not None:
|
|
448
466
|
return result
|
|
@@ -457,6 +475,13 @@ class DocumentsCacheHelper:
|
|
|
457
475
|
else:
|
|
458
476
|
model = self.get_model(document)
|
|
459
477
|
|
|
478
|
+
# The document may have been re-read from disk while the model was
|
|
479
|
+
# parsed; the captured snapshot then no longer describes the parsed
|
|
480
|
+
# content and must not label anything written during the build (the
|
|
481
|
+
# builder writes RESOURCE cache entries under this snapshot).
|
|
482
|
+
if disk_info is not None and document.disk_info != disk_info:
|
|
483
|
+
disk_info = None
|
|
484
|
+
|
|
460
485
|
languages, workspace_languages = self.build_languages_from_model(document, model)
|
|
461
486
|
builder = NamespaceBuilder(
|
|
462
487
|
imports_manager,
|
|
@@ -466,14 +491,20 @@ class DocumentsCacheHelper:
|
|
|
466
491
|
document_type,
|
|
467
492
|
languages,
|
|
468
493
|
workspace_languages,
|
|
494
|
+
disk_info=disk_info,
|
|
469
495
|
)
|
|
470
496
|
builder.set_semantic_model_enabled(semantic_model_enabled)
|
|
471
497
|
|
|
472
498
|
result = builder.build()
|
|
473
499
|
|
|
474
|
-
# Save to disk cache
|
|
475
|
-
|
|
476
|
-
|
|
500
|
+
# Save to disk cache — the re-checks in _namespace_is_cacheable catch
|
|
501
|
+
# a watcher re-read (disk_info) and a didOpen on another thread
|
|
502
|
+
# (version) that happened during the build.
|
|
503
|
+
if cache_namespaces and self._namespace_is_cacheable(document, disk_info):
|
|
504
|
+
assert disk_info is not None
|
|
505
|
+
self._save_namespace_to_cache(
|
|
506
|
+
source, document, document_type, result, imports_manager, disk_info, semantic_model_enabled
|
|
507
|
+
)
|
|
477
508
|
|
|
478
509
|
# Update the folder-scoped reference index
|
|
479
510
|
self.get_project_index(document).update_file(result.source, result)
|
|
@@ -491,6 +522,18 @@ class DocumentsCacheHelper:
|
|
|
491
522
|
experimental_config = self.workspace.get_configuration(ExperimentalConfig, document.uri)
|
|
492
523
|
return self.analysis_config.semantic_model or experimental_config.semantic_model
|
|
493
524
|
|
|
525
|
+
@staticmethod
|
|
526
|
+
def _namespace_is_cacheable(document: TextDocument, disk_info: Optional[DiskInfo]) -> bool:
|
|
527
|
+
"""Whether a namespace built from this document may be persisted.
|
|
528
|
+
|
|
529
|
+
Only when the analyzed content is the on-disk state: not an editor
|
|
530
|
+
buffer (version), backed by a trusted snapshot, and the document still
|
|
531
|
+
holds exactly that snapshot.
|
|
532
|
+
"""
|
|
533
|
+
return (
|
|
534
|
+
document.version is None and disk_info is not None and disk_info.trusted and document.disk_info == disk_info
|
|
535
|
+
)
|
|
536
|
+
|
|
494
537
|
def _try_load_cached_namespace(
|
|
495
538
|
self,
|
|
496
539
|
source: str,
|
|
@@ -498,10 +541,14 @@ class DocumentsCacheHelper:
|
|
|
498
541
|
document_type: Optional[DocumentType],
|
|
499
542
|
imports_manager: ImportsManager,
|
|
500
543
|
semantic_model_enabled: bool,
|
|
544
|
+
disk_info: DiskInfo,
|
|
501
545
|
) -> Optional[Namespace]:
|
|
502
546
|
"""Attempt to load a Namespace from the disk cache.
|
|
503
547
|
|
|
504
|
-
|
|
548
|
+
``disk_info`` is the snapshot of the document content the namespace
|
|
549
|
+
will represent; the cached entry is validated against it, not against
|
|
550
|
+
the current disk state. Returns None on cache miss or validation
|
|
551
|
+
failure.
|
|
505
552
|
"""
|
|
506
553
|
data_cache = imports_manager.data_cache
|
|
507
554
|
|
|
@@ -510,7 +557,9 @@ class DocumentsCacheHelper:
|
|
|
510
557
|
return None
|
|
511
558
|
|
|
512
559
|
try:
|
|
513
|
-
entry = data_cache.read_entry(
|
|
560
|
+
entry = data_cache.read_entry(
|
|
561
|
+
CacheSection.NAMESPACE, _namespace_cache_key(source, document_type), NamespaceMetaData, NamespaceData
|
|
562
|
+
)
|
|
514
563
|
except (SystemExit, KeyboardInterrupt):
|
|
515
564
|
raise
|
|
516
565
|
except BaseException as e:
|
|
@@ -536,7 +585,7 @@ class DocumentsCacheHelper:
|
|
|
536
585
|
)
|
|
537
586
|
return None
|
|
538
587
|
|
|
539
|
-
if not imports_manager.validate_namespace_meta(meta):
|
|
588
|
+
if not imports_manager.validate_namespace_meta(meta, disk_info):
|
|
540
589
|
self._logger.debug(
|
|
541
590
|
lambda: f"Cache miss for {source}: cached entry is stale, re-analyzing",
|
|
542
591
|
context_name="cache",
|
|
@@ -588,19 +637,43 @@ class DocumentsCacheHelper:
|
|
|
588
637
|
def _save_namespace_to_cache(
|
|
589
638
|
self,
|
|
590
639
|
source: str,
|
|
640
|
+
document: TextDocument,
|
|
641
|
+
document_type: Optional[DocumentType],
|
|
591
642
|
namespace: Namespace,
|
|
592
643
|
imports_manager: ImportsManager,
|
|
644
|
+
disk_info: DiskInfo,
|
|
593
645
|
semantic_model_enabled: bool,
|
|
594
646
|
) -> None:
|
|
595
|
-
"""Save a Namespace to the disk cache.
|
|
647
|
+
"""Save a Namespace to the disk cache.
|
|
648
|
+
|
|
649
|
+
``disk_info`` must be the snapshot captured before the namespace was
|
|
650
|
+
built; the entry is skipped when the namespace's state is not fully
|
|
651
|
+
trustworthy or the document changed in the meantime.
|
|
652
|
+
"""
|
|
596
653
|
try:
|
|
597
654
|
meta = imports_manager.build_namespace_meta(
|
|
598
|
-
source, namespace, semantic_model_enabled=semantic_model_enabled
|
|
655
|
+
source, namespace, disk_info, semantic_model_enabled=semantic_model_enabled
|
|
599
656
|
)
|
|
657
|
+
if meta is None:
|
|
658
|
+
self._logger.debug(
|
|
659
|
+
lambda: f"Skip caching namespace for {source}: state not trustworthy",
|
|
660
|
+
context_name="cache",
|
|
661
|
+
)
|
|
662
|
+
return
|
|
663
|
+
|
|
600
664
|
data = namespace.to_data()
|
|
601
665
|
|
|
666
|
+
# to_data() takes time — re-check that the document still holds
|
|
667
|
+
# exactly the content this namespace was built from.
|
|
668
|
+
if not self._namespace_is_cacheable(document, disk_info):
|
|
669
|
+
self._logger.debug(
|
|
670
|
+
lambda: f"Skip caching namespace for {source}: document changed during save",
|
|
671
|
+
context_name="cache",
|
|
672
|
+
)
|
|
673
|
+
return
|
|
674
|
+
|
|
602
675
|
data_cache = imports_manager.data_cache
|
|
603
|
-
data_cache.save_entry(CacheSection.NAMESPACE, source, meta, data)
|
|
676
|
+
data_cache.save_entry(CacheSection.NAMESPACE, _namespace_cache_key(source, document_type), meta, data)
|
|
604
677
|
except (SystemExit, KeyboardInterrupt):
|
|
605
678
|
raise
|
|
606
679
|
except BaseException as e:
|
|
@@ -59,6 +59,12 @@ class ResolvedImports:
|
|
|
59
59
|
resources: Dict[str, ResourceEntry] = field(default_factory=dict)
|
|
60
60
|
variables_imports: Dict[str, VariablesEntry] = field(default_factory=dict)
|
|
61
61
|
|
|
62
|
+
# Metadata of each resolved dependency, captured together with its doc at
|
|
63
|
+
# resolve time (keys: "lib:{import_name}", "res:{source}", "var:{import_name}").
|
|
64
|
+
# A None value means the dependency was resolved but no trustworthy meta
|
|
65
|
+
# exists — namespaces built from it must not be persisted.
|
|
66
|
+
dependency_metas: Dict[str, Optional[Any]] = field(default_factory=dict)
|
|
67
|
+
|
|
62
68
|
|
|
63
69
|
class ImportResolver:
|
|
64
70
|
"""Resolves all imports for a Robot Framework file.
|
|
@@ -78,6 +84,7 @@ class ImportResolver:
|
|
|
78
84
|
|
|
79
85
|
__slots__ = (
|
|
80
86
|
"_base_dir",
|
|
87
|
+
"_dependency_metas",
|
|
81
88
|
"_diagnostics",
|
|
82
89
|
"_import_entries",
|
|
83
90
|
"_imports_manager",
|
|
@@ -115,6 +122,7 @@ class ImportResolver:
|
|
|
115
122
|
self._variables: Dict[str, Any] = {}
|
|
116
123
|
self._variables_dirty = True
|
|
117
124
|
self._source_hints: Dict[str, str] = {}
|
|
125
|
+
self._dependency_metas: Dict[str, Optional[Any]] = {}
|
|
118
126
|
|
|
119
127
|
def resolve(self, imports: Iterable[Import], *, source_hints: Optional[Dict[str, str]] = None) -> ResolvedImports:
|
|
120
128
|
"""Resolve all imports and return the result."""
|
|
@@ -129,6 +137,7 @@ class ImportResolver:
|
|
|
129
137
|
variables_imports=self._variables_imports,
|
|
130
138
|
import_entries=self._import_entries,
|
|
131
139
|
diagnostics=self._diagnostics,
|
|
140
|
+
dependency_metas=self._dependency_metas,
|
|
132
141
|
)
|
|
133
142
|
|
|
134
143
|
# ------------------------------------------------------------------
|
|
@@ -186,13 +195,14 @@ class ImportResolver:
|
|
|
186
195
|
):
|
|
187
196
|
for library in DEFAULT_LIBRARIES:
|
|
188
197
|
try:
|
|
189
|
-
library_doc = self._imports_manager.
|
|
198
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_library_import_with_meta(
|
|
190
199
|
library,
|
|
191
200
|
(),
|
|
192
201
|
base_dir=self._base_dir,
|
|
193
202
|
sentinel=None,
|
|
194
203
|
variables=self._variables,
|
|
195
204
|
)
|
|
205
|
+
self._dependency_metas[f"lib:{library}"] = meta
|
|
196
206
|
entry = LibraryEntry(
|
|
197
207
|
name=library_doc.name,
|
|
198
208
|
import_name=library,
|
|
@@ -312,13 +322,14 @@ class ImportResolver:
|
|
|
312
322
|
def _import_library(self, imp: LibraryImport, base_dir: str, *, top_level: bool) -> None:
|
|
313
323
|
assert imp.name is not None
|
|
314
324
|
|
|
315
|
-
library_doc = self._imports_manager.
|
|
325
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_library_import_with_meta(
|
|
316
326
|
imp.name,
|
|
317
327
|
imp.args,
|
|
318
328
|
base_dir=base_dir,
|
|
319
329
|
sentinel=self._sentinel,
|
|
320
330
|
variables=self._variables,
|
|
321
331
|
)
|
|
332
|
+
self._dependency_metas[f"lib:{imp.name}"] = meta
|
|
322
333
|
entry = LibraryEntry(
|
|
323
334
|
name=library_doc.name,
|
|
324
335
|
import_name=imp.name,
|
|
@@ -407,13 +418,17 @@ class ImportResolver:
|
|
|
407
418
|
if hint is not None and os.path.isfile(hint):
|
|
408
419
|
source_hint = hint
|
|
409
420
|
|
|
410
|
-
resource_doc = self._imports_manager.
|
|
421
|
+
resource_doc, meta = self._imports_manager.get_resource_doc_for_resource_import_with_meta(
|
|
411
422
|
imp.name,
|
|
412
423
|
base_dir,
|
|
413
424
|
sentinel=self._sentinel,
|
|
414
425
|
variables=self._variables,
|
|
415
426
|
source=source_hint,
|
|
416
427
|
)
|
|
428
|
+
if resource_doc.source is not None:
|
|
429
|
+
# setdefault: on re-imports of the same resource the meta captured
|
|
430
|
+
# with the first (analyzed) doc wins.
|
|
431
|
+
self._dependency_metas.setdefault(f"res:{resource_doc.source}", meta)
|
|
417
432
|
entry = ResourceEntry(
|
|
418
433
|
name=resource_doc.name,
|
|
419
434
|
import_name=imp.name,
|
|
@@ -535,13 +550,14 @@ class ImportResolver:
|
|
|
535
550
|
def _import_variables(self, imp: VariablesImport, base_dir: str, *, top_level: bool) -> None:
|
|
536
551
|
assert imp.name is not None
|
|
537
552
|
|
|
538
|
-
library_doc = self._imports_manager.
|
|
553
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_variables_import_with_meta(
|
|
539
554
|
imp.name,
|
|
540
555
|
imp.args,
|
|
541
556
|
base_dir=base_dir,
|
|
542
557
|
sentinel=self._sentinel,
|
|
543
558
|
variables=self._variables,
|
|
544
559
|
)
|
|
560
|
+
self._dependency_metas[f"var:{imp.name}"] = meta
|
|
545
561
|
entry = VariablesEntry(
|
|
546
562
|
name=library_doc.name,
|
|
547
563
|
import_name=imp.name,
|
|
@@ -2,6 +2,7 @@ import ast
|
|
|
2
2
|
import multiprocessing as mp
|
|
3
3
|
import os
|
|
4
4
|
import threading
|
|
5
|
+
import time
|
|
5
6
|
import weakref
|
|
6
7
|
from abc import ABC, abstractmethod
|
|
7
8
|
from concurrent.futures import ProcessPoolExecutor, TimeoutError
|
|
@@ -42,7 +43,13 @@ from robotcode.core.uri import Uri
|
|
|
42
43
|
from robotcode.core.utils.caching import SimpleLRUCache
|
|
43
44
|
from robotcode.core.utils.glob_path import Pattern
|
|
44
45
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
45
|
-
from robotcode.core.utils.path import
|
|
46
|
+
from robotcode.core.utils.path import (
|
|
47
|
+
DiskInfo,
|
|
48
|
+
disk_info_from_stat,
|
|
49
|
+
normalized_path,
|
|
50
|
+
path_is_relative_to,
|
|
51
|
+
probe_disk_info,
|
|
52
|
+
)
|
|
46
53
|
|
|
47
54
|
from ..__version__ import __version__
|
|
48
55
|
from ..utils import RF_VERSION
|
|
@@ -272,14 +279,23 @@ class _LibrariesEntry(_ImportEntry):
|
|
|
272
279
|
with self._lock:
|
|
273
280
|
return self._lib_doc is not None
|
|
274
281
|
|
|
275
|
-
def
|
|
282
|
+
def get_libdoc_with_meta(self) -> Tuple[LibraryDoc, Optional["LibraryMetaData"]]:
|
|
283
|
+
"""Return the LibraryDoc together with the meta of the state it was built from.
|
|
284
|
+
|
|
285
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
286
|
+
the same state — reading ``get_libdoc()`` and ``meta`` separately can
|
|
287
|
+
interleave with an invalidation and pair a doc with a newer meta.
|
|
288
|
+
"""
|
|
276
289
|
with self._lock:
|
|
277
290
|
if self._lib_doc is None:
|
|
278
291
|
self._update()
|
|
279
292
|
|
|
280
293
|
assert self._lib_doc is not None
|
|
281
294
|
|
|
282
|
-
return self._lib_doc
|
|
295
|
+
return self._lib_doc, self._meta
|
|
296
|
+
|
|
297
|
+
def get_libdoc(self) -> LibraryDoc:
|
|
298
|
+
return self.get_libdoc_with_meta()[0]
|
|
283
299
|
|
|
284
300
|
|
|
285
301
|
@dataclass(frozen=True, slots=True)
|
|
@@ -343,7 +359,11 @@ class _ResourcesEntry(_ImportEntry):
|
|
|
343
359
|
)
|
|
344
360
|
|
|
345
361
|
self._document = self.parent.documents_manager.get_or_open_document(self.source_path)
|
|
346
|
-
|
|
362
|
+
# Bind the meta to the document's disk state instead of a fresh stat, so
|
|
363
|
+
# it describes exactly the content this entry's ResourceDoc is built
|
|
364
|
+
# from. A dirty/editor-open document yields no meta — its state must
|
|
365
|
+
# never be persisted.
|
|
366
|
+
self._meta = RobotFileMeta.from_document(self._document)
|
|
347
367
|
|
|
348
368
|
if self._document._version is None:
|
|
349
369
|
self.file_watchers.append(
|
|
@@ -390,15 +410,21 @@ class _ResourcesEntry(_ImportEntry):
|
|
|
390
410
|
def _get_namespace(self) -> "Namespace":
|
|
391
411
|
return self.parent.get_namespace_for_resource(self._get_document())
|
|
392
412
|
|
|
393
|
-
def
|
|
413
|
+
def get_resource_doc_with_meta(self) -> Tuple[ResourceDoc, Optional["RobotFileMeta"]]:
|
|
414
|
+
"""Return the ResourceDoc together with the meta of the content it was built from.
|
|
415
|
+
|
|
416
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
417
|
+
the same state — reading ``get_libdoc()`` and ``meta`` separately can
|
|
418
|
+
interleave with an invalidation and pair a doc with a newer meta.
|
|
419
|
+
"""
|
|
394
420
|
with self._lock:
|
|
395
421
|
if self._lib_doc is None:
|
|
396
422
|
self._lib_doc = self.parent.get_resource_doc_from_document(self._get_document())
|
|
397
423
|
|
|
398
|
-
return self._lib_doc
|
|
424
|
+
return self._lib_doc, self._meta
|
|
399
425
|
|
|
400
426
|
def get_libdoc(self) -> ResourceDoc:
|
|
401
|
-
return self.
|
|
427
|
+
return self.get_resource_doc_with_meta()[0]
|
|
402
428
|
|
|
403
429
|
|
|
404
430
|
@dataclass(frozen=True, slots=True)
|
|
@@ -495,14 +521,22 @@ class _VariablesEntry(_ImportEntry):
|
|
|
495
521
|
with self._lock:
|
|
496
522
|
return self._lib_doc is not None
|
|
497
523
|
|
|
498
|
-
def
|
|
524
|
+
def get_libdoc_with_meta(self) -> Tuple[VariablesDoc, Optional["LibraryMetaData"]]:
|
|
525
|
+
"""Return the VariablesDoc together with the meta of the state it was built from.
|
|
526
|
+
|
|
527
|
+
Doc and meta are taken under the entry lock so they always belong to
|
|
528
|
+
the same state (see ``_LibrariesEntry.get_libdoc_with_meta``).
|
|
529
|
+
"""
|
|
499
530
|
with self._lock:
|
|
500
531
|
if self._lib_doc is None:
|
|
501
532
|
self._update()
|
|
502
533
|
|
|
503
534
|
assert self._lib_doc is not None
|
|
504
535
|
|
|
505
|
-
return self._lib_doc
|
|
536
|
+
return self._lib_doc, self._meta
|
|
537
|
+
|
|
538
|
+
def get_libdoc(self) -> VariablesDoc:
|
|
539
|
+
return self.get_libdoc_with_meta()[0]
|
|
506
540
|
|
|
507
541
|
|
|
508
542
|
@dataclass(slots=True)
|
|
@@ -513,7 +547,7 @@ class LibraryMetaData:
|
|
|
513
547
|
submodule_search_locations: Optional[List[str]]
|
|
514
548
|
by_path: bool
|
|
515
549
|
|
|
516
|
-
|
|
550
|
+
file_infos: Optional[Dict[str, DiskInfo]] = None
|
|
517
551
|
|
|
518
552
|
has_errors: bool = False
|
|
519
553
|
|
|
@@ -528,16 +562,34 @@ class LibraryMetaData:
|
|
|
528
562
|
|
|
529
563
|
raise ValueError("Cannot determine cache key.")
|
|
530
564
|
|
|
565
|
+
@property
|
|
566
|
+
def trusted(self) -> bool:
|
|
567
|
+
return self.file_infos is None or all(info.trusted for info in self.file_infos.values())
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _stat_library_file(path: str) -> os.stat_result:
|
|
571
|
+
# Follow symlinks so target changes behind an unchanged link are detected
|
|
572
|
+
# (editable installs); for a broken link fall back to the link's own state
|
|
573
|
+
# so a single dangling .py file doesn't make the whole library uncacheable.
|
|
574
|
+
try:
|
|
575
|
+
return os.stat(path)
|
|
576
|
+
except OSError:
|
|
577
|
+
return os.stat(path, follow_symlinks=False)
|
|
531
578
|
|
|
532
|
-
|
|
579
|
+
|
|
580
|
+
def _collect_library_file_infos(
|
|
533
581
|
origin: Optional[str],
|
|
534
582
|
submodule_search_locations: Optional[List[str]],
|
|
535
|
-
) -> Optional[Dict[str,
|
|
536
|
-
"""Collect
|
|
537
|
-
|
|
583
|
+
) -> Optional[Dict[str, DiskInfo]]:
|
|
584
|
+
"""Collect DiskInfo snapshots from origin and submodule_search_locations."""
|
|
585
|
+
file_infos: Dict[str, DiskInfo] = {}
|
|
586
|
+
|
|
587
|
+
# one capture moment for the whole collection, so all snapshots share the
|
|
588
|
+
# same racy-mtime reference point
|
|
589
|
+
now_ns = time.time_ns()
|
|
538
590
|
|
|
539
591
|
if origin is not None:
|
|
540
|
-
|
|
592
|
+
file_infos[origin] = disk_info_from_stat(_stat_library_file(origin), now_ns)
|
|
541
593
|
|
|
542
594
|
if submodule_search_locations:
|
|
543
595
|
for loc in submodule_search_locations:
|
|
@@ -545,9 +597,9 @@ def _collect_library_mtimes(
|
|
|
545
597
|
for filename in filenames:
|
|
546
598
|
if filename.endswith(".py"):
|
|
547
599
|
filepath = os.path.join(dirpath, filename)
|
|
548
|
-
|
|
600
|
+
file_infos[filepath] = disk_info_from_stat(_stat_library_file(filepath), now_ns)
|
|
549
601
|
|
|
550
|
-
return
|
|
602
|
+
return file_infos or None
|
|
551
603
|
|
|
552
604
|
|
|
553
605
|
def _matches_any_pattern(
|
|
@@ -564,7 +616,23 @@ def _matches_any_pattern(
|
|
|
564
616
|
@dataclass(slots=True)
|
|
565
617
|
class RobotFileMeta:
|
|
566
618
|
source: str
|
|
567
|
-
|
|
619
|
+
info: DiskInfo
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def trusted(self) -> bool:
|
|
623
|
+
return self.info.trusted
|
|
624
|
+
|
|
625
|
+
@classmethod
|
|
626
|
+
def from_document(cls, document: TextDocument) -> Optional["RobotFileMeta"]:
|
|
627
|
+
"""Meta describing exactly the disk state the document's text was read from.
|
|
628
|
+
|
|
629
|
+
``None`` when the document has no disk info, i.e. its text is not known
|
|
630
|
+
to match any on-disk state (editor buffer, read race).
|
|
631
|
+
"""
|
|
632
|
+
info = document.disk_info
|
|
633
|
+
if info is None:
|
|
634
|
+
return None
|
|
635
|
+
return cls(str(normalized_path(document.uri.to_path())), info)
|
|
568
636
|
|
|
569
637
|
|
|
570
638
|
@dataclass(slots=True)
|
|
@@ -576,7 +644,7 @@ class NamespaceMetaData:
|
|
|
576
644
|
"""
|
|
577
645
|
|
|
578
646
|
source: str
|
|
579
|
-
|
|
647
|
+
source_info: DiskInfo
|
|
580
648
|
config_fingerprint: Any
|
|
581
649
|
dependency_fingerprints: Dict[str, Any] = field(default_factory=dict)
|
|
582
650
|
semantic_model_enabled: bool = False
|
|
@@ -761,101 +829,76 @@ class ImportsManager:
|
|
|
761
829
|
"""
|
|
762
830
|
return self._config_fingerprint
|
|
763
831
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
832
|
+
@staticmethod
|
|
833
|
+
def _is_dependency_meta_trusted(value: Any) -> bool:
|
|
834
|
+
# fail closed: a meta type this dispatch does not know must never be
|
|
835
|
+
# persisted — silently treating it as trusted is the exact failure
|
|
836
|
+
# class the freshness system exists to prevent
|
|
837
|
+
return isinstance(value, (RobotFileMeta, LibraryMetaData)) and value.trusted
|
|
770
838
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
839
|
+
def build_namespace_meta(
|
|
840
|
+
self,
|
|
841
|
+
source: str,
|
|
842
|
+
namespace: "Namespace",
|
|
843
|
+
disk_info: DiskInfo,
|
|
844
|
+
*,
|
|
845
|
+
semantic_model_enabled: bool = False,
|
|
846
|
+
) -> Optional[NamespaceMetaData]:
|
|
847
|
+
"""Build a NamespaceMetaData for the given namespace and its dependencies.
|
|
848
|
+
|
|
849
|
+
``disk_info`` must be the snapshot of the content the namespace was
|
|
850
|
+
built from; the dependency fingerprints are the metas recorded during
|
|
851
|
+
import resolution. Returns ``None`` when any of that state is missing
|
|
852
|
+
or untrusted — such a namespace must not be persisted, because its
|
|
853
|
+
fingerprints could describe newer file states than the analyzed
|
|
854
|
+
content and would validate as fresh forever.
|
|
774
855
|
"""
|
|
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
|
|
803
|
-
|
|
804
|
-
for entry in namespace.variables_imports.values():
|
|
805
|
-
try:
|
|
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
|
|
856
|
+
if not disk_info.trusted:
|
|
857
|
+
return None
|
|
815
858
|
|
|
816
|
-
|
|
859
|
+
dependency_metas = namespace.dependency_metas
|
|
860
|
+
if dependency_metas is None:
|
|
861
|
+
return None
|
|
817
862
|
|
|
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
|
|
863
|
+
for value in dependency_metas.values():
|
|
864
|
+
if value is None or not self._is_dependency_meta_trusted(value):
|
|
865
|
+
return None
|
|
826
866
|
|
|
827
867
|
return NamespaceMetaData(
|
|
828
868
|
source=source,
|
|
829
|
-
|
|
869
|
+
source_info=disk_info,
|
|
830
870
|
config_fingerprint=self.config_fingerprint,
|
|
831
|
-
dependency_fingerprints=
|
|
871
|
+
dependency_fingerprints=dict(dependency_metas),
|
|
832
872
|
semantic_model_enabled=semantic_model_enabled,
|
|
833
873
|
)
|
|
834
874
|
|
|
835
|
-
def validate_namespace_meta(self, meta: NamespaceMetaData) -> bool:
|
|
836
|
-
"""Check whether a cached NamespaceMetaData is
|
|
875
|
+
def validate_namespace_meta(self, meta: NamespaceMetaData, source_disk_info: Optional[DiskInfo]) -> bool:
|
|
876
|
+
"""Check whether a cached NamespaceMetaData is fresh for the given content.
|
|
837
877
|
|
|
838
878
|
Performs a 2-level validation:
|
|
839
|
-
Level 1 (fast):
|
|
879
|
+
Level 1 (fast): source_disk_info, config_fingerprint
|
|
840
880
|
Level 2 (dependency check): each dependency fingerprint
|
|
881
|
+
|
|
882
|
+
Validation is against the *effective* content: the source is compared
|
|
883
|
+
to the snapshot of the document the namespace would represent, and
|
|
884
|
+
resource dependencies are checked document-first — a dirty editor
|
|
885
|
+
buffer fails validation so the namespace is rebuilt against the buffer
|
|
886
|
+
instead of being served from disk state.
|
|
841
887
|
"""
|
|
842
888
|
source = meta.source
|
|
843
889
|
|
|
844
890
|
# Level 1: fast checks
|
|
845
|
-
|
|
846
|
-
current_mtime = os.stat(source, follow_symlinks=False).st_mtime_ns
|
|
847
|
-
except OSError:
|
|
891
|
+
if source_disk_info is None:
|
|
848
892
|
self._logger.debug(
|
|
849
|
-
lambda: f"Cache miss for {source}: source
|
|
893
|
+
lambda: f"Cache miss for {source}: source has no trustworthy disk state",
|
|
850
894
|
context_name="cache",
|
|
851
895
|
)
|
|
852
896
|
return False
|
|
853
897
|
|
|
854
|
-
if meta.
|
|
898
|
+
if meta.source_info != source_disk_info:
|
|
855
899
|
self._logger.debug(
|
|
856
900
|
lambda: (
|
|
857
|
-
f"Cache miss for {source}: source
|
|
858
|
-
f" (cached={meta.source_mtime_ns}, current={current_mtime})"
|
|
901
|
+
f"Cache miss for {source}: source changed (cached={meta.source_info}, current={source_disk_info})"
|
|
859
902
|
),
|
|
860
903
|
context_name="cache",
|
|
861
904
|
)
|
|
@@ -874,7 +917,7 @@ class ImportsManager:
|
|
|
874
917
|
if key.startswith("lib:"):
|
|
875
918
|
lib_name = key[4:]
|
|
876
919
|
try:
|
|
877
|
-
lib_meta = self.get_cached_library_meta(lib_name)
|
|
920
|
+
lib_meta = self.get_cached_library_meta(lib_name, args=None)
|
|
878
921
|
if lib_meta is None:
|
|
879
922
|
lib_meta, _, _ = self.get_library_meta(lib_name, base_dir=base_dir)
|
|
880
923
|
if lib_meta is None or lib_meta != saved_value:
|
|
@@ -898,8 +941,15 @@ class ImportsManager:
|
|
|
898
941
|
elif key.startswith("res:"):
|
|
899
942
|
res_source = key[4:]
|
|
900
943
|
try:
|
|
901
|
-
|
|
902
|
-
|
|
944
|
+
# Document-first: an open document is the effective content
|
|
945
|
+
# of this dependency. A dirty buffer (no disk snapshot)
|
|
946
|
+
# must fail validation so dependents are rebuilt against
|
|
947
|
+
# the buffer instead of being served from cached disk
|
|
948
|
+
# state. Only unopened resources are probed on disk.
|
|
949
|
+
res_document = self.documents_manager.get(Uri.from_path(res_source))
|
|
950
|
+
if res_document is not None:
|
|
951
|
+
res_meta = RobotFileMeta.from_document(res_document)
|
|
952
|
+
else:
|
|
903
953
|
res_meta = self.get_resource_meta(res_source)
|
|
904
954
|
if res_meta is None or res_meta != saved_value:
|
|
905
955
|
self._logger.debug(
|
|
@@ -922,7 +972,7 @@ class ImportsManager:
|
|
|
922
972
|
elif key.startswith("var:"):
|
|
923
973
|
var_name = key[4:]
|
|
924
974
|
try:
|
|
925
|
-
var_meta = self.get_cached_variables_meta(var_name)
|
|
975
|
+
var_meta = self.get_cached_variables_meta(var_name, args=None)
|
|
926
976
|
if var_meta is None:
|
|
927
977
|
var_meta, _ = self.get_variables_meta(var_name, base_dir=base_dir)
|
|
928
978
|
if var_meta is None or var_meta != saved_value:
|
|
@@ -952,13 +1002,24 @@ class ImportsManager:
|
|
|
952
1002
|
def get_resource_doc_from_document(self, document: TextDocument) -> ResourceDoc:
|
|
953
1003
|
source = str(document.uri.to_path())
|
|
954
1004
|
|
|
955
|
-
|
|
956
|
-
|
|
1005
|
+
# Capture the meta before parsing, so it can only be older than the
|
|
1006
|
+
# parsed content, never newer (stale metas cause a harmless cache
|
|
1007
|
+
# miss, newer ones would poison the cache).
|
|
1008
|
+
meta = RobotFileMeta.from_document(document)
|
|
1009
|
+
|
|
1010
|
+
if meta is not None and meta.trusted:
|
|
1011
|
+
cached = self._get_model_doc_cached(source, meta)
|
|
957
1012
|
if cached is not None:
|
|
958
1013
|
return cached
|
|
959
1014
|
|
|
960
1015
|
model = self.document_cache_helper.get_resource_model(document)
|
|
961
|
-
|
|
1016
|
+
|
|
1017
|
+
# The document may have been re-read from disk while parsing; in that
|
|
1018
|
+
# case the model does not necessarily belong to the captured meta.
|
|
1019
|
+
if meta is not None and document.disk_info != meta.info:
|
|
1020
|
+
meta = None
|
|
1021
|
+
|
|
1022
|
+
return self.get_libdoc_from_model(model, source, meta)
|
|
962
1023
|
|
|
963
1024
|
def clear_cache(self) -> None:
|
|
964
1025
|
count = self.data_cache.clear_all()
|
|
@@ -1302,7 +1363,7 @@ class ImportsManager:
|
|
|
1302
1363
|
import_name,
|
|
1303
1364
|
None,
|
|
1304
1365
|
True,
|
|
1305
|
-
|
|
1366
|
+
file_infos=_collect_library_file_infos(import_name, None),
|
|
1306
1367
|
)
|
|
1307
1368
|
else:
|
|
1308
1369
|
module_spec = self._get_module_spec_cached(import_name)
|
|
@@ -1313,7 +1374,9 @@ class ImportsManager:
|
|
|
1313
1374
|
module_spec.origin,
|
|
1314
1375
|
module_spec.submodule_search_locations,
|
|
1315
1376
|
False,
|
|
1316
|
-
|
|
1377
|
+
file_infos=_collect_library_file_infos(
|
|
1378
|
+
module_spec.origin, module_spec.submodule_search_locations
|
|
1379
|
+
),
|
|
1317
1380
|
)
|
|
1318
1381
|
|
|
1319
1382
|
if result is not None:
|
|
@@ -1367,7 +1430,7 @@ class ImportsManager:
|
|
|
1367
1430
|
import_name,
|
|
1368
1431
|
None,
|
|
1369
1432
|
True,
|
|
1370
|
-
|
|
1433
|
+
file_infos=_collect_library_file_infos(import_name, None),
|
|
1371
1434
|
)
|
|
1372
1435
|
else:
|
|
1373
1436
|
module_spec = self._get_module_spec_cached(import_name)
|
|
@@ -1378,7 +1441,9 @@ class ImportsManager:
|
|
|
1378
1441
|
module_spec.origin,
|
|
1379
1442
|
module_spec.submodule_search_locations,
|
|
1380
1443
|
False,
|
|
1381
|
-
|
|
1444
|
+
file_infos=_collect_library_file_infos(
|
|
1445
|
+
module_spec.origin, module_spec.submodule_search_locations
|
|
1446
|
+
),
|
|
1382
1447
|
)
|
|
1383
1448
|
|
|
1384
1449
|
if result is not None:
|
|
@@ -1399,36 +1464,44 @@ class ImportsManager:
|
|
|
1399
1464
|
|
|
1400
1465
|
return None, name
|
|
1401
1466
|
|
|
1402
|
-
def get_cached_library_meta(
|
|
1467
|
+
def get_cached_library_meta(
|
|
1468
|
+
self, import_name: str, args: Optional[Tuple[Any, ...]] = ()
|
|
1469
|
+
) -> Optional[LibraryMetaData]:
|
|
1403
1470
|
"""Return already-computed LibraryMetaData from the internal cache, or None.
|
|
1404
1471
|
|
|
1405
|
-
Matches by import name and args to find the correct entry, since
|
|
1406
|
-
|
|
1472
|
+
Matches by import name and args to find the correct entry, since the
|
|
1473
|
+
same library with different args may produce different keywords.
|
|
1474
|
+
``args=None`` matches by name only — the metadata describes the
|
|
1475
|
+
library's module files and is the same regardless of the arguments.
|
|
1407
1476
|
"""
|
|
1408
1477
|
with self._libaries_lock:
|
|
1409
1478
|
for entry in self._libaries.values():
|
|
1410
|
-
if entry.name
|
|
1479
|
+
if entry.name != import_name:
|
|
1480
|
+
continue
|
|
1481
|
+
if args is not None:
|
|
1482
|
+
if entry.args == args:
|
|
1483
|
+
return entry.meta
|
|
1484
|
+
elif entry.meta is not None:
|
|
1411
1485
|
return entry.meta
|
|
1412
1486
|
return None
|
|
1413
1487
|
|
|
1414
|
-
def
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
|
1422
|
-
|
|
1423
|
-
def get_cached_variables_meta(self, import_name: str, args: Tuple[Any, ...] = ()) -> Optional[LibraryMetaData]:
|
|
1488
|
+
def get_cached_variables_meta(
|
|
1489
|
+
self, import_name: str, args: Optional[Tuple[Any, ...]] = ()
|
|
1490
|
+
) -> Optional[LibraryMetaData]:
|
|
1424
1491
|
"""Return already-computed LibraryMetaData for a variables import, or None.
|
|
1425
1492
|
|
|
1426
1493
|
Matches by import name and args, since variable files can return
|
|
1427
|
-
different content depending on the arguments.
|
|
1494
|
+
different content depending on the arguments. ``args=None`` matches by
|
|
1495
|
+
name only (see ``get_cached_library_meta``).
|
|
1428
1496
|
"""
|
|
1429
1497
|
with self._variables_lock:
|
|
1430
1498
|
for entry in self._variables.values():
|
|
1431
|
-
if entry.name
|
|
1499
|
+
if entry.name != import_name:
|
|
1500
|
+
continue
|
|
1501
|
+
if args is not None:
|
|
1502
|
+
if entry.args == args:
|
|
1503
|
+
return entry.meta
|
|
1504
|
+
elif entry.meta is not None:
|
|
1432
1505
|
return entry.meta
|
|
1433
1506
|
return None
|
|
1434
1507
|
|
|
@@ -1601,7 +1674,12 @@ class ImportsManager:
|
|
|
1601
1674
|
) -> None:
|
|
1602
1675
|
"""Save an import result to the disk cache, or log skip if no meta."""
|
|
1603
1676
|
try:
|
|
1604
|
-
if meta is not None:
|
|
1677
|
+
if meta is not None and not meta.trusted:
|
|
1678
|
+
self._logger.debug(
|
|
1679
|
+
lambda: f"Skip caching {kind} {name}{args!r}: file state too fresh to trust",
|
|
1680
|
+
context_name="import",
|
|
1681
|
+
)
|
|
1682
|
+
elif meta is not None:
|
|
1605
1683
|
try:
|
|
1606
1684
|
self.data_cache.save_entry(section, meta.cache_key, meta, result)
|
|
1607
1685
|
except (SystemExit, KeyboardInterrupt):
|
|
@@ -1665,14 +1743,14 @@ class ImportsManager:
|
|
|
1665
1743
|
return result, meta
|
|
1666
1744
|
|
|
1667
1745
|
@_logger.call
|
|
1668
|
-
def
|
|
1746
|
+
def get_libdoc_for_library_import_with_meta(
|
|
1669
1747
|
self,
|
|
1670
1748
|
name: str,
|
|
1671
1749
|
args: Tuple[Any, ...],
|
|
1672
1750
|
base_dir: str,
|
|
1673
1751
|
sentinel: Any = None,
|
|
1674
1752
|
variables: Optional[Dict[str, Any]] = None,
|
|
1675
|
-
) -> LibraryDoc:
|
|
1753
|
+
) -> Tuple[LibraryDoc, Optional[LibraryMetaData]]:
|
|
1676
1754
|
with self._logger.measure_time(lambda: f"loading library {name}{args!r}", context_name="import"):
|
|
1677
1755
|
source = self.find_library(name, base_dir, variables)
|
|
1678
1756
|
|
|
@@ -1704,36 +1782,45 @@ class ImportsManager:
|
|
|
1704
1782
|
fin.atexit = False # type: ignore[misc]
|
|
1705
1783
|
entry.references.add(sentinel)
|
|
1706
1784
|
|
|
1707
|
-
return entry.
|
|
1785
|
+
return entry.get_libdoc_with_meta()
|
|
1708
1786
|
|
|
1709
|
-
def
|
|
1710
|
-
|
|
1711
|
-
|
|
1787
|
+
def get_libdoc_for_library_import(
|
|
1788
|
+
self,
|
|
1789
|
+
name: str,
|
|
1790
|
+
args: Tuple[Any, ...],
|
|
1791
|
+
base_dir: str,
|
|
1792
|
+
sentinel: Any = None,
|
|
1793
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
1794
|
+
) -> LibraryDoc:
|
|
1795
|
+
return self.get_libdoc_for_library_import_with_meta(name, args, base_dir, sentinel, variables)[0]
|
|
1712
1796
|
|
|
1713
1797
|
@_logger.call
|
|
1714
1798
|
def get_libdoc_from_model(
|
|
1715
1799
|
self,
|
|
1716
1800
|
model: ast.AST,
|
|
1717
1801
|
source: str,
|
|
1802
|
+
meta: Optional[RobotFileMeta] = None,
|
|
1718
1803
|
) -> ResourceDoc:
|
|
1719
|
-
|
|
1804
|
+
"""Build (or load from cache) the ResourceDoc for a parsed model.
|
|
1805
|
+
|
|
1806
|
+
``meta`` must describe the disk state of exactly the content the model
|
|
1807
|
+
was parsed from; the disk cache is only consulted and written when such
|
|
1808
|
+
a trusted meta is available. There is deliberately no fallback to a
|
|
1809
|
+
fresh stat here — the file may have changed since the model's text was
|
|
1810
|
+
read, and labeling old content with a newer state would make the cache
|
|
1811
|
+
entry validate as fresh forever.
|
|
1812
|
+
"""
|
|
1720
1813
|
entry = self._resource_libdoc_cache.get(model)
|
|
1721
1814
|
if entry is not None and source in entry:
|
|
1722
1815
|
return entry[source]
|
|
1723
1816
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
if result is None:
|
|
1817
|
+
if meta is not None and meta.trusted:
|
|
1818
|
+
result = self._get_model_doc_cached(source, meta)
|
|
1819
|
+
if result is None:
|
|
1820
|
+
result = get_model_doc(model=model, source=source)
|
|
1821
|
+
self._save_model_doc_cache(source, result, meta)
|
|
1822
|
+
else:
|
|
1731
1823
|
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
1824
|
|
|
1738
1825
|
if entry is None:
|
|
1739
1826
|
entry = {}
|
|
@@ -1745,22 +1832,17 @@ class ImportsManager:
|
|
|
1745
1832
|
|
|
1746
1833
|
@staticmethod
|
|
1747
1834
|
def get_resource_meta(source: str) -> Optional[RobotFileMeta]:
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
return RobotFileMeta(normalized, mtime_ns)
|
|
1752
|
-
except OSError:
|
|
1835
|
+
normalized = str(normalized_path(source))
|
|
1836
|
+
info = probe_disk_info(normalized)
|
|
1837
|
+
if info is None:
|
|
1753
1838
|
return None
|
|
1839
|
+
return RobotFileMeta(normalized, info)
|
|
1754
1840
|
|
|
1755
|
-
def _get_model_doc_cached(self, source: str) ->
|
|
1756
|
-
meta = self.get_resource_meta(source)
|
|
1757
|
-
if meta is None:
|
|
1758
|
-
return None, None
|
|
1759
|
-
|
|
1841
|
+
def _get_model_doc_cached(self, source: str, meta: RobotFileMeta) -> Optional[ResourceDoc]:
|
|
1760
1842
|
try:
|
|
1761
1843
|
entry = self.data_cache.read_entry(CacheSection.RESOURCE, meta.source, RobotFileMeta, ResourceDoc)
|
|
1762
1844
|
if entry is not None and entry.meta == meta:
|
|
1763
|
-
return entry.data
|
|
1845
|
+
return entry.data
|
|
1764
1846
|
except (SystemExit, KeyboardInterrupt):
|
|
1765
1847
|
raise
|
|
1766
1848
|
except BaseException as e:
|
|
@@ -1770,7 +1852,7 @@ class ImportsManager:
|
|
|
1770
1852
|
context_name="import",
|
|
1771
1853
|
)
|
|
1772
1854
|
|
|
1773
|
-
return None
|
|
1855
|
+
return None
|
|
1774
1856
|
|
|
1775
1857
|
def _save_model_doc_cache(self, source: str, result: ResourceDoc, meta: "RobotFileMeta") -> None:
|
|
1776
1858
|
|
|
@@ -1833,7 +1915,7 @@ class ImportsManager:
|
|
|
1833
1915
|
return result, meta
|
|
1834
1916
|
|
|
1835
1917
|
@_logger.call
|
|
1836
|
-
def
|
|
1918
|
+
def get_libdoc_for_variables_import_with_meta(
|
|
1837
1919
|
self,
|
|
1838
1920
|
name: str,
|
|
1839
1921
|
args: Tuple[Any, ...],
|
|
@@ -1842,7 +1924,7 @@ class ImportsManager:
|
|
|
1842
1924
|
variables: Optional[Dict[str, Any]] = None,
|
|
1843
1925
|
resolve_variables: bool = True,
|
|
1844
1926
|
resolve_command_line_vars: bool = True,
|
|
1845
|
-
) -> VariablesDoc:
|
|
1927
|
+
) -> Tuple[VariablesDoc, Optional[LibraryMetaData]]:
|
|
1846
1928
|
with self._logger.measure_time(lambda: f"getting libdoc for variables import {name}", context_name="import"):
|
|
1847
1929
|
source = self.find_variables(name, base_dir, variables, resolve_variables, resolve_command_line_vars)
|
|
1848
1930
|
|
|
@@ -1879,7 +1961,27 @@ class ImportsManager:
|
|
|
1879
1961
|
fin = weakref.finalize(sentinel, self.__remove_variables_entry, entry_key, entry)
|
|
1880
1962
|
fin.atexit = False # type: ignore[misc]
|
|
1881
1963
|
|
|
1882
|
-
return entry.
|
|
1964
|
+
return entry.get_libdoc_with_meta()
|
|
1965
|
+
|
|
1966
|
+
def get_libdoc_for_variables_import(
|
|
1967
|
+
self,
|
|
1968
|
+
name: str,
|
|
1969
|
+
args: Tuple[Any, ...],
|
|
1970
|
+
base_dir: str,
|
|
1971
|
+
sentinel: Any = None,
|
|
1972
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
1973
|
+
resolve_variables: bool = True,
|
|
1974
|
+
resolve_command_line_vars: bool = True,
|
|
1975
|
+
) -> VariablesDoc:
|
|
1976
|
+
return self.get_libdoc_for_variables_import_with_meta(
|
|
1977
|
+
name,
|
|
1978
|
+
args,
|
|
1979
|
+
base_dir,
|
|
1980
|
+
sentinel,
|
|
1981
|
+
variables,
|
|
1982
|
+
resolve_variables,
|
|
1983
|
+
resolve_command_line_vars,
|
|
1984
|
+
)[0]
|
|
1883
1985
|
|
|
1884
1986
|
@_logger.call
|
|
1885
1987
|
def _get_entry_for_resource_import(
|
|
@@ -1929,7 +2031,7 @@ class ImportsManager:
|
|
|
1929
2031
|
|
|
1930
2032
|
return namespace, libdoc
|
|
1931
2033
|
|
|
1932
|
-
def
|
|
2034
|
+
def get_resource_doc_for_resource_import_with_meta(
|
|
1933
2035
|
self,
|
|
1934
2036
|
name: str,
|
|
1935
2037
|
base_dir: str,
|
|
@@ -1937,11 +2039,24 @@ class ImportsManager:
|
|
|
1937
2039
|
variables: Optional[Dict[str, Any]] = None,
|
|
1938
2040
|
*,
|
|
1939
2041
|
source: Optional[str] = None,
|
|
1940
|
-
) -> ResourceDoc:
|
|
2042
|
+
) -> Tuple[ResourceDoc, Optional[RobotFileMeta]]:
|
|
1941
2043
|
with self._logger.measure_time(lambda: f"getting resource doc for {name}", context_name="import"):
|
|
1942
2044
|
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
|
|
1943
2045
|
|
|
1944
|
-
return entry.
|
|
2046
|
+
return entry.get_resource_doc_with_meta()
|
|
2047
|
+
|
|
2048
|
+
def get_resource_doc_for_resource_import(
|
|
2049
|
+
self,
|
|
2050
|
+
name: str,
|
|
2051
|
+
base_dir: str,
|
|
2052
|
+
sentinel: Any = None,
|
|
2053
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
2054
|
+
*,
|
|
2055
|
+
source: Optional[str] = None,
|
|
2056
|
+
) -> ResourceDoc:
|
|
2057
|
+
return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables, source=source)[
|
|
2058
|
+
0
|
|
2059
|
+
]
|
|
1945
2060
|
|
|
1946
2061
|
def complete_library_import(
|
|
1947
2062
|
self,
|
|
@@ -26,7 +26,7 @@ from robotcode.core.lsp.types import (
|
|
|
26
26
|
from robotcode.core.text_document import TextDocument
|
|
27
27
|
from robotcode.core.uri import Uri
|
|
28
28
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
29
|
-
from robotcode.core.utils.path import FileId, file_id
|
|
29
|
+
from robotcode.core.utils.path import DiskInfo, FileId, file_id, normalized_path
|
|
30
30
|
|
|
31
31
|
from ..utils.stubs import Languages
|
|
32
32
|
from ..utils.variables import (
|
|
@@ -47,7 +47,7 @@ from .entities import (
|
|
|
47
47
|
)
|
|
48
48
|
from .errors import DIAGNOSTICS_SOURCE_NAME
|
|
49
49
|
from .import_resolver import ImportResolver
|
|
50
|
-
from .imports_manager import ImportsManager
|
|
50
|
+
from .imports_manager import ImportsManager, RobotFileMeta
|
|
51
51
|
from .keyword_finder import KeywordFinder
|
|
52
52
|
from .library_doc import (
|
|
53
53
|
KeywordDoc,
|
|
@@ -184,6 +184,7 @@ class Namespace:
|
|
|
184
184
|
finder: KeywordFinder,
|
|
185
185
|
sentinel: object,
|
|
186
186
|
semantic_model: Optional["SemanticModel"] = None,
|
|
187
|
+
dependency_metas: Optional[Dict[str, Optional[Any]]] = None,
|
|
187
188
|
) -> None:
|
|
188
189
|
self.imports_manager = imports_manager
|
|
189
190
|
self.source = source
|
|
@@ -211,6 +212,7 @@ class Namespace:
|
|
|
211
212
|
self._finder: KeywordFinder = finder
|
|
212
213
|
self._sentinel = sentinel # prevent GC — ref-counted by imports_manager
|
|
213
214
|
self._semantic_model = semantic_model
|
|
215
|
+
self._dependency_metas = dependency_metas
|
|
214
216
|
|
|
215
217
|
# Lazy-computed caches
|
|
216
218
|
self._namespaces: Optional[Dict[KeywordMatcher, List[LibraryEntry]]] = None
|
|
@@ -222,6 +224,15 @@ class Namespace:
|
|
|
222
224
|
imports_manager.resources_changed.add(self._on_resources_changed)
|
|
223
225
|
imports_manager.variables_changed.add(self._on_variables_changed)
|
|
224
226
|
|
|
227
|
+
@property
|
|
228
|
+
def dependency_metas(self) -> Optional[Dict[str, Optional[Any]]]:
|
|
229
|
+
"""Metadata of the dependencies captured while this namespace was resolved.
|
|
230
|
+
|
|
231
|
+
``None`` when the namespace was built by a path that did not record
|
|
232
|
+
them — such a namespace must never be persisted to the disk cache.
|
|
233
|
+
"""
|
|
234
|
+
return self._dependency_metas
|
|
235
|
+
|
|
225
236
|
@property
|
|
226
237
|
def document(self) -> Optional[TextDocument]:
|
|
227
238
|
return self._document() if self._document is not None else None
|
|
@@ -702,6 +713,7 @@ class Namespace:
|
|
|
702
713
|
finder=finder,
|
|
703
714
|
sentinel=sentinel,
|
|
704
715
|
semantic_model=semantic_model,
|
|
716
|
+
dependency_metas=resolved.dependency_metas,
|
|
705
717
|
)
|
|
706
718
|
|
|
707
719
|
|
|
@@ -723,6 +735,8 @@ class NamespaceBuilder:
|
|
|
723
735
|
document_type: Optional[DocumentType] = None,
|
|
724
736
|
languages: Optional[Languages] = None,
|
|
725
737
|
workspace_languages: Optional[Languages] = None,
|
|
738
|
+
*,
|
|
739
|
+
disk_info: Optional[DiskInfo] = None,
|
|
726
740
|
) -> None:
|
|
727
741
|
self._imports_manager = imports_manager
|
|
728
742
|
self._model = model
|
|
@@ -732,6 +746,7 @@ class NamespaceBuilder:
|
|
|
732
746
|
self._document_type = document_type
|
|
733
747
|
self._languages = languages
|
|
734
748
|
self._workspace_languages = workspace_languages
|
|
749
|
+
self._disk_info = disk_info
|
|
735
750
|
self._enable_semantic_model = False
|
|
736
751
|
|
|
737
752
|
def set_semantic_model_enabled(self, enabled: bool) -> None:
|
|
@@ -747,7 +762,12 @@ class NamespaceBuilder:
|
|
|
747
762
|
from .semantic_analyzer.analyzer import SemanticAnalyzer
|
|
748
763
|
|
|
749
764
|
with self._logger.measure_time(lambda: f"Build Namespace for {self._source}", context_name="import"):
|
|
750
|
-
|
|
765
|
+
source_meta = (
|
|
766
|
+
RobotFileMeta(str(normalized_path(self._source)), self._disk_info)
|
|
767
|
+
if self._disk_info is not None
|
|
768
|
+
else None
|
|
769
|
+
)
|
|
770
|
+
library_doc = self._imports_manager.get_libdoc_from_model(self._model, self._source, source_meta)
|
|
751
771
|
document_uri = (
|
|
752
772
|
self._document.document_uri if self._document is not None else str(Uri.from_path(self._source))
|
|
753
773
|
)
|
|
@@ -839,4 +859,5 @@ class NamespaceBuilder:
|
|
|
839
859
|
finder=finder,
|
|
840
860
|
sentinel=sentinel,
|
|
841
861
|
semantic_model=analyzer_result.semantic_model,
|
|
862
|
+
dependency_metas=resolved.dependency_metas,
|
|
842
863
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotcode-robot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Support classes for RobotCode for handling Robot Framework projects.
|
|
5
5
|
Project-URL: Homepage, https://robotcode.io
|
|
6
6
|
Project-URL: Donate, https://opencollective.com/robotcode
|
|
@@ -26,7 +26,7 @@ Classifier: Topic :: Utilities
|
|
|
26
26
|
Classifier: Typing :: Typed
|
|
27
27
|
Requires-Python: >=3.10
|
|
28
28
|
Requires-Dist: platformdirs>=4.3
|
|
29
|
-
Requires-Dist: robotcode-core==2.
|
|
29
|
+
Requires-Dist: robotcode-core==2.7.0
|
|
30
30
|
Requires-Dist: robotframework>=5.0.0
|
|
31
31
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
robotcode/robot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
robotcode/robot/__version__.py,sha256=
|
|
2
|
+
robotcode/robot/__version__.py,sha256=EtKWW0Hnl5oWglRNH0HZigvcDT2FEs58ek8buJdwW1E,22
|
|
3
3
|
robotcode/robot/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
|
|
4
4
|
robotcode/robot/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
robotcode/robot/config/loader.py,sha256=qrP810HNMDpqhXopWa0dOa0Wq_zQfVctsNYKY6sLKGI,8654
|
|
6
|
-
robotcode/robot/config/model.py,sha256=
|
|
6
|
+
robotcode/robot/config/model.py,sha256=WjaAKyhPzjQvjI2iDW19y0GxRY4JiSJKTtWs4fAgtFQ,104628
|
|
7
7
|
robotcode/robot/config/utils.py,sha256=A8gD5IItxuC7AY5Olcc8tmsSbqCCi_vZPVoK3BqWt_8,4070
|
|
8
8
|
robotcode/robot/diagnostics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
robotcode/robot/diagnostics/analyzer_result.py,sha256=hcakb6YE8zVDKPXsUeaxOldW3dAThOk7Z4w-IRejKoY,1012
|
|
10
10
|
robotcode/robot/diagnostics/data_cache.py,sha256=KmSKbGPPqL-uU1lHwd_tWIyTKnVTPTNJzJZGZB8_Vdw,17720
|
|
11
11
|
robotcode/robot/diagnostics/diagnostic_rules.py,sha256=nhtkLFZP0hzFRj9-i7Zz7klO1mRz3aNBs1PrvurbSrc,408
|
|
12
12
|
robotcode/robot/diagnostics/diagnostics_modifier.py,sha256=7SGr9Knh41bacGPTwn2VDg5cgXVILBeRB6FN_jyjvVk,9945
|
|
13
|
-
robotcode/robot/diagnostics/document_cache_helper.py,sha256=
|
|
13
|
+
robotcode/robot/diagnostics/document_cache_helper.py,sha256=3Hb43euh_QSTlG934DmAnk68c-QkavqKLexj1VjMI9o,31604
|
|
14
14
|
robotcode/robot/diagnostics/entities.py,sha256=P1R1kJPvXNGjjah7c6TVfltCF_u1NAKGpzFnPthw3F8,11215
|
|
15
15
|
robotcode/robot/diagnostics/errors.py,sha256=n14np0VmXLMT5Y6R9Dw6tfhreHV_xvPXSPz-a5-OTMM,2363
|
|
16
|
-
robotcode/robot/diagnostics/import_resolver.py,sha256=
|
|
17
|
-
robotcode/robot/diagnostics/imports_manager.py,sha256=
|
|
16
|
+
robotcode/robot/diagnostics/import_resolver.py,sha256=hoxtMPSMXD_hX8JxJ97nO7LUnaL8QGwqQ9-aw7i1AF8,26675
|
|
17
|
+
robotcode/robot/diagnostics/imports_manager.py,sha256=y7RJ82ftqX6MAQS7xmSMddMf6QHEcG1AnI6Kf2AzZCU,80282
|
|
18
18
|
robotcode/robot/diagnostics/keyword_finder.py,sha256=nwTmjvhyu5H7HHzyk8gt-pny2K2xdH2qCNyKXwwXRnk,18998
|
|
19
19
|
robotcode/robot/diagnostics/library_doc.py,sha256=wGJ1eQtfu3GwC1tvkOFpM60uCF1Z-GwbKbUV5epH-WM,121608
|
|
20
20
|
robotcode/robot/diagnostics/model_helper.py,sha256=_ySXwerkK5xvjWa82T0DFy8jpxRzcaavTJiAHnk2lXQ,29740
|
|
21
|
-
robotcode/robot/diagnostics/namespace.py,sha256=
|
|
21
|
+
robotcode/robot/diagnostics/namespace.py,sha256=OV7aYNTb7pMsJLc8QDl-oI49Zdhd3EXsw8UPi_LdpqU,33724
|
|
22
22
|
robotcode/robot/diagnostics/namespace_analyzer.py,sha256=QVGm9AjukAGDul7WjNuxcbLDpgdEtfze2sa5_ni1Eac,100143
|
|
23
23
|
robotcode/robot/diagnostics/project_index.py,sha256=nOBvfAxLS9KoKjye9_4YWmHPuJoZqspCQ1PLHieZKuc,7209
|
|
24
24
|
robotcode/robot/diagnostics/scope_tree.py,sha256=2Ev4BhzTbVSsXku0p8naftbTWQXbGqCiEMBUrtiVicI,8337
|
|
@@ -41,6 +41,6 @@ robotcode/robot/utils/robot_path.py,sha256=ObLP8PqamTZAz5oi0eDXE_dnyZwAPXp4TJYm6
|
|
|
41
41
|
robotcode/robot/utils/stubs.py,sha256=umugZYAyneFNgqRJBRMJPzm0u0B_TH8Sx_y-ykXnxpw,351
|
|
42
42
|
robotcode/robot/utils/variables.py,sha256=q4lcigv9_NxiSgIrxvzmfmjKoKj_o_qMjWwnSKRH71k,9309
|
|
43
43
|
robotcode/robot/utils/visitor.py,sha256=nP3O0qh3YYuxR6S8wYJRBFfNwIVgsgohURBlrnFkRYQ,2299
|
|
44
|
-
robotcode_robot-2.
|
|
45
|
-
robotcode_robot-2.
|
|
46
|
-
robotcode_robot-2.
|
|
44
|
+
robotcode_robot-2.7.0.dist-info/METADATA,sha256=UJBll3gAP1xot1Ffu-iiB0fOVGrNB-7hCw4IuZ2x-2c,2893
|
|
45
|
+
robotcode_robot-2.7.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
46
|
+
robotcode_robot-2.7.0.dist-info/RECORD,,
|