robotcode-robot 2.8.0.dev670__py3-none-any.whl → 2.9.0.dev51__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.
@@ -1 +1 @@
1
- __version__ = "2.8.0-dev.670"
1
+ __version__ = "2.9.0-dev.51"
@@ -3222,6 +3222,42 @@ class RobotProfile(RobotExtendBaseProfile):
3222
3222
  )
3223
3223
 
3224
3224
 
3225
+ @dataclass(frozen=True)
3226
+ class UnknownProfile:
3227
+ """A profile name or pattern that selects no profile, and where it comes from."""
3228
+
3229
+ name: str
3230
+ # the profile whose `inherits` names it; None for a name of the selection or of `default-profiles`
3231
+ inherited_by: Optional[str] = None
3232
+ # whether it comes from `default-profiles`
3233
+ default: bool = False
3234
+
3235
+ @property
3236
+ def selected(self) -> bool:
3237
+ """Whether the name comes from the selection: `-p`, `ROBOTCODE_PROFILES` or an editor."""
3238
+ return self.inherited_by is None and not self.default
3239
+
3240
+ @property
3241
+ def message(self) -> str:
3242
+ return f"No profile matches '{self.name}'."
3243
+
3244
+
3245
+ class ProfileNotFoundError(ValueError):
3246
+ """A profile name or pattern that selects no profile."""
3247
+
3248
+ def __init__(self, unknown: UnknownProfile) -> None:
3249
+ super().__init__(unknown.message)
3250
+ self.unknown = unknown
3251
+
3252
+
3253
+ class ProfileInheritanceError(ValueError):
3254
+ """A profile that inherits itself, directly or through other profiles."""
3255
+
3256
+ def __init__(self, path: Tuple[str, ...]) -> None:
3257
+ super().__init__(f"The profile '{path[0]}' inherits itself: {' -> '.join(path)}.")
3258
+ self.path = path
3259
+
3260
+
3225
3261
  @dataclass
3226
3262
  class RobotConfig(RobotExtendBaseProfile):
3227
3263
  """Robot Framework configuration."""
@@ -3288,11 +3324,13 @@ class RobotConfig(RobotExtendBaseProfile):
3288
3324
  *names: str,
3289
3325
  verbose_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3290
3326
  error_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3327
+ unknown_profile_callback: Optional[Callable[[UnknownProfile], None]] = None,
3291
3328
  ) -> Dict[str, Tuple[RobotProfile, Optional[Set[Tuple[str, RobotProfile]]]]]:
3292
3329
  result: Dict[str, Tuple[RobotProfile, Optional[Set[Tuple[str, RobotProfile]]]]] = {}
3293
3330
 
3294
3331
  profiles = self.profiles or {}
3295
3332
 
3333
+ from_default_profiles = not names
3296
3334
  if not names:
3297
3335
  if verbose_callback:
3298
3336
  verbose_callback("No profiles given, try to check if there are default profiles specified.")
@@ -3306,40 +3344,65 @@ class RobotConfig(RobotExtendBaseProfile):
3306
3344
 
3307
3345
  names = (*(default_profile or ()),)
3308
3346
 
3309
- def select(name: str, parent_profiles: Optional[Set[Tuple[str, RobotProfile]]] = None) -> None:
3347
+ # each name that selects no profile is reported once for each place it comes from
3348
+ reported: Set[UnknownProfile] = set()
3349
+
3350
+ def report(unknown: UnknownProfile) -> None:
3351
+ if unknown in reported:
3352
+ return
3353
+ reported.add(unknown)
3354
+
3355
+ # a name that selects no profile is an error of the configuration; only callers that must go on, such as
3356
+ # the language server, take it and go on without it
3357
+ if unknown_profile_callback is not None:
3358
+ unknown_profile_callback(unknown)
3359
+ # a name of `default-profiles` is only reported: the `default-profiles` of the user configuration can name
3360
+ # a profile of that file, which the `[profiles]` of a project replace
3361
+ elif unknown.default and error_callback is not None:
3362
+ error_callback(unknown.message)
3363
+ else:
3364
+ raise ProfileNotFoundError(unknown)
3365
+
3366
+ # `path` holds the profiles whose `inherits` lead to the name; the last one names it
3367
+ def select(name: str, path: Tuple[str, ...] = ()) -> None:
3310
3368
  if not name:
3311
3369
  return
3312
3370
 
3313
3371
  nonlocal result
3314
3372
 
3373
+ parent = path[-1] if path else None
3374
+
3315
3375
  if verbose_callback:
3316
- if parent_profiles is not None:
3317
- verbose_callback(
3318
- f"Selecting profiles matching '{name}'"
3319
- f" for parent profile '{next(f for f in parent_profiles)[0]}'."
3320
- )
3376
+ if parent is not None:
3377
+ verbose_callback(f"Selecting profiles matching '{name}' for parent profile '{parent}'.")
3321
3378
  else:
3322
3379
  verbose_callback(f"Selecting profiles matching '{name}'.")
3323
3380
 
3324
- profile_names = [p for p in profiles.keys() if fnmatch.fnmatchcase(p, name)]
3381
+ if name in profiles:
3382
+ # an exact name selects its profile, also when it contains `*`, `?` or `[`
3383
+ profile_names = [name]
3384
+ else:
3385
+ # a pattern in `inherits` does not match the profile that has it
3386
+ profile_names = [p for p in profiles.keys() if p != parent and fnmatch.fnmatchcase(p, name)]
3325
3387
 
