robotcode-robot 0.93.0__py3-none-any.whl → 0.94.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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,
|