robotcode-robot 0.93.1__py3-none-any.whl → 0.94.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.
@@ -0,0 +1,458 @@
1
+ from itertools import chain
2
+ from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
3
+
4
+ from robot.libraries import STDLIBS
5
+ from robotcode.core.lsp.types import (
6
+ DiagnosticSeverity,
7
+ )
8
+
9
+ from ..utils import get_robot_version
10
+ from ..utils.match import eq_namespace
11
+ from .entities import (
12
+ LibraryEntry,
13
+ ResourceEntry,
14
+ )
15
+ from .errors import Error
16
+ from .library_doc import (
17
+ KeywordDoc,
18
+ KeywordError,
19
+ LibraryDoc,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from .namespace import Namespace
24
+
25
+
26
+ class DiagnosticsEntry(NamedTuple):
27
+ message: str
28
+ severity: DiagnosticSeverity
29
+ code: Optional[str] = None
30
+
31
+
32
+ class CancelSearchError(Exception):
33
+ pass
34
+
35
+
36
+ DEFAULT_BDD_PREFIXES = {"Given ", "When ", "Then ", "And ", "But "}
37
+
38
+
39
+ class KeywordFinder:
40
+ def __init__(self, namespace: "Namespace", library_doc: LibraryDoc) -> None:
41
+ self.namespace = namespace
42
+ self.self_library_doc = library_doc
43
+
44
+ self.diagnostics: List[DiagnosticsEntry] = []
45
+ self.multiple_keywords_result: Optional[List[KeywordDoc]] = None
46
+ self._cache: Dict[
47
+ Tuple[Optional[str], bool],
48
+ Tuple[
49
+ Optional[KeywordDoc],
50
+ List[DiagnosticsEntry],
51
+ Optional[List[KeywordDoc]],
52
+ ],
53
+ ] = {}
54
+ self.handle_bdd_style = True
55
+ self._all_keywords: Optional[List[LibraryEntry]] = None
56
+ self._resource_keywords: Optional[List[ResourceEntry]] = None
57
+ self._library_keywords: Optional[List[LibraryEntry]] = None
58
+
59
+ def reset_diagnostics(self) -> None:
60
+ self.diagnostics = []
61
+ self.multiple_keywords_result = None
62
+
63
+ def find_keyword(
64
+ self,
65
+ name: Optional[str],
66
+ *,
67
+ raise_keyword_error: bool = False,
68
+ handle_bdd_style: bool = True,
69
+ ) -> Optional[KeywordDoc]:
70
+ try:
71
+ self.reset_diagnostics()
72
+
73
+ self.handle_bdd_style = handle_bdd_style
74
+
75
+ cached = self._cache.get((name, self.handle_bdd_style), None)
76
+
77
+ if cached is not None:
78
+ self.diagnostics = cached[1]
79
+ self.multiple_keywords_result = cached[2]
80
+ return cached[0]
81
+
82
+ try:
83
+ result = self._find_keyword(name)
84
+ if result is None:
85
+ self.diagnostics.append(
86
+ DiagnosticsEntry(
87
+ f"No keyword with name '{name}' found.",
88
+ DiagnosticSeverity.ERROR,
89
+ Error.KEYWORD_NOT_FOUND,
90
+ )
91
+ )
92
+ except KeywordError as e:
93
+ if e.multiple_keywords:
94
+ self._add_to_multiple_keywords_result(e.multiple_keywords)
95
+
96
+ if raise_keyword_error:
97
+ raise
98
+
99
+ result = None
100
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
101
+
102
+ self._cache[(name, self.handle_bdd_style)] = (
103
+ result,
104
+ self.diagnostics,
105
+ self.multiple_keywords_result,
106
+ )
107
+
108
+ return result
109
+ except CancelSearchError:
110
+ return None
111
+
112
+ def _find_keyword(self, name: Optional[str]) -> Optional[KeywordDoc]:
113
+ if not name:
114
+ self.diagnostics.append(
115
+ DiagnosticsEntry(
116
+ "Keyword name cannot be empty.",
117
+ DiagnosticSeverity.ERROR,
118
+ Error.KEYWORD_ERROR,
119
+ )
120
+ )
121
+ raise CancelSearchError
122
+ if not isinstance(name, str):
123
+ self.diagnostics.append( # type: ignore
124
+ DiagnosticsEntry(
125
+ "Keyword name must be a string.",
126
+ DiagnosticSeverity.ERROR,
127
+ Error.KEYWORD_ERROR,
128
+ )
129
+ )
130
+ raise CancelSearchError
131
+
132
+ result = self._get_keyword_from_self(name)
133
+ if not result and "." in name:
134
+ result = self._get_explicit_keyword(name)
135
+
136
+ if not result:
137
+ result = self._get_implicit_keyword(name)
138
+
139
+ if not result and self.handle_bdd_style:
140
+ return self._get_bdd_style_keyword(name)
141
+
142
+ return result
143
+
144
+ def _get_keyword_from_self(self, name: str) -> Optional[KeywordDoc]:
145
+ if get_robot_version() >= (6, 0):
146
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = [
147
+ (None, v) for v in self.self_library_doc.keywords.iter_all(name)
148
+ ]
149
+ if len(found) > 1:
150
+ found = self._select_best_matches(found)
151
+ if len(found) > 1:
152
+ self.diagnostics.append(
153
+ DiagnosticsEntry(
154
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
155
+ DiagnosticSeverity.ERROR,
156
+ Error.MULTIPLE_KEYWORDS,
157
+ )
158
+ )
159
+ raise CancelSearchError
160
+
161
+ if len(found) == 1:
162
+ # TODO warning if keyword found is defined in resource and suite
163
+ return found[0][1]
164
+
165
+ return None
166
+
167
+ try:
168
+ return self.self_library_doc.keywords.get(name, None)
169
+ except KeywordError as e:
170
+ self.diagnostics.append(DiagnosticsEntry(str(e), DiagnosticSeverity.ERROR, Error.KEYWORD_ERROR))
171
+ raise CancelSearchError from e
172
+
173
+ def _yield_owner_and_kw_names(self, full_name: str) -> Iterator[Tuple[str, ...]]:
174
+ tokens = full_name.split(".")
175
+ for i in range(1, len(tokens)):
176
+ yield ".".join(tokens[:i]), ".".join(tokens[i:])
177
+
178
+ def _get_explicit_keyword(self, name: str) -> Optional[KeywordDoc]:
179
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
180
+ for owner_name, kw_name in self._yield_owner_and_kw_names(name):
181
+ found.extend(self.find_keywords(owner_name, kw_name))
182
+
183
+ if get_robot_version() >= (6, 0) and len(found) > 1:
184
+ found = self._select_best_matches(found)
185
+
186
+ if len(found) > 1:
187
+ self.diagnostics.append(
188
+ DiagnosticsEntry(
189
+ self._create_multiple_keywords_found_message(name, found, implicit=False),
190
+ DiagnosticSeverity.ERROR,
191
+ Error.MULTIPLE_KEYWORDS,
192
+ )
193
+ )
194
+ raise CancelSearchError
195
+
196
+ return found[0][1] if found else None
197
+
198
+ def find_keywords(self, owner_name: str, name: str) -> List[Tuple[LibraryEntry, KeywordDoc]]:
199
+ if self._all_keywords is None:
200
+ self._all_keywords = list(
201
+ chain(
202
+ self.namespace._libraries.values(),
203
+ self.namespace._resources.values(),
204
+ )
205
+ )
206
+
207
+ if get_robot_version() >= (6, 0):
208
+ result: List[Tuple[LibraryEntry, KeywordDoc]] = []
209
+ for v in self._all_keywords:
210
+ if eq_namespace(v.alias or v.name, owner_name):
211
+ result.extend((v, kw) for kw in v.library_doc.keywords.iter_all(name))
212
+ return result
213
+
214
+ result = []
215
+ for v in self._all_keywords:
216
+ if eq_namespace(v.alias or v.name, owner_name):
217
+ kw = v.library_doc.keywords.get(name, None)
218
+ if kw is not None:
219
+ result.append((v, kw))
220
+ return result
221
+
222
+ def _add_to_multiple_keywords_result(self, kw: Iterable[KeywordDoc]) -> None:
223
+ if self.multiple_keywords_result is None:
224
+ self.multiple_keywords_result = list(kw)
225
+ else:
226
+ self.multiple_keywords_result.extend(kw)
227
+
228
+ def _create_multiple_keywords_found_message(
229
+ self,
230
+ name: str,
231
+ found: Sequence[Tuple[Optional[LibraryEntry], KeywordDoc]],
232
+ implicit: bool = True,
233
+ ) -> str:
234
+ self._add_to_multiple_keywords_result([k for _, k in found])
235
+
236
+ if any(e[1].is_embedded for e in found):
237
+ error = f"Multiple keywords matching name '{name}' found"
238
+ else:
239
+ error = f"Multiple keywords with name '{name}' found"
240
+
241
+ if implicit:
242
+ error += ". Give the full name of the keyword you want to use"
243
+
244
+ names = sorted(f"{e[1].name if e[0] is None else f'{e[0].alias or e[0].name}.{e[1].name}'}" for e in found)
245
+ return "\n ".join([f"{error}:", *names])
246
+
247
+ def _get_implicit_keyword(self, name: str) -> Optional[KeywordDoc]:
248
+ result = self._get_keyword_from_resource_files(name)
249
+ if not result:
250
+ return self._get_keyword_from_libraries(name)
251
+ return result
252
+
253
+ def _prioritize_same_file_or_public(
254
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
255
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
256
+ matches = [h for h in entries if h[1].source == self.namespace.source]
257
+ if matches:
258
+ return matches
259
+
260
+ matches = [handler for handler in entries if not handler[1].is_private()]
261
+
262
+ return matches or entries
263
+
264
+ def _select_best_matches(
265
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
266
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
267
+ normal = [hand for hand in entries if not hand[1].is_embedded]
268
+ if normal:
269
+ return normal
270
+
271
+ matches = [hand for hand in entries if not self._is_worse_match_than_others(hand, entries)]
272
+ return matches or entries
273
+
274
+ def _is_worse_match_than_others(
275
+ self,
276
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
277
+ alternatives: List[Tuple[Optional[LibraryEntry], KeywordDoc]],
278
+ ) -> bool:
279
+ for other in alternatives:
280
+ if (
281
+ candidate[1] is not other[1]
282
+ and self._is_better_match(other, candidate)
283
+ and not self._is_better_match(candidate, other)
284
+ ):
285
+ return True
286
+ return False
287
+
288
+ def _is_better_match(
289
+ self,
290
+ candidate: Tuple[Optional[LibraryEntry], KeywordDoc],
291
+ other: Tuple[Optional[LibraryEntry], KeywordDoc],
292
+ ) -> bool:
293
+ return (
294
+ other[1].matcher.embedded_arguments.match(candidate[1].name) is not None
295
+ and candidate[1].matcher.embedded_arguments.match(other[1].name) is None
296
+ )
297
+
298
+ def _get_keyword_from_resource_files(self, name: str) -> Optional[KeywordDoc]:
299
+ if self._resource_keywords is None:
300
+ self._resource_keywords = list(chain(self.namespace._resources.values()))
301
+
302
+ if get_robot_version() >= (6, 0):
303
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
304
+ for v in self._resource_keywords:
305
+ r = v.library_doc.keywords.get_all(name)
306
+ if r:
307
+ found.extend([(v, k) for k in r])
308
+ else:
309
+ found = []
310
+ for k in self._resource_keywords:
311
+ s = k.library_doc.keywords.get(name, None)
312
+ if s is not None:
313
+ found.append((k, s))
314
+
315
+ if not found:
316
+ return None
317
+
318
+ if get_robot_version() >= (6, 0):
319
+ if len(found) > 1:
320
+ found = self._prioritize_same_file_or_public(found)
321
+
322
+ if len(found) > 1:
323
+ found = self._select_best_matches(found)
324
+
325
+ if len(found) > 1:
326
+ found = self._get_keyword_based_on_search_order(found)
327
+
328
+ else:
329
+ if len(found) > 1:
330
+ found = self._get_keyword_based_on_search_order(found)
331
+
332
+ if len(found) == 1:
333
+ return found[0][1]
334
+
335
+ self.diagnostics.append(
336
+ DiagnosticsEntry(
337
+ self._create_multiple_keywords_found_message(name, found),
338
+ DiagnosticSeverity.ERROR,
339
+ Error.MULTIPLE_KEYWORDS,
340
+ )
341
+ )
342
+ raise CancelSearchError
343
+
344
+ def _get_keyword_based_on_search_order(
345
+ self, entries: List[Tuple[Optional[LibraryEntry], KeywordDoc]]
346
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
347
+ for libname in self.namespace.search_order:
348
+ for e in entries:
349
+ if e[0] is not None and eq_namespace(libname, e[0].alias or e[0].name):
350
+ return [e]
351
+
352
+ return entries
353
+
354
+ def _get_keyword_from_libraries(self, name: str) -> Optional[KeywordDoc]:
355
+ if self._library_keywords is None:
356
+ self._library_keywords = list(chain(self.namespace._libraries.values()))
357
+
358
+ if get_robot_version() >= (6, 0):
359
+ found: List[Tuple[Optional[LibraryEntry], KeywordDoc]] = []
360
+ for v in self._library_keywords:
361
+ r = v.library_doc.keywords.get_all(name)
362
+ if r:
363
+ found.extend([(v, k) for k in r])
364
+ else:
365
+ found = []
366
+
367
+ for k in self._library_keywords:
368
+ s = k.library_doc.keywords.get(name, None)
369
+ if s is not None:
370
+ found.append((k, s))
371
+
372
+ if not found:
373
+ return None
374
+
375
+ if get_robot_version() >= (6, 0):
376
+ if len(found) > 1:
377
+ found = self._select_best_matches(found)
378
+ if len(found) > 1:
379
+ found = self._get_keyword_based_on_search_order(found)
380
+ else:
381
+ if len(found) > 1:
382
+ found = self._get_keyword_based_on_search_order(found)
383
+ if len(found) == 2:
384
+ found = self._filter_stdlib_runner(*found)
385
+
386
+ if len(found) == 1:
387
+ return found[0][1]
388
+
389
+ self.diagnostics.append(
390
+ DiagnosticsEntry(
391
+ self._create_multiple_keywords_found_message(name, found),
392
+ DiagnosticSeverity.ERROR,
393
+ Error.MULTIPLE_KEYWORDS,
394
+ )
395
+ )
396
+ raise CancelSearchError
397
+
398
+ def _filter_stdlib_runner(
399
+ self,
400
+ entry1: Tuple[Optional[LibraryEntry], KeywordDoc],
401
+ entry2: Tuple[Optional[LibraryEntry], KeywordDoc],
402
+ ) -> List[Tuple[Optional[LibraryEntry], KeywordDoc]]:
403
+ stdlibs_without_remote = STDLIBS - {"Remote"}
404
+ if entry1[0] is not None and entry1[0].name in stdlibs_without_remote:
405
+ standard, custom = entry1, entry2
406
+ elif entry2[0] is not None and entry2[0].name in stdlibs_without_remote:
407
+ standard, custom = entry2, entry1
408
+ else:
409
+ return [entry1, entry2]
410
+
411
+ self.diagnostics.append(
412
+ DiagnosticsEntry(
413
+ self._create_custom_and_standard_keyword_conflict_warning_message(custom, standard),
414
+ DiagnosticSeverity.WARNING,
415
+ Error.CONFLICTING_LIBRARY_KEYWORDS,
416
+ )
417
+ )
418
+
419
+ return [custom]
420
+
421
+ def _create_custom_and_standard_keyword_conflict_warning_message(
422
+ self,
423
+ custom: Tuple[Optional[LibraryEntry], KeywordDoc],
424
+ standard: Tuple[Optional[LibraryEntry], KeywordDoc],
425
+ ) -> str:
426
+ custom_with_name = standard_with_name = ""
427
+ if custom[0] is not None and custom[0].alias is not None:
428
+ custom_with_name = " imported as '%s'" % custom[0].alias
429
+ if standard[0] is not None and standard[0].alias is not None:
430
+ standard_with_name = " imported as '%s'" % standard[0].alias
431
+ return (
432
+ f"Keyword '{standard[1].name}' found both from a custom test library "
433
+ f"'{'' if custom[0] is None else custom[0].name}'{custom_with_name} "
434
+ f"and a standard library '{standard[1].name}'{standard_with_name}. "
435
+ f"The custom keyword is used. To select explicitly, and to get "
436
+ f"rid of this warning, use either "
437
+ f"'{'' if custom[0] is None else custom[0].alias or custom[0].name}.{custom[1].name}' "
438
+ f"or '{'' if standard[0] is None else standard[0].alias or standard[0].name}.{standard[1].name}'."
439
+ )
440
+
441
+ def _get_bdd_style_keyword(self, name: str) -> Optional[KeywordDoc]:
442
+ if get_robot_version() < (6, 0):
443
+ lower = name.lower()
444
+ for prefix in ["given ", "when ", "then ", "and ", "but "]:
445
+ if lower.startswith(prefix):
446
+ return self._find_keyword(name[len(prefix) :])
447
+ return None
448
+
449
+ parts = name.split()
450
+ if len(parts) < 2:
451
+ return None
452
+ for index in range(1, len(parts)):
453
+ prefix = " ".join(parts[:index]).title()
454
+ if prefix.title() in (
455
+ self.namespace.languages.bdd_prefixes if self.namespace.languages is not None else DEFAULT_BDD_PREFIXES
456
+ ):
457
+ return self._find_keyword(" ".join(parts[index:]))
458
+ return None
@@ -74,6 +74,7 @@ from robotcode.robot.diagnostics.entities import (
74
74
  )
75
75
  from robotcode.robot.utils import get_robot_version
76
76
  from robotcode.robot.utils.ast import (
77
+ cached_isinstance,
77
78
  get_variable_token,
78
79
  range_from_token,
79
80
  strip_variable_token,
@@ -247,7 +248,7 @@ class KeywordMatcher:
247
248
  return self._embedded_arguments
248
249
 
249
250
  def __eq__(self, o: object) -> bool:
250
- if isinstance(o, KeywordMatcher):
251
+ if cached_isinstance(o, KeywordMatcher):
251
252
  if self._is_namespace != o._is_namespace:
252
253
  return False
253
254
 
@@ -256,7 +257,7 @@ class KeywordMatcher:
256
257
 
257
258
  o = o.name
258
259
 
259
- if not isinstance(o, str):
260
+ if not cached_isinstance(o, str):
260
261
  return False
261
262
 
262
263
  if self.embedded_arguments:
@@ -907,7 +908,8 @@ class KeywordStore:
907
908
  return self.__matchers
908
909
 
909
910
  def __getitem__(self, key: str) -> KeywordDoc:
910
- items = [(k, v) for k, v in self._matchers.items() if k == key]
911
+ key_matcher = KeywordMatcher(key)
912
+ items = [(k, v) for k, v in self._matchers.items() if k == key_matcher]
911
913
 
912
914
  if not items:
913
915
  raise KeyError
@@ -933,8 +935,10 @@ class KeywordStore:
933
935
  multiple_keywords=[v for _, v in items],
934
936
  )
935
937
 
936
- def __contains__(self, __x: object) -> bool:
937
- return any(k == __x for k in self._matchers.keys())
938
+ def __contains__(self, _x: object) -> bool:
939
+ if not isinstance(_x, KeywordMatcher):
940
+ _x = KeywordMatcher(str(_x))
941
+ return any(k == _x for k in self._matchers.keys())
938
942
 
939
943
  def __len__(self) -> int:
940
944
  return len(self.keywords)
@@ -961,7 +965,11 @@ class KeywordStore:
961
965
  return default
962
966
 
963
967
  def get_all(self, key: str) -> List[KeywordDoc]:
964
- return [v for k, v in self._matchers.items() if k == key]
968
+ return list(self.iter_all(key))
969
+
970
+ def iter_all(self, key: str) -> Iterable[KeywordDoc]:
971
+ key_matcher = KeywordMatcher(key)
972
+ yield from (v for k, v in self._matchers.items() if k == key_matcher)
965
973
 
966
974
 
967
975
  @dataclass
@@ -1540,7 +1548,6 @@ def resolve_robot_variables(
1540
1548
  command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1541
1549
  variables: Optional[Dict[str, Optional[Any]]] = None,
1542
1550
  ) -> Any:
1543
-
1544
1551
  result: Variables = _get_default_variables().copy()
1545
1552
 
1546
1553
  for k, v in {
@@ -1554,14 +1561,16 @@ def resolve_robot_variables(
1554
1561
  result[k1] = v1
1555
1562
 
1556
1563
  if variables is not None:
1557
- vars = [_Variable(k, v) for k, v in variables.items() if v is not None and not isinstance(v, NativeValue)]
1564
+ vars = [
1565
+ _Variable(k, v) for k, v in variables.items() if v is not None and not cached_isinstance(v, NativeValue)
1566
+ ]
1558
1567
  if get_robot_version() < (7, 0):
1559
1568
  result.set_from_variable_table(vars)
1560
1569
  else:
1561
1570
  result.set_from_variable_section(vars)
1562
1571
 
1563
1572
  for k2, v2 in variables.items():
1564
- if isinstance(v2, NativeValue):
1573
+ if cached_isinstance(v2, NativeValue):
1565
1574
  result[k2] = v2.value
1566
1575
 
1567
1576
  result.resolve_delayed()
@@ -1576,7 +1585,6 @@ def resolve_variable(
1576
1585
  command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1577
1586
  variables: Optional[Dict[str, Optional[Any]]] = None,
1578
1587
  ) -> Any:
1579
-
1580
1588
  _update_env(working_dir)
1581
1589
 
1582
1590
  if contains_variable(name, "$@&%"):
@@ -2645,8 +2653,6 @@ def complete_variables_import(
2645
2653
  def get_model_doc(
2646
2654
  model: ast.AST,
2647
2655
  source: str,
2648
- model_type: str = "RESOURCE",
2649
- scope: str = "GLOBAL",
2650
2656
  append_model_errors: bool = True,
2651
2657
  ) -> LibraryDoc:
2652
2658
  errors: List[Error] = []
@@ -2777,8 +2783,8 @@ def get_model_doc(
2777
2783
  libdoc = LibraryDoc(
2778
2784
  name=lib.name or "",
2779
2785
  doc=lib.doc,
2780
- type=model_type,
2781
- scope=scope,
2786
+ type="RESOURCE",
2787
+ scope="GLOBAL",
2782
2788
  source=source,
2783
2789
  line_no=1,
2784
2790
  errors=errors,