3326
3388
  if not profile_names:
3327
- message = f"Can't find any configuration profiles matching the pattern '{name}'."
3328
- if error_callback is None:
3329
- raise ValueError(message)
3330
-
3331
- error_callback(message)
3389
+ report(UnknownProfile(name, inherited_by=parent, default=from_default_profiles and parent is None))
3332
3390
  return
3333
3391
 
3392
+ parent_profiles = {(parent, profiles[parent])} if parent is not None else None
3393
+
3334
3394
  for v in profile_names:
3395
+ if v in path:
3396
+ raise ProfileInheritanceError((*path[path.index(v) :], v))
3397
+
3335
3398
  p = profiles[v]
3336
3399
 
3337
3400
  if p.inherits:
3338
3401
  if isinstance(p.inherits, list):
3339
3402
  for i in p.inherits:
3340
- select(str(i), {(v, p)})
3403
+ select(str(i), (*path, v))
3341
3404
  else:
3342
- select(str(p.inherits), {(v, p)})
3405
+ select(str(p.inherits), (*path, v))
3343
3406
 
3344
3407
  if v in result:
3345
3408
  if parent_profiles is None:
@@ -3362,14 +3425,21 @@ class RobotConfig(RobotExtendBaseProfile):
3362
3425
  *names: str,
3363
3426
  verbose_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3364
3427
  error_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3428
+ unknown_profile_callback: Optional[Callable[[UnknownProfile], None]] = None,
3365
3429
  ) -> RobotBaseProfile:
3366
- return self.combine_profiles_ex(*names, verbose_callback=verbose_callback, error_callback=error_callback)[0]
3430
+ return self.combine_profiles_ex(
3431
+ *names,
3432
+ verbose_callback=verbose_callback,
3433
+ error_callback=error_callback,
3434
+ unknown_profile_callback=unknown_profile_callback,
3435
+ )[0]
3367
3436
 
3368
3437
  def combine_profiles_ex(
3369
3438
  self,
3370
3439
  *names: str,
3371
3440
  verbose_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3372
3441
  error_callback: Optional[Callable[[Union[str, Callable[[], Any]]], None]] = None,
3442
+ unknown_profile_callback: Optional[Callable[[UnknownProfile], None]] = None,
3373
3443
  ) -> Tuple[RobotBaseProfile, Dict[str, RobotProfile], List[str]]:
3374
3444
  enabled_profiles = []
3375
3445
  type_hints = get_type_hints(RobotBaseProfile)
@@ -3390,7 +3460,10 @@ class RobotConfig(RobotExtendBaseProfile):
3390
3460
  )
3391
3461
 
3392
3462
  selected_profiles = self._select_profiles(
3393
- *names, verbose_callback=verbose_callback, error_callback=error_callback
3463
+ *names,
3464
+ verbose_callback=verbose_callback,
3465
+ error_callback=error_callback,
3466
+ unknown_profile_callback=unknown_profile_callback,
3394
3467
  )
3395
3468
  if verbose_callback:
3396
3469
  if selected_profiles:
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from typing import TYPE_CHECKING, Dict, List, Optional, Set
3
3
 
4
4
  from robotcode.core.lsp.types import Diagnostic, Location, Range
@@ -25,3 +25,7 @@ class AnalyzerResult:
25
25
  testcase_metadata_references: Dict[str, Set[Location]]
26
26
  scope_tree: ScopeTree
27
27
  semantic_model: Optional["SemanticModel"] = None
28
+ # the environment variables the analysis read, which the namespace cache compares when it restores the analysis:
29
+ # whether each one checked without a default value is set, and the hash of each one whose value the analysis used
30
+ environment_set: Dict[str, bool] = field(default_factory=dict)
31
+ environment_values: Dict[str, Optional[str]] = field(default_factory=dict)
@@ -640,6 +640,16 @@ class DocumentsCacheHelper:
640
640
  )
641
641
  return None
642
642
 
