robotcode-robot 2.5.0__tar.gz → 2.5.1__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 (38) hide show
  1. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/PKG-INFO +1 -1
  2. robotcode_robot-2.5.1/src/robotcode/robot/__version__.py +1 -0
  3. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/document_cache_helper.py +8 -0
  4. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/imports_manager.py +58 -5
  5. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/keyword_finder.py +14 -9
  6. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/library_doc.py +2 -8
  7. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/model_helper.py +38 -58
  8. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/namespace_analyzer.py +6 -9
  9. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/variables.py +8 -1
  10. robotcode_robot-2.5.0/src/robotcode/robot/__version__.py +0 -1
  11. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/.gitignore +0 -0
  12. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/README.md +0 -0
  13. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/pyproject.toml +0 -0
  14. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/__init__.py +0 -0
  15. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/__init__.py +0 -0
  16. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/loader.py +0 -0
  17. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/model.py +0 -0
  18. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/config/utils.py +0 -0
  19. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/__init__.py +0 -0
  20. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/data_cache.py +0 -0
  21. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/diagnostics_modifier.py +0 -0
  22. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/entities.py +0 -0
  23. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/errors.py +0 -0
  24. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/import_resolver.py +0 -0
  25. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/namespace.py +0 -0
  26. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/project_index.py +0 -0
  27. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/scope_tree.py +0 -0
  28. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/variable_scope.py +0 -0
  29. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/diagnostics/workspace_config.py +0 -0
  30. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/py.typed +0 -0
  31. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/__init__.py +0 -0
  32. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/ast.py +0 -0
  33. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/markdownformatter.py +0 -0
  34. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/match.py +0 -0
  35. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/robot_patching.py +0 -0
  36. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/robot_path.py +0 -0
  37. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/src/robotcode/robot/utils/stubs.py +0 -0
  38. {robotcode_robot-2.5.0 → robotcode_robot-2.5.1}/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.5.0
3
+ Version: 2.5.1
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
@@ -0,0 +1 @@
1
+ __version__ = "2.5.1"
@@ -496,9 +496,17 @@ class DocumentsCacheHelper:
496
496
  return None
497
497
 
498
498
  if entry is None or entry.meta is None:
499
+ self._logger.debug(
500
+ lambda: f"Cache miss for {source}: no cached entry found",
501
+ context_name="cache",
502
+ )
499
503
  return None
500
504
 
501
505
  if not imports_manager.validate_namespace_meta(entry.meta):
506
+ self._logger.debug(
507
+ lambda: f"Cache miss for {source}: cached entry is stale, re-analyzing",
508
+ context_name="cache",
509
+ )
502
510
  return None
503
511
 
504
512
  # Meta is valid — load the full NamespaceData (lazy deserialization)
@@ -835,20 +835,37 @@ class ImportsManager:
835
835
  Level 1 (fast): source_mtime_ns, config_fingerprint
836
836
  Level 2 (dependency check): each dependency fingerprint
