robotcode-robot 0.93.0__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.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/diagnostics/document_cache_helper.py +11 -6
- robotcode/robot/diagnostics/entities.py +2 -2
- robotcode/robot/diagnostics/errors.py +6 -1
- robotcode/robot/diagnostics/imports_manager.py +79 -37
- robotcode/robot/diagnostics/keyword_finder.py +458 -0
- robotcode/robot/diagnostics/library_doc.py +37 -19
- robotcode/robot/diagnostics/model_helper.py +42 -27
- robotcode/robot/diagnostics/namespace.py +142 -552
- robotcode/robot/diagnostics/namespace_analyzer.py +952 -462
- robotcode/robot/utils/markdownformatter.py +8 -11
- robotcode/robot/utils/visitor.py +7 -13
- {robotcode_robot-0.93.0.dist-info → robotcode_robot-0.94.0.dist-info}/METADATA +2 -2
- robotcode_robot-0.94.0.dist-info/RECORD +31 -0
- robotcode_robot-0.93.0.dist-info/RECORD +0 -30
- {robotcode_robot-0.93.0.dist-info → robotcode_robot-0.94.0.dist-info}/WHEEL +0 -0
- {robotcode_robot-0.93.0.dist-info → robotcode_robot-0.94.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -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
|
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
|
260
|
+
if not cached_isinstance(o, str):
|
260
261
|
return False
|
261
262
|
|
262
263
|
if self.embedded_arguments:
|
@@ -711,6 +712,7 @@ class KeywordDoc(SourceEntity):
|
|
711
712
|
add_signature: bool = True,
|
712
713
|
header_level: int = 2,
|
713
714
|
add_type: bool = True,
|
715
|
+
modify_doc_handler: Optional[Callable[[str], str]] = None,
|
714
716
|
) -> str:
|
715
717
|
result = ""
|
716
718
|
|
@@ -723,12 +725,18 @@ class KeywordDoc(SourceEntity):
|
|
723
725
|
|
724
726
|
result += f"##{'#' * header_level} Documentation:\n"
|
725
727
|
|
728
|
+
doc: Optional[str] = None
|
726
729
|
if self.doc_format == ROBOT_DOC_FORMAT:
|
727
|
-
|
730
|
+
doc = MarkDownFormatter().format(self.doc)
|
728
731
|
elif self.doc_format == REST_DOC_FORMAT:
|
729
|
-
|
732
|
+
doc = convert_from_rest(self.doc)
|
730
733
|
else:
|
731
|
-
|
734
|
+
doc = self.doc
|
735
|
+
|
736
|
+
if doc is not None:
|
737
|
+
if modify_doc_handler is not None:
|
738
|
+
doc = modify_doc_handler(doc)
|
739
|
+
result += doc
|
732
740
|
|
733
741
|
return result
|
734
742
|
|
@@ -900,7 +908,8 @@ class KeywordStore:
|
|
900
908
|
return self.__matchers
|
901
909
|
|
902
910
|
def __getitem__(self, key: str) -> KeywordDoc:
|
903
|
-
|
911
|
+
key_matcher = KeywordMatcher(key)
|
912
|
+
items = [(k, v) for k, v in self._matchers.items() if k == key_matcher]
|
904
913
|
|
905
914
|
if not items:
|
906
915
|
raise KeyError
|
@@ -926,8 +935,10 @@ class KeywordStore:
|
|
926
935
|
multiple_keywords=[v for _, v in items],
|
927
936
|
)
|
928
937
|
|
929
|
-
def __contains__(self,
|
930
|
-
|
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())
|
931
942
|
|
932
943
|
def __len__(self) -> int:
|
933
944
|
return len(self.keywords)
|
@@ -954,7 +965,11 @@ class KeywordStore:
|
|
954
965
|
return default
|
955
966
|
|
956
967
|
def get_all(self, key: str) -> List[KeywordDoc]:
|
957
|
-
return
|
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)
|
958
973
|
|
959
974
|
|
960
975
|
@dataclass
|
@@ -1533,7 +1548,6 @@ def resolve_robot_variables(
|
|
1533
1548
|
command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
|
1534
1549
|
variables: Optional[Dict[str, Optional[Any]]] = None,
|
1535
1550
|
) -> Any:
|
1536
|
-
|
1537
1551
|
result: Variables = _get_default_variables().copy()
|
1538
1552
|
|
1539
1553
|
for k, v in {
|
@@ -1547,14 +1561,16 @@ def resolve_robot_variables(
|
|
1547
1561
|
result[k1] = v1
|
1548
1562
|
|
1549
1563
|
if variables is not None:
|
1550
|
-
vars = [
|
1564
|
+
vars = [
|
1565
|
+
_Variable(k, v) for k, v in variables.items() if v is not None and not cached_isinstance(v, NativeValue)
|
1566
|
+
]
|
1551
1567
|
if get_robot_version() < (7, 0):
|
1552
1568
|
result.set_from_variable_table(vars)
|
1553
1569
|
else:
|
1554
1570
|
result.set_from_variable_section(vars)
|
1555
1571
|
|
1556
1572
|
for k2, v2 in variables.items():
|
1557
|
-
if
|
1573
|
+
if cached_isinstance(v2, NativeValue):
|
1558
1574
|
result[k2] = v2.value
|
1559
1575
|
|
1560
1576
|
result.resolve_delayed()
|
@@ -1569,7 +1585,6 @@ def resolve_variable(
|
|
1569
1585
|
command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
|
1570
1586
|
variables: Optional[Dict[str, Optional[Any]]] = None,
|
1571
1587
|
) -> Any:
|
1572
|
-
|
1573
1588
|
_update_env(working_dir)
|
1574
1589
|
|
1575
1590
|
if contains_variable(name, "$@&%"):
|
@@ -1588,6 +1603,7 @@ def replace_variables_scalar(
|
|
1588
1603
|
base_dir: str = ".",
|
1589
1604
|
command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
|
1590
1605
|
variables: Optional[Dict[str, Optional[Any]]] = None,
|
1606
|
+
ignore_errors: bool = False,
|
1591
1607
|
) -> Any:
|
1592
1608
|
|
1593
1609
|
_update_env(working_dir)
|
@@ -1595,9 +1611,13 @@ def replace_variables_scalar(
|
|
1595
1611
|
if contains_variable(scalar, "$@&%"):
|
1596
1612
|
robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
|
1597
1613
|
if get_robot_version() >= (6, 1):
|
1598
|
-
return VariableReplacer(robot_variables).replace_scalar(
|
1614
|
+
return VariableReplacer(robot_variables).replace_scalar(
|
1615
|
+
scalar.replace("\\", "\\\\"), ignore_errors=ignore_errors
|
1616
|
+
)
|
1599
1617
|
|
1600
|
-
return VariableReplacer(robot_variables.store).replace_scalar(
|
1618
|
+
return VariableReplacer(robot_variables.store).replace_scalar(
|
1619
|
+
scalar.replace("\\", "\\\\"), ignore_errors=ignore_errors
|
1620
|
+
)
|
1601
1621
|
|
1602
1622
|
return scalar.replace("\\", "\\\\")
|
1603
1623
|
|
@@ -2633,8 +2653,6 @@ def complete_variables_import(
|
|
2633
2653
|
def get_model_doc(
|
2634
2654
|
model: ast.AST,
|
2635
2655
|
source: str,
|
2636
|
-
model_type: str = "RESOURCE",
|
2637
|
-
scope: str = "GLOBAL",
|
2638
2656
|
append_model_errors: bool = True,
|
2639
2657
|
) -> LibraryDoc:
|
2640
2658
|
errors: List[Error] = []
|
@@ -2765,8 +2783,8 @@ def get_model_doc(
|
|
2765
2783
|
libdoc = LibraryDoc(
|
2766
2784
|
name=lib.name or "",
|
2767
2785
|
doc=lib.doc,
|
2768
|
-
type=
|
2769
|
-
scope=
|
2786
|
+
type="RESOURCE",
|
2787
|
+
scope="GLOBAL",
|
2770
2788
|
source=source,
|
2771
2789
|
line_no=1,
|
2772
2790
|
errors=errors,
|