643
+ # from_data resolved the imports again with the current Python path and environment variables. A namespace
644
+ # with a failed import is never stored and the dependency keys name the files found, so a failed import or
645
+ # other keys mean that an import finds another file or none now.
646
+ if result.failed_imports or set(result.dependency_metas or {}) != set(meta.dependency_fingerprints):
647
+ self._logger.debug(
648
+ lambda: f"Cache miss for {source}: the imports find other files now, re-analyzing",
649
+ context_name="cache",
650
+ )
651
+ return None
652
+
643
653
  self._logger.debug(
644
654
  lambda: f"Loaded namespace from disk cache for {source}",
645
655
  context_name="import",
@@ -52,10 +52,6 @@ class Import(SourceEntity):
52
52
  name: Optional[str]
53
53
  name_token: Optional[Token]
54
54
 
55
- @property
56
- def hint_key(self) -> str:
57
- return f"{self.name}\0{self.source or ''}\0{self.line_no}"
58
-
59
55
  @property
60
56
  def range(self) -> Range:
61
57
  return Range(
@@ -9,7 +9,6 @@ The resolver takes imports + a VariableScope and returns ResolvedImports
9
9
  containing all resolved entries and collected diagnostics.
10
10
  """
11
11
 
12
- import os
13
12
  from dataclasses import dataclass, field
14
13
  from pathlib import Path
15
14
  from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
@@ -129,7 +128,6 @@ class ImportResolver:
129
128
  "_scope",
130
129
  "_sentinel",
131
130
  "_source",
132
- "_source_hints",
133
131
  "_source_id",
134
132
  "_variables",
135
133
  "_variables_dirty",
@@ -159,14 +157,11 @@ class ImportResolver:
159
157
  self._diagnostics: List[Diagnostic] = []
160
158
  self._variables: Dict[str, Any] = {}
161
159
  self._variables_dirty = True
162
- self._source_hints: Dict[str, str] = {}
163
160
  self._dependency_metas: Dict[str, Optional[Any]] = {}
164
161
  self._failed_imports: List[FailedImport] = []
165
162
 
166
- def resolve(self, imports: Iterable[Import], *, source_hints: Optional[Dict[str, str]] = None) -> ResolvedImports:
163
+ def resolve(self, imports: Iterable[Import]) -> ResolvedImports:
167
164
  """Resolve all imports and return the result."""
168
- if source_hints:
169
- self._source_hints = source_hints
170
165
  self._refresh_variables()
171
166
  self._import_default_libraries()
172
167
  self._handle_imports(imports, self._base_dir, top_level=True)
@@ -542,19 +537,11 @@ class ImportResolver:
542
537
  ) -> None:
543
538
  assert imp.name is not None
544
539
 
545
- # Look up cached source hint to skip find_resource() filesystem search
546
- source_hint: Optional[str] = None
547
- if self._source_hints:
548
- hint = self._source_hints.get(imp.hint_key)
549
- if hint is not None and os.path.isfile(hint):
550
- source_hint = hint
551
-
552
540
  resource_doc, meta = self._imports_manager.get_resource_doc_for_resource_import_with_meta(
553
541
  imp.name,
554
542
  base_dir,
555
543
  sentinel=self._sentinel,
556
544
  variables=self._variables,
557
- source=source_hint,
558
545
  )
559
546
  if resource_doc.source is not None:
560
547
  # setdefault: on re-imports of the same resource the meta captured
@@ -59,7 +59,7 @@ from ..__version__ import __version__
59
59
  from ..utils import RF_VERSION
60
60
  from ..utils.robot_path import find_file_ex
61
61
  from ..utils.stubs import Languages
62
- from ..utils.variables import contains_variable
62
+ from ..utils.variables import contains_variable, environment_value_hash
63
63
  from .data_cache import CACHE_DIR_NAME, CacheSection, build_cache_dir
64
64
  from .data_cache import SqliteDataCache as DefaultDataCache
65
65
  from .entities import (
@@ -722,6 +722,10 @@ class NamespaceMetaData:
722
722
  config_fingerprint: Any
723
723
  dependency_fingerprints: Dict[str, Any] = field(default_factory=dict)
724
724
  semantic_model_enabled: bool = False
725
+ # the environment variables the analysis read: whether each one checked without a default value was set, and the
726
+ # hash of each one whose value it used (see environment_value_hash)
727
+ environment_set: Dict[str, bool] = field(default_factory=dict)
728
+ environment_values: Dict[str, Optional[str]] = field(default_factory=dict)
725
729
 
726
730
 
727
731
  class ImportsManager:
@@ -808,7 +812,6 @@ class ImportsManager:
808
812
  self._config_fingerprint: Any = (
809
813
  tuple(sorted(self.cmd_variables.items())),
810
814
  tuple(self.cmd_variable_files),
811
- tuple(sorted((k, v) for k, v in self._environment.items() if k not in os.environ)),
812
815
  tuple(self.global_library_search_order),
813
816
  self._languages_fingerprint,
814
817
  )
@@ -895,8 +898,9 @@ class ImportsManager:
895
898
  """Cached configuration snapshot.
896
899
 
897
900
  Computed once at init from cmd_variables, cmd_variable_files,
898
- environment, and global_library_search_order. Changes are
899
- detected via direct ``==`` comparison.
901
+ global_library_search_order and the language settings. Changes are
902
+ detected via direct ``==`` comparison. Environment variables are
903
+ not part of it: each namespace keeps the ones its analysis read.
900
904
  """
901
905
  return self._config_fingerprint
902
906
 
@@ -949,13 +953,15 @@ class ImportsManager:
949
953
  config_fingerprint=self.config_fingerprint,
950
954
  dependency_fingerprints=dict(dependency_metas),
951
955
  semantic_model_enabled=semantic_model_enabled,
956
+ environment_set=dict(namespace.environment_set),
957
+ environment_values=dict(namespace.environment_values),
952
958
  )
953
959
 
954
960
  def validate_namespace_meta(self, meta: NamespaceMetaData, source_disk_info: Optional[DiskInfo]) -> bool:
955
961
  """Check whether a cached NamespaceMetaData is fresh for the given content.
956
962
 
957
963
  Performs a 2-level validation:
958
- Level 1 (fast): source_disk_info, config_fingerprint
964
+ Level 1 (fast): source_disk_info, config_fingerprint, the environment variables the analysis read
959
965
  Level 2 (dependency check): each dependency fingerprint
960
966
 
961
967
  Validation is against the *effective* content: the source is compared
@@ -990,6 +996,25 @@ class ImportsManager:
990
996
  )
991
997
  return False
992
998
 
999
+ # the analysis reads os.environ itself, so it does not matter where a variable was set
1000
+ for env_name, was_set in meta.environment_set.items():
1001
+ if (os.environ.get(env_name) is not None) != was_set:
1002
+ name = env_name
1003
+ self._logger.debug(
1004
+ lambda: f"Cache miss for {source}: environment variable {name!r} was set or unset",
1005
+ context_name="cache",
1006
+ )
1007
+ return False
1008
+
1009
+ for env_name, value_hash in meta.environment_values.items():
1010
+ if environment_value_hash(os.environ.get(env_name)) != value_hash:
1011
+ name = env_name
1012
+ self._logger.debug(
1013
+ lambda: f"Cache miss for {source}: environment variable {name!r} changed",
1014
+ context_name="cache",
1015
+ )
1016
+ return False
1017
+
993
1018
  # Level 2: dependency checks — direct comparison, no hashing
994
1019
  for key, saved_value in meta.dependency_fingerprints.items():
995
1020
  if key.startswith("lib:"):
@@ -2211,10 +2236,8 @@ class ImportsManager:
2211
2236
  base_dir: str,
2212
2237
  sentinel: Any = None,
2213
2238
  variables: Optional[Dict[str, Any]] = None,
2214
- *,
2215
- source: Optional[str] = None,
2216
2239
  ) -> _ResourcesEntry:
