robotcode-robot 2.6.1__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/PKG-INFO +2 -2
  2. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/pyproject.toml +1 -1
  3. robotcode_robot-2.7.0/src/robotcode/robot/__version__.py +1 -0
  4. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/config/model.py +29 -0
  5. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/document_cache_helper.py +84 -11
  6. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/import_resolver.py +20 -4
  7. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/imports_manager.py +269 -154
  8. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/namespace.py +24 -3
  9. robotcode_robot-2.6.1/src/robotcode/robot/__version__.py +0 -1
  10. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/.gitignore +0 -0
  11. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/README.md +0 -0
  12. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/__init__.py +0 -0
  13. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/config/__init__.py +0 -0
  14. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/config/loader.py +0 -0
  15. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/config/utils.py +0 -0
  16. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/__init__.py +0 -0
  17. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/analyzer_result.py +0 -0
  18. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/data_cache.py +0 -0
  19. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/diagnostic_rules.py +0 -0
  20. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/diagnostics_modifier.py +0 -0
  21. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/entities.py +0 -0
  22. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/errors.py +0 -0
  23. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/keyword_finder.py +0 -0
  24. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/library_doc.py +0 -0
  25. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/model_helper.py +0 -0
  26. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/namespace_analyzer.py +0 -0
  27. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/project_index.py +0 -0
  28. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/scope_tree.py +0 -0
  29. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/__init__.py +0 -0
  30. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +0 -0
  31. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/enums.py +0 -0
  32. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/model.py +0 -0
  33. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/nodes.py +0 -0
  34. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/run_keyword.py +0 -0
  35. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/serialization.py +0 -0
  36. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py +0 -0
  37. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/variable_scope.py +0 -0
  38. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/diagnostics/workspace_config.py +0 -0
  39. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/py.typed +0 -0
  40. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/__init__.py +0 -0
  41. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/ast.py +0 -0
  42. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/markdownformatter.py +0 -0
  43. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/match.py +0 -0
  44. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/robot_patching.py +0 -0
  45. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/robot_path.py +0 -0
  46. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/stubs.py +0 -0
  47. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/variables.py +0 -0
  48. {robotcode_robot-2.6.1 → robotcode_robot-2.7.0}/src/robotcode/robot/utils/visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotcode-robot
3
- Version: 2.6.1
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.6.1
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
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "robotframework>=5.0.0",
32
32
  "tomli>=1.1.0; python_version < '3.11'",
33
33
  "platformdirs>=4.3",
34
- "robotcode-core==2.6.1",
34
+ "robotcode-core==2.7.0",
35
35
  ]
36
36
  dynamic = ["version"]
37
37
 
@@ -0,0 +1 @@
1
+ __version__ = "2.7.0"
@@ -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
- if cache_namespaces:
476
- self._save_namespace_to_cache(source, result, imports_manager, semantic_model_enabled)
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
- Returns None on cache miss or validation failure.
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(CacheSection.NAMESPACE, source, NamespaceMetaData, NamespaceData)
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.get_libdoc_for_library_import(
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.get_libdoc_for_library_import(
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.get_resource_doc_for_resource_import(
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.get_libdoc_for_variables_import(
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,