837
837
  """
838
+ source = meta.source
839
+
838
840
  # Level 1: fast checks
839
841
  try:
840
- current_mtime = os.stat(meta.source, follow_symlinks=False).st_mtime_ns
842
+ current_mtime = os.stat(source, follow_symlinks=False).st_mtime_ns
841
843
  except OSError:
844
+ self._logger.debug(
845
+ lambda: f"Cache miss for {source}: source file no longer exists",
846
+ context_name="cache",
847
+ )
842
848
  return False
843
849
 
844
850
  if meta.source_mtime_ns != current_mtime:
851
+ self._logger.debug(
852
+ lambda: (
853
+ f"Cache miss for {source}: source mtime changed"
854
+ f" (cached={meta.source_mtime_ns}, current={current_mtime})"
855
+ ),
856
+ context_name="cache",
857
+ )
845
858
  return False
846
859
 
847
860
  if meta.config_fingerprint != self.config_fingerprint:
861
+ self._logger.debug(
862
+ lambda: f"Cache miss for {source}: config fingerprint changed",
863
+ context_name="cache",
864
+ )
848
865
  return False
849
866
 
850
867
  # Level 2: dependency checks — direct comparison, no hashing
851
- base_dir = os.path.dirname(meta.source)
868
+ base_dir = os.path.dirname(source)
852
869
  for key, saved_value in meta.dependency_fingerprints.items():
853
870
  if key.startswith("lib:"):
854
871
  lib_name = key[4:]
@@ -857,10 +874,22 @@ class ImportsManager:
857
874
  if lib_meta is None:
858
875
  lib_meta, _, _ = self.get_library_meta(lib_name, base_dir=base_dir)
859
876
  if lib_meta is None or lib_meta != saved_value:
877
+ self._logger.debug(
878
+ lambda: (
879
+ f"Cache miss for {source}: library {lib_name!r} changed"
880
+ f" (found={'yes' if lib_meta is not None else 'no'})"
881
+ ),
882
+ context_name="cache",
883
+ )
860
884
  return False
861
885
  except (SystemExit, KeyboardInterrupt):
862
886
  raise
863
- except BaseException:
887
+ except BaseException as e:
888
+ ex = e
889
+ self._logger.debug(
890
+ lambda: f"Cache miss for {source}: error checking library {lib_name!r}: {ex}",
891
+ context_name="cache",
892
+ )
864
893
  return False
865
894
  elif key.startswith("res:"):
866
895
  res_source = key[4:]
@@ -869,10 +898,22 @@ class ImportsManager:
869
898
  if res_meta is None:
870
899
  res_meta = self.get_resource_meta(res_source)
871
900
  if res_meta is None or res_meta != saved_value:
901
+ self._logger.debug(
902
+ lambda: (
903
+ f"Cache miss for {source}: resource {res_source!r} changed"
904
+ f" (found={'yes' if res_meta is not None else 'no'})"
905
+ ),
906
+ context_name="cache",
907
+ )
872
908
  return False
873
909
  except (SystemExit, KeyboardInterrupt):
874
910
  raise
875
- except BaseException:
911
+ except BaseException as e:
912
+ ex = e
913
+ self._logger.debug(
914
+ lambda: f"Cache miss for {source}: error checking resource {res_source!r}: {ex}",
915
+ context_name="cache",
916
+ )
876
917
  return False
877
918
  elif key.startswith("var:"):
878
919
  var_name = key[4:]
@@ -881,10 +922,22 @@ class ImportsManager:
881
922
  if var_meta is None:
882
923
  var_meta, _ = self.get_variables_meta(var_name, base_dir=base_dir)
883
924
  if var_meta is None or var_meta != saved_value:
925
+ self._logger.debug(
926
+ lambda: (
927
+ f"Cache miss for {source}: variables {var_name!r} changed"
928
+ f" (found={'yes' if var_meta is not None else 'no'})"
929
+ ),
930
+ context_name="cache",
931
+ )
884
932
  return False
885
933
  except (SystemExit, KeyboardInterrupt):
886
934
  raise
887
- except BaseException:
935
+ except BaseException as e:
936
+ ex = e
937
+ self._logger.debug(
938
+ lambda: f"Cache miss for {source}: error checking variables {var_name!r}: {ex}",
939
+ context_name="cache",
940
+ )
888
941
  return False
889
942
 
890
943
  return True
@@ -1,7 +1,7 @@
1
1
  import functools
2
2
  import re
3
3
  from itertools import chain
4
- from typing import Dict, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
4
+ from typing import Dict, FrozenSet, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
5
5
 
6
6
  from robot.libraries import STDLIBS
7
7
  from robotcode.core.lsp.types import (
@@ -38,6 +38,16 @@ class CancelSearchError(Exception):
38
38
  DEFAULT_BDD_PREFIXES = {"Given ", "When ", "Then ", "And ", "But "}
39
39
 
40
40
 
41
+ @functools.lru_cache(maxsize=None)
42
+ def build_bdd_prefix_regexp(prefixes: FrozenSet[str]) -> "re.Pattern[str]":
43
+ sorted_prefixes = sorted(prefixes, key=len, reverse=True)
44
+ pattern = "|".join(p.strip().replace(" ", r"\s") for p in sorted_prefixes).lower()
45
+ return re.compile(rf"({pattern})\s", re.IGNORECASE)
46
+
47
+
48
+ DEFAULT_BDD_PREFIX_REGEXP = build_bdd_prefix_regexp(frozenset(DEFAULT_BDD_PREFIXES))
49
+
50
+
41
51
  class KeywordFinder:
42
52
  def __init__(
43
53
  self,
@@ -501,14 +511,9 @@ class KeywordFinder:
501
511
 
502
512
  @functools.cached_property
503
513
  def bdd_prefix_regexp(self) -> "re.Pattern[str]":
504
- prefixes = (
505
- "|".join(
506
- self._languages.bdd_prefixes if self._languages is not None else ["given", "when", "then", "and", "but"]
507
- )
508
- .replace(" ", r"\s")
509
- .lower()
510
- )
511
- return re.compile(rf"({prefixes})\s", re.IGNORECASE)
514
+ if self._languages is not None:
515
+ return build_bdd_prefix_regexp(frozenset(self._languages.bdd_prefixes))
516
+ return DEFAULT_BDD_PREFIX_REGEXP
512
517
 
513
518
  def _get_bdd_style_keyword(self, name: str) -> Optional[KeywordDoc]:
514
519
  match = self.bdd_prefix_regexp.match(name)
@@ -79,7 +79,7 @@ from ..utils.ast import (
79
79
  from ..utils.markdownformatter import MarkDownFormatter
80
80
  from ..utils.match import normalize, normalize_namespace
81
81
  from ..utils.robot_patching import patch_variable_not_found
82
- from ..utils.variables import contains_variable, search_variable
82
+ from ..utils.variables import contains_variable, replace_curdir_in_variable_values, search_variable
83
83
  from .entities import (
84
84
  ArgumentDefinition,
85
85
  Import,
@@ -3199,13 +3199,7 @@ class _MyResourceBuilder(ResourceBuilder):
3199
3199
 
3200
3200
  values = node.get_values(Token.ARGUMENT)
3201
3201
  has_value = bool(values)
3202
- value = tuple(
3203
- s.replace(
3204
- "${CURDIR}",
3205
- str(Path(self.source).parent).replace("\\\\", "\\\\\\\\"),
3206
- )
3207
- for s in values
3208
- )
3202
+ value = replace_curdir_in_variable_values(values, self.source)
3209
3203
 
3210
3204
  stripped_name_token = strip_variable_token(name_token, matcher=matcher, parse_type=True)
3211
3205
 
@@ -39,7 +39,7 @@ from .entities import (
39
39
  VariableDefinition,
40
40
  VariableNotFoundDefinition,
41
41
  )
42
- from .keyword_finder import DEFAULT_BDD_PREFIXES
42
+ from .keyword_finder import DEFAULT_BDD_PREFIX_REGEXP, build_bdd_prefix_regexp
43
43
  from .library_doc import (
44
44
  ArgumentInfo,
45
45
  KeywordArgumentKind,
@@ -562,38 +562,35 @@ class ModelHelper:
562
562
  BDD_TOKEN_REGEX = re.compile(r"^(Given|When|Then|And|But)\s", flags=re.IGNORECASE)
563
563
  BDD_TOKEN = re.compile(r"^(Given|When|Then|And|But)$", flags=re.IGNORECASE)
564
564
 
565
+ @classmethod
566
+ def _get_bdd_prefix_regexp(cls, namespace: "Namespace") -> "re.Pattern[str]":
567
+ if namespace.languages is not None:
568
+ return build_bdd_prefix_regexp(frozenset(namespace.languages.bdd_prefixes))
569
+ return DEFAULT_BDD_PREFIX_REGEXP
570
+
565
571
  @classmethod
566
572
  def split_bdd_prefix(cls, namespace: "Namespace", token: Token) -> Tuple[Optional[Token], Optional[Token]]:
567
- bdd_token = None
568
-
569
- parts = token.value.split()
570
- if len(parts) < 2:
571
- return None, token
572
-
573
- for index in range(1, len(parts)):
574
- prefix = " ".join(parts[:index]).title()
575
- if prefix in (
576
- namespace.languages.bdd_prefixes if namespace.languages is not None else DEFAULT_BDD_PREFIXES
577
- ):
578
- bdd_len = len(prefix)
579
- bdd_token = Token(
580
- token.type,
581
- token.value[:bdd_len],
582
- token.lineno,
583
- token.col_offset,
584
- token.error,
585
- )
573
+ bdd_match = cls._get_bdd_prefix_regexp(namespace).match(token.value)
574
+ if bdd_match:
575
+ bdd_len = len(bdd_match.group(1))
576
+ bdd_token = Token(
577
+ token.type,
578
+ token.value[:bdd_len],
579
+ token.lineno,
580
+ token.col_offset,
581
+ token.error,
582
+ )
586
583
 
587
- token = Token(
588
- token.type,
589
- token.value[bdd_len + 1 :],
590
- token.lineno,
591
- token.col_offset + bdd_len + 1,
592
- token.error,
593
- )
594
- break
584
+ token = Token(
585
+ token.type,
586
+ token.value[bdd_len + 1 :],
587
+ token.lineno,
588
+ token.col_offset + bdd_len + 1,
589
+ token.error,
590
+ )
591
+ return bdd_token, token
595
592
 
596
- return bdd_token, token
593
+ return None, token
597
594
 
598
595
  @classmethod
599
596
  def strip_bdd_prefix(cls, namespace: "Namespace", token: Token) -> Token:
@@ -611,24 +608,16 @@ class ModelHelper:
611
608
  )
612
609
  return token
613
610
 
614
- parts = token.value.split()
615
- if len(parts) < 2:
616
- return token
617
-
618
- for index in range(1, len(parts)):
619
- prefix = " ".join(parts[:index]).title()
620
- if prefix in (
621
- namespace.languages.bdd_prefixes if namespace.languages is not None else DEFAULT_BDD_PREFIXES
622
- ):
623
- bdd_len = len(prefix)
624
- token = Token(
625
- token.type,
626
- token.value[bdd_len + 1 :],
627
- token.lineno,
628
- token.col_offset + bdd_len + 1,
629
- token.error,
630
- )
631
- break
611
+ bdd_match = cls._get_bdd_prefix_regexp(namespace).match(token.value)
612
+ if bdd_match:
613
+ bdd_len = len(bdd_match.group(1))
614
+ token = Token(
615
+ token.type,
616
+ token.value[bdd_len + 1 :],
617
+ token.lineno,
618
+ token.col_offset + bdd_len + 1,
619
+ token.error,
620
+ )
632
621
 
633
622
  return token
634
623
 
@@ -638,17 +627,8 @@ class ModelHelper:
638
627
  bdd_match = cls.BDD_TOKEN.match(token.value)
639
628
  return bool(bdd_match)
640
629
 
641
- parts = token.value.split()
642
-
643
- for index in range(len(parts)):
644
- prefix = " ".join(parts[: index + 1]).title()
645
-
646
- if prefix.title() in (
647
- namespace.languages.bdd_prefixes if namespace.languages is not None else DEFAULT_BDD_PREFIXES
648
- ):
649
- return True
650
-
651
- return False
630
+ bdd_match = cls._get_bdd_prefix_regexp(namespace).match(token.value + " ")
631
+ return bdd_match is not None and len(bdd_match.group(1)) == len(token.value)
652
632
 
653
633
  @classmethod
654
634
  def get_keyword_definition_at_token(cls, library_doc: LibraryDoc, token: Token) -> Optional[KeywordDoc]:
@@ -7,7 +7,6 @@ from collections import defaultdict
7
7
  from concurrent.futures import CancelledError
8
8
  from dataclasses import dataclass
9
9
  from io import StringIO
10
- from pathlib import Path
11
10
  from tokenize import TokenError, generate_tokens
12
11
  from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
13
12
 
@@ -63,6 +62,7 @@ from ..utils.variables import (
63
62
  InvalidVariableError,
64
63
  VariableMatcher,
65
64
  contains_variable,
65
+ replace_curdir_in_variable_values,
66
66
  search_variable,
67
67
  split_from_equals,
68
68
  )
@@ -283,13 +283,7 @@ class NamespaceAnalyzer(Visitor):
283
283
 
284
284
  values = node.get_values(Token.ARGUMENT)
285
285
  has_value = bool(values)
286
- value = tuple(
287
- s.replace(
288
- "${CURDIR}",
289
- str(Path(self._source).parent).replace("\\", "\\\\"),
290
- )
291
- for s in values
292
- )
286
+ value = replace_curdir_in_variable_values(values, self._source)
293
287
 
294
288
  var_def = VariableDefinition(
295
289
  name=name,
@@ -697,6 +691,7 @@ class NamespaceAnalyzer(Visitor):
697
691
  allow_variables: bool = False,
698
692
  ignore_errors_if_contains_variables: bool = False,
699
693
  unescape_keyword: bool = True,
694
+ is_template: bool = False,
700
695
  ) -> Optional[KeywordDoc]:
701
696
  result: Optional[KeywordDoc] = None
702
697
 
@@ -782,7 +777,7 @@ class NamespaceAnalyzer(Visitor):
782
777
  else:
783
778
  self._keyword_references[result].add(Location(self._document_uri, kw_range))
784
779
 
785
- if result.is_embedded:
780
+ if result.is_embedded and not is_template:
786
781
  self._analyze_token_variables(keyword_token)
787
782
 
788
783
  if result.errors:
@@ -1105,6 +1100,7 @@ class NamespaceAnalyzer(Visitor):
1105
1100
  [],
1106
1101
  analyze_run_keywords=False,
1107
1102
  allow_variables=True,
1103
+ is_template=True,
1108
1104
  )
1109
1105
 
1110
1106
  self._test_template = node
@@ -1122,6 +1118,7 @@ class NamespaceAnalyzer(Visitor):
1122
1118
  [],
1123
1119
  analyze_run_keywords=False,
1124
1120
  allow_variables=True,
1121
+ is_template=True,
1125
1122
  )
1126
1123
  self._template = node
1127
1124
 
@@ -1,5 +1,6 @@
1
1
  import functools
2
- from typing import Any, Optional, Tuple, cast
2
+ from pathlib import Path
3
+ from typing import Any, Optional, Sequence, Tuple, cast
3
4
 
4
5
  from robot.utils.escaping import split_from_equals as robot_split_from_equals
5
6
  from robot.variables.search import contains_variable as robot_contains_variable
@@ -225,3 +226,9 @@ def search_variable(
225
226
  @functools.lru_cache(maxsize=1024)
226
227
  def split_from_equals(string: str) -> Tuple[str, Optional[str]]:
227
228
  return cast(Tuple[str, Optional[str]], robot_split_from_equals(string))
229
+
230
+
231
+ def replace_curdir_in_variable_values(values: Sequence[str], source: str) -> Tuple[str, ...]:
232
+ """Replace ${CURDIR} in variable values with the escaped parent directory of source."""
233
+ curdir = str(Path(source).parent).replace("\\", "\\\\")
234
+ return tuple(s.replace("${CURDIR}", curdir) for s in values)
@@ -1 +0,0 @@
1
- __version__ = "2.5.0"