2217
- source = source or self.find_resource(name, base_dir, variables=variables)
2240
+ source = self.find_resource(name, base_dir, variables=variables)
2218
2241
  normalized_source = os.path.normpath(os.path.abspath(source))
2219
2242
 
2220
2243
  entry_key = _ResourcesEntryKey(normalized_source)
@@ -2238,12 +2261,10 @@ class ImportsManager:
2238
2261
  base_dir: str,
2239
2262
  sentinel: Any = None,
2240
2263
  variables: Optional[Dict[str, Any]] = None,
2241
- *,
2242
- source: Optional[str] = None,
2243
2264
  ) -> Tuple["Namespace", LibraryDoc]:
2244
2265
  with self._logger.measure_time(lambda: f"getting namespace and libdoc for {name}", context_name="import"):
2245
2266
  with self._logger.measure_time(lambda: f"getting resource entry {name}", context_name="import"):
2246
- entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
2267
+ entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
2247
2268
 
2248
2269
  with self._logger.measure_time(lambda: f"getting namespace {name}", context_name="import"):
2249
2270
  namespace = entry.get_namespace()
@@ -2258,11 +2279,9 @@ class ImportsManager:
2258
2279
  base_dir: str,
2259
2280
  sentinel: Any = None,
2260
2281
  variables: Optional[Dict[str, Any]] = None,
2261
- *,
2262
- source: Optional[str] = None,
2263
2282
  ) -> Tuple[ResourceDoc, Optional[RobotFileMeta]]:
2264
2283
  with self._logger.measure_time(lambda: f"getting resource doc for {name}", context_name="import"):
2265
- entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
2284
+ entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
2266
2285
 
2267
2286
  return entry.get_resource_doc_with_meta()
2268
2287
 
@@ -2272,12 +2291,8 @@ class ImportsManager:
2272
2291
  base_dir: str,
2273
2292
  sentinel: Any = None,
2274
2293
  variables: Optional[Dict[str, Any]] = None,
2275
- *,
2276
- source: Optional[str] = None,
2277
2294
  ) -> ResourceDoc:
2278
- return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables, source=source)[
2279
- 0
2280
- ]
2295
+ return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables)[0]
2281
2296
 
2282
2297
  def complete_library_import(
2283
2298
  self,
@@ -2728,7 +2728,7 @@ class _Logger(AbstractLogger):
2728
2728
 
2729
2729
  def _import_test_library(name: str) -> Union[Any, Tuple[Any, str]]:
2730
2730
  with OutputCapturer(library_import=True):
2731
- importer = Importer("test library", LOGGER)
2731
+ importer = Importer("library", LOGGER)
2732
2732
  return importer.import_class_or_module(name, return_source=True)
2733
2733
 
2734
2734
 
@@ -133,12 +133,6 @@ class NamespaceData:
133
133
  # --- ScopeTree (local scopes only, file_scope is reconstructed) ---
134
134
  local_scopes: List[LocalScope] = field(default_factory=list)
135
135
 
136
- # --- Resolved resource import sources ---
137
- # Maps (import_name\0source_file) → resolved absolute path for resources.
138
- # Used as source hints in from_data() to skip find_resource() filesystem
139
- # lookups during import re-resolution.
140
- resolved_resource_sources: Dict[str, str] = field(default_factory=dict)
141
-
142
136
  # --- Authoritative variable definitions (stable_id → VariableDefinition) ---
143
137
  # Stores the exact VariableDefinition objects that were referenced during
144
138
  # analysis. Used as fallback in from_data() when the re-parsed resource doc
@@ -189,6 +183,8 @@ class Namespace:
189
183
  semantic_model: Optional["SemanticModel"] = None,
190
184
  dependency_metas: Optional[Dict[str, Optional[Any]]] = None,
191
185
  failed_imports: Optional[List[FailedImport]] = None,
186
+ environment_set: Optional[Dict[str, bool]] = None,
187
+ environment_values: Optional[Dict[str, Optional[str]]] = None,
192
188
  ) -> None:
193
189
  self.imports_manager = imports_manager
194
190
  self.source = source
@@ -219,6 +215,8 @@ class Namespace:
219
215
  self._semantic_model = semantic_model
220
216
  self._dependency_metas = dependency_metas
221
217
  self._failed_imports = failed_imports or []
218
+ self._environment_set = environment_set or {}
219
+ self._environment_values = environment_values or {}
222
220
 
223
221
  # Lazy-computed caches
224
222
  self._namespaces: Optional[Dict[KeywordMatcher, List[LibraryEntry]]] = None
@@ -244,6 +242,16 @@ class Namespace:
244
242
  """The imports whose resolution raised or whose library or variables documentation has errors."""
245
243
  return self._failed_imports
246
244
 
245
+ @property
246
+ def environment_set(self) -> Dict[str, bool]:
247
+ """Whether each environment variable the analysis checked without a default value was set."""
248
+ return self._environment_set
249
+
250
+ @property
251
+ def environment_values(self) -> Dict[str, Optional[str]]:
252
+ """The hash of each environment variable whose value the analysis used, see `environment_value_hash`."""
253
+ return self._environment_values
254
+
247
255
  @property
248
256
  def document(self) -> Optional[TextDocument]:
249
257
  return self._document() if self._document is not None else None
@@ -538,12 +546,6 @@ class Namespace:
538
546
  else:
539
547
  var_assigns_merged[sid] = set(ranges)
540
548
 
541
- # Build resolved resource source hints for from_data() optimization
542
- resolved_res_sources: Dict[str, str] = {}
543
- for imp, entry in self._import_entries.items():
544
- if isinstance(entry, ResourceEntry) and entry.library_doc.source:
545
- resolved_res_sources[imp.hint_key] = entry.library_doc.source
546
-
547
549
  return NamespaceData(
548
550
  source=self.source,
549
551
  source_id=str(self.source_id) if self.source_id else None,
@@ -563,7 +565,6 @@ class Namespace:
563
565
  metadata_references={k: set(v) for k, v in self._metadata_references.items()},
564
566
  testcase_metadata_references={k: set(v) for k, v in self._testcase_metadata_references.items()},
565
567
  local_scopes=list(self._scope_tree.local_scopes),
566
- resolved_resource_sources=resolved_res_sources,
567
568
  variable_definitions=all_var_defs,
568
569
  )
569
570
 
@@ -595,7 +596,7 @@ class Namespace:
595
596
  )
596
597
 
597
598
  resolver = ImportResolver(imports_manager, data.source, scope, sentinel=sentinel)
598
- resolved = resolver.resolve(data.imports, source_hints=data.resolved_resource_sources)
599
+ resolved = resolver.resolve(data.imports)
599
600
 
600
601
  # Add imported variables to scope (needed for variable lookup)
601
602
  for resource_entry in resolved.resources.values():
@@ -893,4 +894,6 @@ class NamespaceBuilder:
893
894
  semantic_model=analyzer_result.semantic_model,
894
895
  dependency_metas=resolved.dependency_metas,
895
896
  failed_imports=resolved.failed_imports,
897
+ environment_set=analyzer_result.environment_set,
898
+ environment_values=analyzer_result.environment_values,
896
899
  )
@@ -61,6 +61,7 @@ from ..utils.variables import (
61
61
  InvalidVariableError,
62
62
  VariableMatcher,
63
63
  contains_variable,
64
+ environment_value_hash,
64
65
  is_number_literal,
65
66
  replace_curdir_in_variable_values,
66
67
  search_variable,
@@ -145,6 +146,8 @@ class NamespaceAnalyzer(Visitor):
145
146
  self._testcase_tag_references: Dict[str, Set[Location]] = defaultdict(set)
146
147
  self._metadata_references: Dict[str, Set[Location]] = defaultdict(set)
147
148
  self._testcase_metadata_references: Dict[str, Set[Location]] = defaultdict(set)
149
+ self._environment_set: Dict[str, bool] = {}
150
+ self._environment_values: Dict[str, Optional[str]] = {}
148
151
 
149
152
  # Phase 1+2 results (set by resolve())
150
153
  self._library_doc: ResourceDoc = cast(ResourceDoc, None) # set by resolve()
@@ -245,6 +248,8 @@ class NamespaceAnalyzer(Visitor):
245
248
  self._metadata_references,
246
249
  self._testcase_metadata_references,
247
250
  self._scope_builder.build(self._variable_scope),
251
+ environment_set=self._environment_set,
252
+ environment_values=self._environment_values,
248
253
  )
249
254
 
250
255
  def _visit_VariableSection(self, node: VariableSection) -> None: # noqa: N802
@@ -673,7 +678,10 @@ class NamespaceAnalyzer(Visitor):
673
678
  and cast(EnvironmentVariableDefinition, var).default_value is None
674
679
  ):
675
680
  env_name = var.name[2:-1]
676
- if os.environ.get(env_name, None) is None:
681
+ # the namespace cache compares every read of the environment when it restores the analysis
682
+ env_value = os.environ.get(env_name, None)
683
+ self._environment_set[env_name] = env_value is not None
684
+ if env_value is None:
677
685
  self._append_diagnostics(
678
686
  range=range_from_token(var_token),
679
687
  message=(
@@ -2039,7 +2047,9 @@ class NamespaceAnalyzer(Visitor):
2039
2047
  return name_default
2040
2048
  return False
2041
2049
  env_name, sep, default = env_inner.partition("=")
2050
+ # the namespace cache compares every read of the environment when it restores the analysis
2042
2051
  env_val = os.environ.get(env_name)
2052
+ self._environment_values[env_name] = environment_value_hash(env_val)
2043
2053
  if env_val is not None:
2044
2054
  return env_val
2045
2055
  if sep:
@@ -98,6 +98,7 @@ from ...utils.variables import (
98
98
  InvalidVariableError,
99
99
  VariableMatcher,
100
100
  contains_variable,
101
+ environment_value_hash,
101
102
  is_number_literal,
102
103
  replace_curdir_in_variable_values,
103
104
  search_variable,
@@ -280,6 +281,8 @@ class SemanticAnalyzer(Visitor):
280
281
  self._testcase_tag_references: Dict[str, Set[Location]] = defaultdict(set)
281
282
  self._metadata_references: Dict[str, Set[Location]] = defaultdict(set)
282
283
  self._testcase_metadata_references: Dict[str, Set[Location]] = defaultdict(set)
284
+ self._environment_set: Dict[str, bool] = {}
285
+ self._environment_values: Dict[str, Optional[str]] = {}
283
286
 
284
287
  # Phase 1+2 results (set by resolve())
285
288
  self._library_doc: ResourceDoc = cast(ResourceDoc, None)
@@ -422,6 +425,8 @@ class SemanticAnalyzer(Visitor):
422
425
  self._metadata_references,
423
426
  self._testcase_metadata_references,
424
427
  self._scope_builder.build(self._variable_scope),
428
+ environment_set=self._environment_set,
429
+ environment_values=self._environment_values,
425
430
  semantic_model=self._semantic_model,
426
431
  )
427
432
 
@@ -2004,7 +2009,10 @@ class SemanticAnalyzer(Visitor):
2004
2009
  and cast(EnvironmentVariableDefinition, var).default_value is None
2005
2010
  ):
2006
2011
  env_name = var.name[2:-1]
2007
- if os.environ.get(env_name, None) is None:
2012
+ # the namespace cache compares every read of the environment when it restores the analysis
2013
+ env_value = os.environ.get(env_name, None)
2014
+ self._environment_set[env_name] = env_value is not None
2015
+ if env_value is None:
2008
2016
  self._append_diagnostics(
2009
2017
  range=range_from_token(var_token),
2010
2018
  message=(
@@ -4456,7 +4464,9 @@ class SemanticAnalyzer(Visitor):
4456
4464
  return name_default
4457
4465
  return False
4458
4466
  env_name, sep, default = env_inner.partition("=")
4467
+ # the namespace cache compares every read of the environment when it restores the analysis
4459
4468
  env_val = os.environ.get(env_name)
4469
+ self._environment_values[env_name] = environment_value_hash(env_val)
4460
4470
  if env_val is not None:
4461
4471
  return env_val
4462
4472
  if sep:
@@ -144,14 +144,18 @@ class LinkFormatter:
144
144
  return pre + self._get_link(url)
145
145
 
146
146
  def _get_image(self, src: str, title: Optional[str] = None) -> str:
147
- return f"![{title or src}]({self._unquote_hash(src)})"
147
+ return f"![{title or src}]({self._target(src)})"
148
148
 
149
149
  def _get_link(self, href: str, content: Optional[str] = None) -> str:
150
- return f"[{content or href}]({self._unquote_hash(href)})"
150
+ return f"[{content or href}]({self._target(href)})"
151
151
 
152
- def _unquote_hash(self, target: str) -> str:
152
+ def _target(self, target: str) -> str:
153
153
  # `#` is escaped in the whole line before links are formatted, a link target needs it as it is
154
- return target.replace("\\#", "#")
154
+ target = target.replace("\\#", "#")
155
+ # Markdown takes a target with spaces only in angle brackets; `<` is already escaped in the whole line
156
+ if " " in target:
157
+ return "<" + target.replace(">", "\\>") + ">"
158
+ return target
155
159
 
156
160
  def _quot(self, attr: str) -> str:
157
161
  return attr if '"' not in attr else attr.replace('"', "&quot;")
@@ -1,4 +1,5 @@
1
1
  import functools
2
+ import hashlib
2
3
  from pathlib import Path
3
4
  from typing import Any, Optional, Sequence, Tuple, cast
4
5
 
@@ -275,3 +276,11 @@ def replace_curdir_in_variable_values(values: Sequence[str], source: str) -> Tup
275
276
  """Replace ${CURDIR} in variable values with the escaped parent directory of source."""
276
277
  curdir = str(Path(source).parent).replace("\\", "\\\\")
277
278
  return tuple(s.replace("${CURDIR}", curdir) for s in values)
279
+
280
+
281
+ def environment_value_hash(value: Optional[str]) -> Optional[str]:
282
+ """The SHA-256 of the value of an environment variable, or None when it is not set.
283
+
284
+ What the analysis cache keeps instead of the value, so that no value of the environment ends up in it.
285
+ """
286
+ return hashlib.sha256(value.encode("utf-8", "surrogateescape")).hexdigest() if value is not None else None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: robotcode-robot
3
- Version: 2.8.0-dev.670
3
+ Version: 2.9.0-dev.51
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
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: >=3.10
29
29
  Requires-Dist: platformdirs>=4.3
30
- Requires-Dist: robotcode-core==2.8.0-dev.670
30
+ Requires-Dist: robotcode-core==2.9.0-dev.51
31
31
  Requires-Dist: robotframework>=5.0.0
32
32
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
33
33
  Description-Content-Type: text/markdown
@@ -1,31 +1,31 @@
1
1
  robotcode/robot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- robotcode/robot/__version__.py,sha256=H0IFmU7VH6n_W0CbM7UtSwMIjNMI6OcCSdI7uBsndDU,30
2
+ robotcode/robot/__version__.py,sha256=68lI1Ud2IVxiSKwJTXlHsfwV7s6ASsCia4CPMEdJ8Ls,29
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=HLbkZem8JiBuy9Q5naghZddAuMPOn8Tk6UBgN2T3Uyc,9073
6
- robotcode/robot/config/model.py,sha256=i8EH_n_20U7eCNY81nWa7oTFAxKj9iSH5Sxm1vr8bOQ,112151
6
+ robotcode/robot/config/model.py,sha256=2dRKRDQV50_k1PmX-7uhqrPTX7Z6oJq4PcAG4pBDRi8,115165
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
- robotcode/robot/diagnostics/analyzer_result.py,sha256=4LC031CMJWVtKJm_UMYKVLGosFPR81rSAgqU1xs4Adc,1071
9
+ robotcode/robot/diagnostics/analyzer_result.py,sha256=5pHW5Yf3C3rmaDvmM7tP0y_qwqLW3YHhkeVV-qDhxtg,1459
10
10
  robotcode/robot/diagnostics/data_cache.py,sha256=Yi52oQTUBFgcPK0UF3lDAj4mRFqoD_VOtrAUyRWXnrQ,18200
11
11
  robotcode/robot/diagnostics/diagnostic_rules.py,sha256=7b4vbGTEuxCef8AHTshV4XAJe75aR4-lJ5QEDyuZtcE,3558
12
12
  robotcode/robot/diagnostics/diagnostics_modifier.py,sha256=MftxJlUEDsn9ZiOsBDApXC2N-4OjXHSZj1Y9PXXlSAg,11742
13
- robotcode/robot/diagnostics/document_cache_helper.py,sha256=D5X6b3-smbZrlYz7E4b0pGDmMA9hCfQ_Da7cRslhx34,34841
14
- robotcode/robot/diagnostics/entities.py,sha256=1djksLkdhp2JeZJAc8sJR2fIBMECX6k9RW-q34pdDAQ,12062
13
+ robotcode/robot/diagnostics/document_cache_helper.py,sha256=llwyocHMKrSpuooXMuBalwnG6hlY6YgMTIPFq2tHFr8,35462
14
+ robotcode/robot/diagnostics/entities.py,sha256=vE5szceIL-ANqJPnAbqIebg3kLddE9OPhxwBL9iKP-M,11949
15
15
  robotcode/robot/diagnostics/errors.py,sha256=lqHDWrMNy3ZhuEIVxeEhaU0ns8PB6pVTQBfaMqRQ8L4,2733
16
- robotcode/robot/diagnostics/import_resolver.py,sha256=lqbJYm3i62nK-EB1tgQ-RZoUtJ6rkB9JX6AzSN-p9t4,33126
17
- robotcode/robot/diagnostics/imports_manager.py,sha256=SRSidp3KyPXvT7NollGu-nhYmVKon8hw2pXoQog9SlE,89243
16
+ robotcode/robot/diagnostics/import_resolver.py,sha256=DZjUOD3uNxFAwsVUqrCTRhColjz9DRPetAvIb_Ccufs,32588
17
+ robotcode/robot/diagnostics/imports_manager.py,sha256=opemApuCLgPNsnqVuYc9MefqHt42g_7Ozqhrmu_osH0,90401
18
18
  robotcode/robot/diagnostics/keyword_finder.py,sha256=nwTmjvhyu5H7HHzyk8gt-pny2K2xdH2qCNyKXwwXRnk,18998
19
- robotcode/robot/diagnostics/library_doc.py,sha256=MYztjgnOmXAPjXn7td651oWats2qDlPrLgCkmwpdSmU,153576
19
+ robotcode/robot/diagnostics/library_doc.py,sha256=xJCFm1uMM2OFZqn6sJNfdjGZJ2WsQjGlRmlqDHv64uA,153571
20
20
  robotcode/robot/diagnostics/model_helper.py,sha256=_ySXwerkK5xvjWa82T0DFy8jpxRzcaavTJiAHnk2lXQ,29740
21
- robotcode/robot/diagnostics/namespace.py,sha256=EK_HH1uZoZFd5zZwo_Hc8TyvTts8AASmvJcsuJjGPgA,35443
22
- robotcode/robot/diagnostics/namespace_analyzer.py,sha256=HrYdY6DM2DvR3Jh9wB-RggbxLDgez5CLk_NRSOZQRcQ,103678
21
+ robotcode/robot/diagnostics/namespace.py,sha256=Rcmjk3V0vcY-DUkgjkRyuOdYXurKaTEqXJLc6efYrwY,35489
22
+ robotcode/robot/diagnostics/namespace_analyzer.py,sha256=dGAEbbznPJlFBKN4dLkMDU7w9xc0mYdmRARvRS3Zlwg,104331
23
23
  robotcode/robot/diagnostics/project_index.py,sha256=Oqs7kV279PixVS6rGeigPKXGJYGqJdTDmDBjU7ODDRA,11006
24
24
  robotcode/robot/diagnostics/scope_tree.py,sha256=UREHoFG7NWh6UTWxET1M5Jxz_XBjXaQJg1GUIYPctq0,8348
25
25
  robotcode/robot/diagnostics/variable_scope.py,sha256=bge49DM-1FfQYIeqK2DtBG7JAMm25mazZ_zjh0DoMmg,3096
26
26
  robotcode/robot/diagnostics/workspace_config.py,sha256=GlsR1VnUBHoLBvIZtUXkm1dBDFPT9qoiYZzwZlurmXM,3277
27
27
  robotcode/robot/diagnostics/semantic_analyzer/__init__.py,sha256=ObbJv-S8LDgkWKZtTZjISBodXxyoLdEiOjVYp5aYbT4,1689
28
- robotcode/robot/diagnostics/semantic_analyzer/analyzer.py,sha256=PrWfgnA2AveDaVpXQUQ-pyx5aDeIcQwA7q2b7K39_Uo,205496
28
+ robotcode/robot/diagnostics/semantic_analyzer/analyzer.py,sha256=CkROVL_B3QsSULXkHkRDTddLCR0sswVVxEKCQUYk_qs,206149
29
29
  robotcode/robot/diagnostics/semantic_analyzer/enums.py,sha256=FIf3bHHqYY2etzpNrZTpZxGFsdtBE15lTssI1g8T2ro,8668
30
30
  robotcode/robot/diagnostics/semantic_analyzer/json_dump.py,sha256=2Yq3CLAKF5IEqKeFDoIYn-Ize_1Ucgz82ORkx3wAguY,13836
31
31
  robotcode/robot/diagnostics/semantic_analyzer/model.py,sha256=TTsrU_aDSC4JwbdypxSaGR79iCN071Y5VUMO8vGm5IE,20476
@@ -36,13 +36,13 @@ robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py,sha256=vBor3
36
36
  robotcode/robot/utils/__init__.py,sha256=-A0YeHv7e7WK1SId7Er7Vu6frrDYWopUrcyFlyxLmPY,396
37
37
  robotcode/robot/utils/ast.py,sha256=S9xyiJZee4Y5VcroNrGIIWp8f9sutb_1o4blAlCqzdg,11218
38
38
  robotcode/robot/utils/markdown_docs.py,sha256=jY3jT919TicVRMhcYo03dR0NzhGq0kEnDolBSPc-lNI,29548
39
- robotcode/robot/utils/markdownformatter.py,sha256=hThVK-L7_0-xcAtNNXvFeAaSzbNix0hP73t_BO_JCsc,14122
39
+ robotcode/robot/utils/markdownformatter.py,sha256=hHilBaj1DzBV1tlNHyNfMvM3h92Six-15guhQy6edgU,14323
40
40
  robotcode/robot/utils/match.py,sha256=KgA6o6TbnQpOT1eAzEjhl_-FZ2XO07xiV0L7IYRzyzE,1125
41
41
  robotcode/robot/utils/robot_patching.py,sha256=jXHhnPvZx8hCQq4B_2uwX9j51Be_toWpUoTYYe8HjPo,1463
42
42
  robotcode/robot/utils/robot_path.py,sha256=ObLP8PqamTZAz5oi0eDXE_dnyZwAPXp4TJYm6ugeENs,2185
43
43
  robotcode/robot/utils/stubs.py,sha256=umugZYAyneFNgqRJBRMJPzm0u0B_TH8Sx_y-ykXnxpw,351
44
- robotcode/robot/utils/variables.py,sha256=M_JVe4DxV7ucL_iRW7VOQnSlY6I72NVhRfki4f3zMQY,9430
44
+ robotcode/robot/utils/variables.py,sha256=9WnP3QqcHdq-zioJ_gJXJobw_2Gcto_rGPCUGmOH9YQ,9829
45
45
  robotcode/robot/utils/visitor.py,sha256=nP3O0qh3YYuxR6S8wYJRBFfNwIVgsgohURBlrnFkRYQ,2299
46
- robotcode_robot-2.8.0.dev670.dist-info/METADATA,sha256=stGbMKy2QKUTxnaz5wGC2_o8ls7f_buhefYjKA-dvVQ,2960
47
- robotcode_robot-2.8.0.dev670.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
- robotcode_robot-2.8.0.dev670.dist-info/RECORD,,
46
+ robotcode_robot-2.9.0.dev51.dist-info/METADATA,sha256=RziLuNl8hmcFuiRfMnuUYgOZm7OFM-E06dYl1ZNS6yE,2958
47
+ robotcode_robot-2.9.0.dev51.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
48
+ robotcode_robot-2.9.0.dev51.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any