robotcode-robot 0.68.0__py3-none-any.whl → 0.68.2__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,2799 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import hashlib
5
+ import importlib
6
+ import importlib.util
7
+ import io
8
+ import os
9
+ import pkgutil
10
+ import re
11
+ import sys
12
+ import tempfile
13
+ import traceback
14
+ from contextlib import contextmanager
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from pathlib import Path
18
+ from typing import (
19
+ AbstractSet,
20
+ Any,
21
+ Callable,
22
+ Dict,
23
+ Iterable,
24
+ Iterator,
25
+ List,
26
+ NamedTuple,
27
+ Optional,
28
+ Sequence,
29
+ Set,
30
+ Tuple,
31
+ TypeVar,
32
+ Union,
33
+ cast,
34
+ )
35
+
36
+ from robot.parsing.lexer.tokens import Token
37
+ from robotcode.core.lsp.types import Position, Range
38
+ from robotcode.robot.diagnostics.entities import (
39
+ ArgumentDefinition,
40
+ ImportedVariableDefinition,
41
+ LibraryArgumentDefinition,
42
+ NativeValue,
43
+ SourceEntity,
44
+ single_call,
45
+ )
46
+ from robotcode.robot.utils import get_robot_version
47
+ from robotcode.robot.utils.ast import (
48
+ get_variable_token,
49
+ range_from_token,
50
+ strip_variable_token,
51
+ )
52
+ from robotcode.robot.utils.markdownformatter import MarkDownFormatter
53
+ from robotcode.robot.utils.match import normalize, normalize_namespace
54
+ from robotcode.robot.utils.stubs import HasError, HasErrors
55
+
56
+ RUN_KEYWORD_NAMES = [
57
+ "Run Keyword",
58
+ "Run Keyword And Continue On Failure",
59
+ "Run Keyword And Ignore Error",
60
+ "Run Keyword And Return",
61
+ "Run Keyword And Return Status",
62
+ "Run Keyword If All Critical Tests Passed",
63
+ "Run Keyword If All Tests Passed",
64
+ "Run Keyword If Any Critical Tests Failed",
65
+ "Run Keyword If Any Tests Failed",
66
+ "Run Keyword If Test Failed",
67
+ "Run Keyword If Test Passed",
68
+ "Run Keyword If Timeout Occurred",
69
+ "Run Keyword And Warn On Failure",
70
+ ]
71
+
72
+ RUN_KEYWORD_WITH_CONDITION_NAMES: Dict[str, int] = {
73
+ "Run Keyword And Expect Error": 1,
74
+ "Run Keyword And Return If": 1,
75
+ "Run Keyword Unless": 1,
76
+ "Repeat Keyword": 1,
77
+ "Wait Until Keyword Succeeds": 2,
78
+ }
79
+
80
+ RUN_KEYWORD_IF_NAME = "Run Keyword If"
81
+
82
+ RUN_KEYWORDS_NAME = "Run Keywords"
83
+
84
+ ALL_RUN_KEYWORDS = [
85
+ *RUN_KEYWORD_NAMES,
86
+ *RUN_KEYWORD_WITH_CONDITION_NAMES.keys(),
87
+ RUN_KEYWORDS_NAME,
88
+ RUN_KEYWORD_IF_NAME,
89
+ ]
90
+
91
+ BUILTIN_LIBRARY_NAME = "BuiltIn"
92
+ RESERVED_LIBRARY_NAME = "Reserved"
93
+
94
+ if get_robot_version() < (7, 0):
95
+ DEFAULT_LIBRARIES = {BUILTIN_LIBRARY_NAME, RESERVED_LIBRARY_NAME, "Easter"}
96
+ else:
97
+ DEFAULT_LIBRARIES = {BUILTIN_LIBRARY_NAME, "Easter"}
98
+
99
+ ROBOT_LIBRARY_PACKAGE = "robot.libraries"
100
+
101
+ ALLOWED_LIBRARY_FILE_EXTENSIONS = [".py"]
102
+
103
+ ROBOT_FILE_EXTENSION = ".robot"
104
+ RESOURCE_FILE_EXTENSION = ".resource"
105
+ REST_EXTENSIONS = {".rst", ".rest"}
106
+ JSON_EXTENSIONS = {".json", ".rsrc"}
107
+
108
+ ALLOWED_RESOURCE_FILE_EXTENSIONS = (
109
+ {
110
+ ROBOT_FILE_EXTENSION,
111
+ RESOURCE_FILE_EXTENSION,
112
+ *REST_EXTENSIONS,
113
+ *JSON_EXTENSIONS,
114
+ }
115
+ if get_robot_version() >= (6, 1)
116
+ else {ROBOT_FILE_EXTENSION, RESOURCE_FILE_EXTENSION, *REST_EXTENSIONS}
117
+ )
118
+
119
+ ALLOWED_VARIABLES_FILE_EXTENSIONS = (
120
+ {".py", ".yml", ".yaml", ".json"} if get_robot_version() >= (6, 1) else {".py", ".yml", ".yaml"}
121
+ )
122
+ ROBOT_DOC_FORMAT = "ROBOT"
123
+ REST_DOC_FORMAT = "REST"
124
+
125
+ _F = TypeVar("_F", bound=Callable[..., Any])
126
+
127
+
128
+ def convert_from_rest(text: str) -> str:
129
+ try:
130
+ from docutils.core import publish_parts
131
+
132
+ parts = publish_parts(
133
+ text,
134
+ writer_name="html5",
135
+ settings_overrides={"syntax_highlight": "none"},
136
+ )
137
+
138
+ return str(parts["html_body"])
139
+
140
+ except ImportError:
141
+ pass
142
+
143
+ return text
144
+
145
+
146
+ def is_embedded_keyword(name: str) -> bool:
147
+ from robot.errors import DataError, VariableError
148
+ from robot.running.arguments.embedded import EmbeddedArguments
149
+
150
+ try:
151
+ if get_robot_version() >= (6, 0):
152
+ if EmbeddedArguments.from_name(name):
153
+ return True
154
+ else:
155
+ if EmbeddedArguments(name):
156
+ return True
157
+ except (VariableError, DataError):
158
+ return True
159
+
160
+ return False
161
+
162
+
163
+ class KeywordMatcher:
164
+ def __init__(
165
+ self,
166
+ name: str,
167
+ can_have_embedded: bool = True,
168
+ is_namespace: bool = False,
169
+ ) -> None:
170
+ self.name = name
171
+ self._can_have_embedded = can_have_embedded and not is_namespace
172
+ self._is_namespace = is_namespace
173
+ self._normalized_name: Optional[str] = None
174
+ self._embedded_arguments: Any = None
175
+
176
+ @property
177
+ def normalized_name(self) -> str:
178
+ if self._normalized_name is None:
179
+ self._normalized_name = str(normalize_namespace(self.name) if self._is_namespace else normalize(self.name))
180
+
181
+ return self._normalized_name
182
+
183
+ @property
184
+ def embedded_arguments(self) -> Any:
185
+ from robot.errors import DataError, VariableError
186
+ from robot.running.arguments.embedded import EmbeddedArguments
187
+
188
+ if self._embedded_arguments is None:
189
+ if self._can_have_embedded:
190
+ try:
191
+ if get_robot_version() >= (6, 0):
192
+ self._embedded_arguments = EmbeddedArguments.from_name(self.name)
193
+ else:
194
+ self._embedded_arguments = EmbeddedArguments(self.name)
195
+ except (VariableError, DataError):
196
+ self._embedded_arguments = ()
197
+ else:
198
+ self._embedded_arguments = ()
199
+
200
+ return self._embedded_arguments
201
+
202
+ def __eq__(self, o: object) -> bool:
203
+ if isinstance(o, KeywordMatcher):
204
+ if self._is_namespace != o._is_namespace:
205
+ return False
206
+
207
+ if not self.embedded_arguments:
208
+ return self.normalized_name == o.normalized_name
209
+
210
+ o = o.name
211
+
212
+ if not isinstance(o, str):
213
+ return False
214
+
215
+ if self.embedded_arguments:
216
+ if get_robot_version() >= (6, 0):
217
+ return self.embedded_arguments.match(o) is not None
218
+
219
+ return self.embedded_arguments.name.match(o) is not None
220
+
221
+ return self.normalized_name == str(normalize_namespace(o) if self._is_namespace else normalize(o))
222
+
223
+ @single_call
224
+ def __hash__(self) -> int:
225
+ return hash(
226
+ (self.embedded_arguments.name, tuple(self.embedded_arguments.args))
227
+ if self.embedded_arguments
228
+ else (self.normalized_name, self._is_namespace)
229
+ )
230
+
231
+ def __str__(self) -> str:
232
+ return self.name
233
+
234
+ def __repr__(self) -> str:
235
+ return f"{type(self).__name__}(name={self.name!r})"
236
+
237
+
238
+ RUN_KEYWORD_WITH_CONDITION_MATCHERS = [KeywordMatcher(e) for e in RUN_KEYWORD_WITH_CONDITION_NAMES]
239
+
240
+ RUN_KEYWORD_IF_MATCHER = KeywordMatcher(RUN_KEYWORD_IF_NAME)
241
+
242
+ RUN_KEYWORDS_MATCHER = KeywordMatcher(RUN_KEYWORDS_NAME)
243
+
244
+ ALL_RUN_KEYWORDS_MATCHERS = [KeywordMatcher(e) for e in ALL_RUN_KEYWORDS]
245
+
246
+
247
+ class TypeDocType(Enum):
248
+ ENUM = "Enum"
249
+ TYPED_DICT = "TypedDict"
250
+ CUSTOM = "Custom"
251
+ STANDARD = "Standard"
252
+
253
+
254
+ @dataclass
255
+ class TypedDictItem:
256
+ key: str
257
+ type: str
258
+ required: Optional[bool] = None
259
+
260
+
261
+ @dataclass
262
+ class EnumMember:
263
+ name: str
264
+ value: str
265
+
266
+
267
+ @dataclass
268
+ class TypeDoc:
269
+ type: str
270
+ name: str
271
+ doc: Optional[str] = None
272
+ accepts: List[str] = field(default_factory=list)
273
+ usages: List[str] = field(default_factory=list)
274
+ members: Optional[List[EnumMember]] = None
275
+ items: Optional[List[TypedDictItem]] = None
276
+ libname: Optional[str] = None
277
+ libtype: Optional[str] = None
278
+
279
+ doc_format: str = ROBOT_DOC_FORMAT
280
+
281
+ @single_call
282
+ def __hash__(self) -> int:
283
+ return hash(
284
+ (
285
+ self.type,
286
+ self.name,
287
+ self.libname,
288
+ self.libtype,
289
+ tuple(self.accepts),
290
+ tuple(self.usages),
291
+ )
292
+ )
293
+
294
+ def to_markdown(self, header_level: int = 2) -> str:
295
+ result = ""
296
+
297
+ result += f"##{'#' * header_level} {self.name} ({self.type})\n\n"
298
+
299
+ if self.doc:
300
+ result += f"###{'#' * header_level} Documentation:\n\n"
301
+ result += "\n\n"
302
+
303
+ if self.doc_format == ROBOT_DOC_FORMAT:
304
+ result += MarkDownFormatter().format(self.doc)
305
+ elif self.doc_format == REST_DOC_FORMAT:
306
+ result += convert_from_rest(self.doc)
307
+ else:
308
+ result += self.doc
309
+
310
+ if self.members:
311
+ result += f"\n\n###{'#' * header_level} Allowed Values:\n\n"
312
+ result += "- " + "\n- ".join(f"`{m.name}`" for m in self.members)
313
+
314
+ if self.items:
315
+ result += f"\n\n###{'#' * header_level} Dictionary Structure:\n\n"
316
+ result += "```\n{"
317
+ result += "\n ".join(f"'{m.key}': <{m.type}> {'# optional' if not m.required else ''}" for m in self.items)
318
+ result += "\n}\n```"
319
+
320
+ if self.accepts:
321
+ result += f"\n\n###{'#' * header_level} Converted Types:\n\n"
322
+ result += "- " + "\n- ".join(self.accepts)
323
+
324
+ return result
325
+
326
+
327
+ @dataclass
328
+ class SourceAndLineInfo:
329
+ source: str
330
+ line_no: int
331
+
332
+
333
+ @dataclass
334
+ class Error:
335
+ message: str
336
+ type_name: str
337
+ source: Optional[str] = None
338
+ line_no: Optional[int] = None
339
+ message_traceback: Optional[List[SourceAndLineInfo]] = None
340
+ traceback: Optional[List[SourceAndLineInfo]] = None
341
+
342
+
343
+ class KeywordArgumentKind(Enum):
344
+ POSITIONAL_ONLY = "POSITIONAL_ONLY"
345
+ POSITIONAL_ONLY_MARKER = "POSITIONAL_ONLY_MARKER"
346
+ POSITIONAL_OR_NAMED = "POSITIONAL_OR_NAMED"
347
+ VAR_POSITIONAL = "VAR_POSITIONAL"
348
+ NAMED_ONLY_MARKER = "NAMED_ONLY_MARKER"
349
+ NAMED_ONLY = "NAMED_ONLY"
350
+ VAR_NAMED = "VAR_NAMED"
351
+
352
+
353
+ def robot_arg_repr(arg: Any) -> Optional[str]:
354
+ from robot.running.arguments.argumentspec import ArgInfo
355
+
356
+ if get_robot_version() >= (7, 0):
357
+ from robot.utils import NOT_SET as robot_notset # noqa: N811
358
+ else:
359
+ robot_notset = ArgInfo.NOTSET
360
+
361
+ robot_arg = cast(ArgInfo, arg)
362
+
363
+ if robot_arg.default is robot_notset:
364
+ return None
365
+
366
+ if robot_arg.default is None:
367
+ return "${None}"
368
+
369
+ if isinstance(robot_arg.default, (int, float, bool)):
370
+ return f"${{{robot_arg.default!r}}}"
371
+
372
+ if isinstance(robot_arg.default, str) and robot_arg.default == "":
373
+ return "${EMPTY}"
374
+
375
+ if isinstance(robot_arg.default, str) and robot_arg.default == "\\ \\":
376
+ return "${SPACE}"
377
+
378
+ return str(robot_arg.default_repr)
379
+
380
+
381
+ @dataclass
382
+ class ArgumentInfo:
383
+ name: str
384
+ str_repr: str
385
+ kind: KeywordArgumentKind
386
+ required: bool
387
+ default_value: Optional[Any] = None
388
+ types: Optional[List[str]] = None
389
+
390
+ @staticmethod
391
+ def from_robot(arg: Any) -> ArgumentInfo:
392
+ from robot.running.arguments.argumentspec import ArgInfo
393
+
394
+ robot_arg = cast(ArgInfo, arg)
395
+
396
+ return ArgumentInfo(
397
+ name=robot_arg.name,
398
+ default_value=robot_arg_repr(robot_arg),
399
+ str_repr=str(arg),
400
+ types=robot_arg.types_reprs
401
+ if get_robot_version() < (7, 0)
402
+ else ([str(robot_arg.type)] if not robot_arg.type.is_union else [str(t) for t in robot_arg.type.nested])
403
+ if robot_arg.type
404
+ else None,
405
+ kind=KeywordArgumentKind[robot_arg.kind],
406
+ required=robot_arg.required,
407
+ )
408
+
409
+ def __str__(self) -> str:
410
+ return self.signature()
411
+
412
+ def signature(self, add_types: bool = True) -> str:
413
+ prefix = ""
414
+ if self.kind == KeywordArgumentKind.POSITIONAL_ONLY_MARKER:
415
+ prefix = "*\u200d"
416
+ elif self.kind == KeywordArgumentKind.NAMED_ONLY_MARKER:
417
+ prefix = "/\u200d"
418
+ elif self.kind == KeywordArgumentKind.VAR_NAMED:
419
+ prefix = "*\u200d*\u200d"
420
+ elif self.kind == KeywordArgumentKind.VAR_POSITIONAL:
421
+ prefix = "*\u200d"
422
+ elif self.kind == KeywordArgumentKind.NAMED_ONLY:
423
+ prefix = "🏷\u200d"
424
+ elif self.kind == KeywordArgumentKind.POSITIONAL_ONLY:
425
+ prefix = "⟶\u200d"
426
+ result = f"{prefix}{self.name!s}"
427
+ if add_types:
428
+ result += (
429
+ f"{(': ' + (' | '.join(f'{s}' for s in self.types))) if self.types else ''}"
430
+ f"{' =' if self.default_value is not None else ''}"
431
+ f"{f' {self.default_value!s}' if self.default_value else ''}"
432
+ if add_types
433
+ else ""
434
+ )
435
+ return result
436
+
437
+ def __hash__(self) -> int:
438
+ return id(self)
439
+
440
+
441
+ DEPRECATED_PATTERN = re.compile(r"^\*DEPRECATED(?P<message>.*)\*(?P<doc>.*)")
442
+
443
+
444
+ @dataclass
445
+ class ArgumentSpec:
446
+ name: Optional[str]
447
+ type: str
448
+ positional_only: List[str]
449
+ positional_or_named: List[str]
450
+ var_positional: Any
451
+ named_only: Any
452
+ var_named: Any
453
+ embedded: Any
454
+ defaults: Any
455
+ types: Optional[Dict[str, str]] = None
456
+ return_type: Optional[str] = None
457
+
458
+ @staticmethod
459
+ def from_robot_argument_spec(spec: Any) -> ArgumentSpec:
460
+ return ArgumentSpec(
461
+ name=spec.name,
462
+ type=str(spec.type),
463
+ positional_only=spec.positional_only,
464
+ positional_or_named=spec.positional_or_named,
465
+ var_positional=spec.var_positional,
466
+ named_only=spec.named_only,
467
+ var_named=spec.var_named,
468
+ embedded=spec.embedded if get_robot_version() >= (7, 0) else None,
469
+ defaults={k: str(v) for k, v in spec.defaults.items()} if spec.defaults else {},
470
+ types={k: str(v) for k, v in spec.types.items()} if get_robot_version() > (7, 0) and spec.types else None,
471
+ return_type=str(spec.return_type) if get_robot_version() > (7, 0) and spec.return_type else None,
472
+ )
473
+
474
+ def resolve(
475
+ self,
476
+ arguments: Any,
477
+ variables: Any,
478
+ resolve_named: bool = True,
479
+ resolve_variables_until: Any = None,
480
+ dict_to_kwargs: bool = False,
481
+ validate: bool = True,
482
+ ) -> Tuple[List[Any], List[Tuple[str, Any]]]:
483
+ from robot.running.arguments.argumentresolver import (
484
+ ArgumentResolver,
485
+ DictToKwargs,
486
+ NamedArgumentResolver,
487
+ VariableReplacer,
488
+ )
489
+ from robot.running.arguments.argumentspec import (
490
+ ArgumentSpec as RobotArgumentSpec,
491
+ )
492
+
493
+ if not hasattr(self, "__robot_arguments"):
494
+ if get_robot_version() < (7, 0):
495
+ self.__robot_arguments = RobotArgumentSpec(
496
+ self.name,
497
+ self.type,
498
+ self.positional_only,
499
+ self.positional_or_named,
500
+ self.var_positional,
501
+ self.named_only,
502
+ self.var_named,
503
+ self.defaults,
504
+ None,
505
+ )
506
+ else:
507
+ self.__robot_arguments = RobotArgumentSpec(
508
+ self.name,
509
+ self.type,
510
+ self.positional_only,
511
+ self.positional_or_named,
512
+ self.var_positional,
513
+ self.named_only,
514
+ self.var_named,
515
+ self.defaults,
516
+ self.embedded,
517
+ None,
518
+ )
519
+ self.__robot_arguments.name = self.name
520
+ if validate:
521
+ if get_robot_version() < (7, 0):
522
+ resolver = ArgumentResolver(
523
+ self.__robot_arguments,
524
+ resolve_named=resolve_named,
525
+ resolve_variables_until=resolve_variables_until,
526
+ dict_to_kwargs=dict_to_kwargs,
527
+ )
528
+ else:
529
+ resolver = ArgumentResolver(
530
+ self.__robot_arguments,
531
+ resolve_named=resolve_named,
532
+ resolve_args_until=resolve_variables_until,
533
+ dict_to_kwargs=dict_to_kwargs,
534
+ )
535
+ return cast(
536
+ Tuple[List[Any], List[Tuple[str, Any]]],
537
+ resolver.resolve(arguments, variables),
538
+ )
539
+
540
+ class MyNamedArgumentResolver(NamedArgumentResolver):
541
+ def _raise_positional_after_named(self) -> None:
542
+ pass
543
+
544
+ positional, named = MyNamedArgumentResolver(self.__robot_arguments).resolve(arguments, variables)
545
+ if get_robot_version() < (7, 0):
546
+ positional, named = VariableReplacer(resolve_variables_until).replace(positional, named, variables)
547
+ else:
548
+ positional, named = VariableReplacer(self.__robot_arguments, resolve_variables_until).replace(
549
+ positional, named, variables
550
+ )
551
+ positional, named = DictToKwargs(self.__robot_arguments, dict_to_kwargs).handle(positional, named)
552
+ return positional, named
553
+
554
+
555
+ @dataclass
556
+ class KeywordDoc(SourceEntity):
557
+ name: str = ""
558
+ name_token: Optional[Token] = field(default=None, compare=False)
559
+ arguments: List[ArgumentInfo] = field(default_factory=list, compare=False)
560
+ arguments_spec: Optional[ArgumentSpec] = field(default=None, compare=False)
561
+ argument_definitions: Optional[List[ArgumentDefinition]] = field(
562
+ default=None, compare=False, metadata={"nosave": True}
563
+ )
564
+ doc: str = field(default="", compare=False)
565
+ tags: List[str] = field(default_factory=list)
566
+ type: str = "keyword"
567
+ libname: Optional[str] = None
568
+ libtype: Optional[str] = None
569
+ longname: Optional[str] = None
570
+ is_embedded: bool = False
571
+ errors: Optional[List[Error]] = field(default=None, compare=False)
572
+ doc_format: str = ROBOT_DOC_FORMAT
573
+ is_error_handler: bool = False
574
+ error_handler_message: Optional[str] = field(default=None, compare=False)
575
+ is_initializer: bool = False
576
+ is_registered_run_keyword: bool = field(default=False, compare=False)
577
+ args_to_process: Optional[int] = field(default=None, compare=False)
578
+ deprecated: bool = field(default=False, compare=False)
579
+ return_type: Optional[str] = field(default=None, compare=False)
580
+
581
+ parent_digest: Optional[str] = field(default=None, init=False, metadata={"nosave": True})
582
+ parent: Optional[LibraryDoc] = field(default=None, init=False, metadata={"nosave": True})
583
+
584
+ def _get_argument_definitions(self) -> Optional[List[ArgumentDefinition]]:
585
+ return (
586
+ [
587
+ LibraryArgumentDefinition(
588
+ self.line_no if self.line_no is not None else -1,
589
+ col_offset=-1,
590
+ end_line_no=-1,
591
+ end_col_offset=-1,
592
+ source=self.source,
593
+ name="${" + a.name + "}",
594
+ name_token=None,
595
+ keyword_doc=self,
596
+ )
597
+ for a in self.arguments
598
+ ]
599
+ if self.arguments_spec is not None and not self.is_error_handler and self.arguments
600
+ else []
601
+ )
602
+
603
+ digest: Optional[str] = field(init=False)
604
+
605
+ def __post_init__(self) -> None:
606
+ s = (
607
+ f"{self.name}|{self.source}|{self.line_no}|"
608
+ f"{self.end_line_no}|{self.col_offset}|{self.end_col_offset}|"
609
+ f"{self.type}|{self.libname}|{self.libtype}"
610
+ )
611
+ self.digest = hashlib.sha224(s.encode("utf-8")).hexdigest()
612
+
613
+ if self.argument_definitions is None:
614
+ self.argument_definitions = self._get_argument_definitions()
615
+
616
+ def __str__(self) -> str:
617
+ return f"{self.name}({', '.join(str(arg) for arg in self.arguments)})"
618
+
619
+ @property
620
+ def matcher(self) -> KeywordMatcher:
621
+ if not hasattr(self, "__matcher"):
622
+ self.__matcher = KeywordMatcher(self.name)
623
+ return self.__matcher
624
+
625
+ @property
626
+ def is_deprecated(self) -> bool:
627
+ return self.deprecated or DEPRECATED_PATTERN.match(self.doc) is not None
628
+
629
+ @property
630
+ def is_resource_keyword(self) -> bool:
631
+ return self.libtype == "RESOURCE"
632
+
633
+ @property
634
+ def is_library_keyword(self) -> bool:
635
+ return self.libtype == "LIBRARY"
636
+
637
+ @property
638
+ def deprecated_message(self) -> str:
639
+ if (m := DEPRECATED_PATTERN.match(self.doc)) is not None:
640
+ return m.group("message").strip()
641
+ return ""
642
+
643
+ @property
644
+ def name_range(self) -> Range:
645
+ if self.name_token is not None:
646
+ return range_from_token(self.name_token)
647
+
648
+ return Range.invalid()
649
+
650
+ @single_call
651
+ def normalized_tags(self) -> List[str]:
652
+ return [normalize(tag) for tag in self.tags]
653
+
654
+ @single_call
655
+ def is_private(self) -> bool:
656
+ if get_robot_version() < (6, 0):
657
+ return False
658
+
659
+ return "robot:private" in self.normalized_tags()
660
+
661
+ @property
662
+ def range(self) -> Range:
663
+ if self.name_token is not None:
664
+ return range_from_token(self.name_token)
665
+
666
+ return Range(
667
+ start=Position(
668
+ line=self.line_no - 1 if self.line_no > 0 else 0,
669
+ character=max(self.col_offset, 0),
670
+ ),
671
+ end=Position(
672
+ line=self.end_line_no - 1 if self.end_line_no >= 0 else max(0, self.line_no),
673
+ character=self.end_col_offset if self.end_col_offset >= 0 else max(self.col_offset, 0),
674
+ ),
675
+ )
676
+
677
+ def to_markdown(
678
+ self,
679
+ add_signature: bool = True,
680
+ header_level: int = 2,
681
+ add_type: bool = True,
682
+ ) -> str:
683
+ result = ""
684
+
685
+ if add_signature:
686
+ result += self._get_signature(header_level, add_type)
687
+
688
+ if self.doc:
689
+ if result:
690
+ result += "\n\n"
691
+
692
+ result += f"##{'#' * header_level} Documentation:\n"
693
+
694
+ if self.doc_format == ROBOT_DOC_FORMAT:
695
+ result += MarkDownFormatter().format(self.doc)
696
+ elif self.doc_format == REST_DOC_FORMAT:
697
+ result += convert_from_rest(self.doc)
698
+ else:
699
+ result += self.doc
700
+
701
+ return result
702
+
703
+ def _get_signature(self, header_level: int, add_type: bool = True) -> str:
704
+ if add_type:
705
+ result = (
706
+ f"#{'#' * header_level} "
707
+ f"{(self.libtype or 'Library').capitalize() if self.is_initializer else 'Keyword'} *{self.name}*\n"
708
+ )
709
+ else:
710
+ if not self.is_initializer:
711
+ result = f"\n\n#{'#' * header_level} {self.name}\n"
712
+ else:
713
+ result = ""
714
+
715
+ if self.arguments:
716
+ result += f"\n##{'#' * header_level} Arguments: \n"
717
+
718
+ result += "\n| | | | |"
719
+ result += "\n|:--|:--|:--|:--|"
720
+
721
+ escaped_pipe = " \\| "
722
+
723
+ for a in self.arguments:
724
+ prefix = ""
725
+ if a.kind in [
726
+ KeywordArgumentKind.NAMED_ONLY_MARKER,
727
+ KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
728
+ ]:
729
+ continue
730
+
731
+ if a.kind == KeywordArgumentKind.VAR_POSITIONAL:
732
+ prefix = "*"
733
+ elif a.kind == KeywordArgumentKind.VAR_NAMED:
734
+ prefix = "**"
735
+ elif a.kind == KeywordArgumentKind.NAMED_ONLY:
736
+ prefix = "🏷"
737
+ elif a.kind == KeywordArgumentKind.POSITIONAL_ONLY:
738
+ prefix = "⟶"
739
+
740
+ def escape_pipe(s: str) -> str:
741
+ return s.replace("|", "\\|")
742
+
743
+ result += (
744
+ f"\n| `{prefix}{a.name!s}`"
745
+ f'| {": " if a.types else " "}'
746
+ f"{escaped_pipe.join(f'`{escape_pipe(s)}`' for s in a.types) if a.types else ''} "
747
+ f"| {'=' if a.default_value is not None else ''} "
748
+ f"| {f'`{a.default_value!s}`' if a.default_value else ''} |"
749
+ )
750
+ if self.return_type:
751
+ if result:
752
+ result += "\n\n"
753
+
754
+ result += f"**Return Type**: `{self.return_type}`\n"
755
+
756
+ if self.tags:
757
+ if result:
758
+ result += "\n\n"
759
+
760
+ result += "**Tags**: \n- "
761
+ result += "\n- ".join(self.tags)
762
+
763
+ return result
764
+
765
+ @property
766
+ def signature(self) -> str:
767
+ return (
768
+ f'({self.type}) "{self.name}": ('
769
+ + ", ".join(
770
+ str(a)
771
+ for a in self.arguments
772
+ if a.kind
773
+ not in [
774
+ KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
775
+ KeywordArgumentKind.NAMED_ONLY_MARKER,
776
+ ]
777
+ )
778
+ + ")"
779
+ )
780
+
781
+ def parameter_signature(self, full_signatures: Optional[Sequence[int]] = None) -> str:
782
+ return (
783
+ "("
784
+ + ", ".join(
785
+ a.signature(full_signatures is None or i in full_signatures)
786
+ for i, a in enumerate(self.arguments)
787
+ if a.kind
788
+ not in [
789
+ KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
790
+ KeywordArgumentKind.NAMED_ONLY_MARKER,
791
+ ]
792
+ )
793
+ + ")"
794
+ )
795
+
796
+ def is_reserved(self) -> bool:
797
+ if get_robot_version() < (7, 0):
798
+ return self.libname == RESERVED_LIBRARY_NAME
799
+
800
+ return False
801
+
802
+ def is_any_run_keyword(self) -> bool:
803
+ return self.libname == BUILTIN_LIBRARY_NAME and self.name in ALL_RUN_KEYWORDS
804
+
805
+ def is_run_keyword(self) -> bool:
806
+ return self.libname == BUILTIN_LIBRARY_NAME and self.name in RUN_KEYWORD_NAMES
807
+
808
+ def is_run_keyword_with_condition(self) -> bool:
809
+ return self.libname == BUILTIN_LIBRARY_NAME and self.name in RUN_KEYWORD_WITH_CONDITION_NAMES
810
+
811
+ def run_keyword_condition_count(self) -> int:
812
+ return (
813
+ RUN_KEYWORD_WITH_CONDITION_NAMES[self.name]
814
+ if self.libname == BUILTIN_LIBRARY_NAME and self.name in RUN_KEYWORD_WITH_CONDITION_NAMES
815
+ else 0
816
+ )
817
+
818
+ def is_run_keyword_if(self) -> bool:
819
+ return self.libname == BUILTIN_LIBRARY_NAME and self.name == RUN_KEYWORD_IF_NAME
820
+
821
+ def is_run_keywords(self) -> bool:
822
+ return self.libname == BUILTIN_LIBRARY_NAME and self.name == RUN_KEYWORDS_NAME
823
+
824
+ @single_call
825
+ def __hash__(self) -> int:
826
+ return hash(
827
+ (
828
+ self.name,
829
+ self.longname,
830
+ self.source,
831
+ self.line_no,
832
+ self.col_offset,
833
+ self.end_line_no,
834
+ self.end_col_offset,
835
+ self.type,
836
+ self.libname,
837
+ self.libtype,
838
+ self.is_embedded,
839
+ self.is_initializer,
840
+ self.is_error_handler,
841
+ self.doc_format,
842
+ tuple(self.tags),
843
+ )
844
+ )
845
+
846
+
847
+ class KeywordError(Exception):
848
+ def __init__(
849
+ self,
850
+ *args: Any,
851
+ multiple_keywords: Optional[List[KeywordDoc]] = None,
852
+ **kwargs: Any,
853
+ ) -> None:
854
+ super().__init__(*args, **kwargs)
855
+ self.multiple_keywords = multiple_keywords
856
+
857
+
858
+ @dataclass
859
+ class KeywordStore:
860
+ source: Optional[str] = None
861
+ source_type: Optional[str] = None
862
+ keywords: List[KeywordDoc] = field(default_factory=list)
863
+
864
+ @property
865
+ def _matchers(self) -> Dict[KeywordMatcher, KeywordDoc]:
866
+ if not hasattr(self, "__matchers"):
867
+ self.__matchers = {v.matcher: v for v in self.keywords}
868
+ return self.__matchers
869
+
870
+ def __getitem__(self, key: str) -> KeywordDoc:
871
+ items = [(k, v) for k, v in self._matchers.items() if k == key]
872
+
873
+ if not items:
874
+ raise KeyError
875
+ if len(items) == 1:
876
+ return items[0][1]
877
+
878
+ if self.source and self.source_type:
879
+ file_info = ""
880
+ if self.source_type == "RESOURCE":
881
+ file_info += f"Resource file '{self.source}'"
882
+ elif self.source_type == "LIBRARY":
883
+ file_info += f"Test library '{self.source}'"
884
+ elif self.source_type == "TESTCASE":
885
+ file_info += "Test case file"
886
+ else:
887
+ file_info += f"File '{self.source}'"
888
+ else:
889
+ file_info = "File"
890
+ error = [f"{file_info} contains multiple keywords matching name '{key}':"]
891
+ names = sorted(k.name for k, v in items)
892
+ raise KeywordError(
893
+ "\n ".join(error + names),
894
+ multiple_keywords=[v for _, v in items],
895
+ )
896
+
897
+ def __contains__(self, __x: object) -> bool:
898
+ return any(k == __x for k in self._matchers.keys())
899
+
900
+ def __len__(self) -> int:
901
+ return len(self.keywords)
902
+
903
+ def __bool__(self) -> bool:
904
+ return len(self) > 0
905
+
906
+ def items(self) -> AbstractSet[Tuple[str, KeywordDoc]]:
907
+ return {(v.name, v) for v in self.keywords}
908
+
909
+ def keys(self) -> AbstractSet[str]:
910
+ return {v.name for v in self.keywords}
911
+
912
+ def values(self) -> AbstractSet[KeywordDoc]:
913
+ return set(self.keywords)
914
+
915
+ def __iter__(self) -> Iterator[KeywordDoc]:
916
+ return self.keywords.__iter__()
917
+
918
+ def get(self, key: str, default: Optional[KeywordDoc] = None) -> Optional[KeywordDoc]:
919
+ try:
920
+ return self.__getitem__(key)
921
+ except KeyError:
922
+ return default
923
+
924
+ def get_all(self, key: str) -> List[KeywordDoc]:
925
+ return [v for k, v in self._matchers.items() if k == key]
926
+
927
+
928
+ @dataclass
929
+ class ModuleSpec:
930
+ name: str
931
+ origin: Optional[str]
932
+ submodule_search_locations: Optional[List[str]]
933
+ member_name: Optional[str]
934
+
935
+
936
+ class LibraryType(Enum):
937
+ CLASS = "CLASS"
938
+ MODULE = "MODULE"
939
+ HYBRID = "HYBRID"
940
+ DYNAMIC = "DYNAMIC"
941
+
942
+
943
+ ROBOT_DEFAULT_SCOPE = "TEST"
944
+
945
+ RE_INLINE_LINK = re.compile(r"([\`])((?:\1|.)+?)\1", re.VERBOSE)
946
+ RE_HEADERS = re.compile(r"^(#{2,9})\s+(\S.*)$", re.MULTILINE)
947
+
948
+
949
+ @dataclass
950
+ class LibraryDoc:
951
+ name: str = ""
952
+ doc: str = field(default="", compare=False)
953
+ version: str = ""
954
+ type: str = "LIBRARY"
955
+ scope: str = ROBOT_DEFAULT_SCOPE
956
+ named_args: bool = True
957
+ doc_format: str = ROBOT_DOC_FORMAT
958
+ source: Optional[str] = None
959
+ line_no: int = -1
960
+ end_line_no: int = -1
961
+ _inits: KeywordStore = field(default_factory=KeywordStore, compare=False)
962
+ _keywords: KeywordStore = field(default_factory=KeywordStore, compare=False)
963
+ types: List[TypeDoc] = field(default_factory=list)
964
+ module_spec: Optional[ModuleSpec] = None
965
+ member_name: Optional[str] = None
966
+ errors: Optional[List[Error]] = field(default=None, compare=False)
967
+ python_path: Optional[List[str]] = None
968
+ stdout: Optional[str] = field(default=None, compare=False)
969
+ has_listener: Optional[bool] = None
970
+ library_type: Optional[LibraryType] = None
971
+
972
+ digest: Optional[str] = field(init=False)
973
+
974
+ @property
975
+ def inits(self) -> KeywordStore:
976
+ return self._inits
977
+
978
+ @inits.setter
979
+ def inits(self, value: KeywordStore) -> None:
980
+ self._inits = value
981
+ self._update_keywords(self._inits)
982
+
983
+ @property
984
+ def keywords(self) -> KeywordStore:
985
+ return self._keywords
986
+
987
+ @keywords.setter
988
+ def keywords(self, value: KeywordStore) -> None:
989
+ self._keywords = value
990
+ self._update_keywords(self._keywords)
991
+
992
+ def _update_keywords(self, keywords: Optional[Iterable[KeywordDoc]]) -> None:
993
+ if not keywords:
994
+ return
995
+
996
+ for k in keywords:
997
+ k.parent = self
998
+ k.parent_digest = self.digest
999
+
1000
+ def __post_init__(self) -> None:
1001
+ s = (
1002
+ f"{self.name}|{self.source}|{self.line_no}|"
1003
+ f"{self.end_line_no}|{self.version}|"
1004
+ f"{self.type}|{self.scope}|{self.doc_format}"
1005
+ )
1006
+ self.digest = hashlib.sha224(s.encode("utf-8")).hexdigest()
1007
+
1008
+ self._update_keywords(self._inits)
1009
+ self._update_keywords(self._keywords)
1010
+
1011
+ @single_call
1012
+ def __hash__(self) -> int:
1013
+ return hash(
1014
+ (
1015
+ self.name,
1016
+ self.source,
1017
+ self.line_no,
1018
+ self.end_line_no,
1019
+ self.version,
1020
+ self.type,
1021
+ self.scope,
1022
+ self.doc_format,
1023
+ )
1024
+ )
1025
+
1026
+ def get_types(self, type_names: Optional[List[str]]) -> List[TypeDoc]:
1027
+ def alias(s: str) -> str:
1028
+ if s == "boolean":
1029
+ return "bool"
1030
+ return s
1031
+
1032
+ if not type_names:
1033
+ return []
1034
+
1035
+ return [t for t in self.types if alias(t.name) in type_names]
1036
+
1037
+ @property
1038
+ def is_deprecated(self) -> bool:
1039
+ return DEPRECATED_PATTERN.match(self.doc) is not None
1040
+
1041
+ @property
1042
+ def deprecated_message(self) -> str:
1043
+ if (m := DEPRECATED_PATTERN.match(self.doc)) is not None:
1044
+ return m.group("message").strip()
1045
+ return ""
1046
+
1047
+ @property
1048
+ def range(self) -> Range:
1049
+ return Range(
1050
+ start=Position(line=self.line_no - 1 if self.line_no >= 0 else 0, character=0),
1051
+ end=Position(
1052
+ line=self.end_line_no - 1 if self.end_line_no >= 0 else max(self.line_no, 0),
1053
+ character=0,
1054
+ ),
1055
+ )
1056
+
1057
+ def to_markdown(
1058
+ self,
1059
+ add_signature: bool = True,
1060
+ only_doc: bool = True,
1061
+ header_level: int = 2,
1062
+ ) -> str:
1063
+ with io.StringIO(newline="\n") as result:
1064
+
1065
+ def write_lines(*args: str) -> None:
1066
+ result.writelines(i + "\n" for i in args)
1067
+
1068
+ if add_signature and any(v for v in self.inits.values() if v.arguments):
1069
+ for i in self.inits.values():
1070
+ write_lines(i.to_markdown(header_level=header_level), "", "---")
1071
+
1072
+ write_lines(
1073
+ f"#{'#' * header_level} {(self.type.capitalize()) if self.type else 'Unknown'} *{self.name}*",
1074
+ "",
1075
+ )
1076
+
1077
+ if self.version or self.scope:
1078
+ write_lines("| | |", "| :--- | :--- |")
1079
+
1080
+ if self.version:
1081
+ write_lines(f"| **Library Version:** | {self.version} |")
1082
+ if self.scope:
1083
+ write_lines(f"| **Library Scope:** | {self.scope} |")
1084
+
1085
+ write_lines("", "")
1086
+
1087
+ if self.doc:
1088
+ write_lines(f"##{'#' * header_level} Introduction", "")
1089
+
1090
+ if self.doc_format == ROBOT_DOC_FORMAT:
1091
+ doc = MarkDownFormatter().format(self.doc)
1092
+
1093
+ if "%TOC%" in doc:
1094
+ doc = self._add_toc(doc, only_doc)
1095
+
1096
+ result.write(doc)
1097
+
1098
+ elif self.doc_format == REST_DOC_FORMAT:
1099
+ result.write(convert_from_rest(self.doc))
1100
+ else:
1101
+ result.write(self.doc)
1102
+
1103
+ if not only_doc:
1104
+ result.write(self._get_doc_for_keywords(header_level=header_level))
1105
+
1106
+ return self._link_inline_links(result.getvalue())
1107
+
1108
+ @property
1109
+ def source_or_origin(self) -> Optional[str]:
1110
+ if self.source is not None:
1111
+ return self.source
1112
+ if self.module_spec is not None:
1113
+ if self.module_spec.origin is not None:
1114
+ return self.module_spec.origin
1115
+
1116
+ if self.module_spec.submodule_search_locations:
1117
+ for e in self.module_spec.submodule_search_locations:
1118
+ p = Path(e, "__init__.py")
1119
+ if p.exists():
1120
+ return str(p)
1121
+
1122
+ return None
1123
+
1124
+ def _link_inline_links(self, text: str) -> str:
1125
+ headers = [v.group(2) for v in RE_HEADERS.finditer(text)]
1126
+
1127
+ def repl(m: re.Match) -> str: # type: ignore
1128
+ if m.group(2) in headers:
1129
+ return f"[{m.group(2)!s}](\\#{str(m.group(2)).lower().replace(' ', '-')})"
1130
+ return str(m.group(0))
1131
+
1132
+ return str(RE_INLINE_LINK.sub(repl, text))
1133
+
1134
+ def _get_doc_for_keywords(self, header_level: int = 2) -> str:
1135
+ result = ""
1136
+ if any(v for v in self.inits.values() if v.arguments):
1137
+ result += "\n---\n\n"
1138
+ result += f"\n##{'#' * header_level} Importing\n\n"
1139
+
1140
+ first = True
1141
+
1142
+ for kw in self.inits.values():
1143
+ if not first:
1144
+ result += "\n---\n"
1145
+ first = False
1146
+
1147
+ result += "\n" + kw.to_markdown(add_type=False)
1148
+
1149
+ if self.keywords:
1150
+ result += "\n---\n\n"
1151
+ result += f"\n##{'#' * header_level} Keywords\n\n"
1152
+
1153
+ first = True
1154
+
1155
+ for kw in self.keywords.values():
1156
+ if not first:
1157
+ result += "\n---\n"
1158
+ first = False
1159
+
1160
+ result += "\n" + kw.to_markdown(header_level=header_level, add_type=False)
1161
+ return result
1162
+
1163
+ def _add_toc(self, doc: str, only_doc: bool = True) -> str:
1164
+ toc = self._create_toc(doc, only_doc)
1165
+ return "\n".join(line if line.strip() != "%TOC%" else toc for line in doc.splitlines())
1166
+
1167
+ def _create_toc(self, doc: str, only_doc: bool = True) -> str:
1168
+ entries = re.findall(r"^##\s+(.+)", doc, flags=re.MULTILINE)
1169
+
1170
+ if not only_doc:
1171
+ if any(v for v in self.inits.values() if v.arguments):
1172
+ entries.append("Importing")
1173
+ if self.keywords:
1174
+ entries.append("Keywords")
1175
+ # TODO if self.data_types:
1176
+ # entries.append("Data types")
1177
+
1178
+ return "\n".join(f"- [{entry}](#{entry.lower().replace(' ', '-')})" for entry in entries)
1179
+
1180
+
1181
+ def var_repr(value: Any) -> str:
1182
+ if value is None:
1183
+ return ""
1184
+
1185
+ if isinstance(value, NativeValue):
1186
+ value = value.value
1187
+
1188
+ if value is None:
1189
+ return "${None}"
1190
+
1191
+ if isinstance(value, (int, float, bool)):
1192
+ return f"${{{value!r}}}"
1193
+
1194
+ if isinstance(value, str) and value == "":
1195
+ return "${EMPTY}"
1196
+
1197
+ if isinstance(value, str) and value == " ":
1198
+ return "${SPACE}"
1199
+
1200
+ if isinstance(value, str):
1201
+ return value
1202
+
1203
+ return "${{ " + repr(value) + " }}"
1204
+
1205
+
1206
+ @dataclass
1207
+ class VariablesDoc(LibraryDoc):
1208
+ type: str = "VARIABLES"
1209
+ scope: str = "GLOBAL"
1210
+
1211
+ variables: List[ImportedVariableDefinition] = field(default_factory=list)
1212
+
1213
+ def to_markdown(
1214
+ self,
1215
+ add_signature: bool = True,
1216
+ only_doc: bool = True,
1217
+ header_level: int = 2,
1218
+ ) -> str:
1219
+ result = super().to_markdown(add_signature, only_doc, header_level)
1220
+
1221
+ if self.variables:
1222
+ result += "\n---\n\n"
1223
+
1224
+ result += "\n```robotframework"
1225
+ result += "\n*** Variables ***"
1226
+
1227
+ for var in self.variables:
1228
+ result += "\n" + var.name
1229
+ if var.has_value:
1230
+ result += " " + var_repr(var.value)
1231
+ else:
1232
+ result += " ..."
1233
+ result += "\n```"
1234
+
1235
+ return result
1236
+
1237
+
1238
+ def is_library_by_path(path: str) -> bool:
1239
+ return path.lower().endswith((".py", "/", os.sep))
1240
+
1241
+
1242
+ def is_variables_by_path(path: str) -> bool:
1243
+ if get_robot_version() >= (6, 1):
1244
+ return path.lower().endswith((".py", ".yml", ".yaml", ".json", "/", os.sep))
1245
+ return path.lower().endswith((".py", ".yml", ".yaml", "/", os.sep))
1246
+
1247
+
1248
+ def _update_env(working_dir: str = ".") -> None:
1249
+ os.chdir(Path(working_dir))
1250
+
1251
+
1252
+ def get_module_spec(module_name: str) -> Optional[ModuleSpec]:
1253
+ import importlib.util
1254
+
1255
+ result = None
1256
+ member_name: Optional[str] = None
1257
+ while result is None:
1258
+ try:
1259
+ result = importlib.util.find_spec(module_name)
1260
+ except (SystemExit, KeyboardInterrupt):
1261
+ raise
1262
+ except BaseException:
1263
+ pass
1264
+ if result is None:
1265
+ splitted = module_name.rsplit(".", 1)
1266
+ if len(splitted) <= 1:
1267
+ break
1268
+ module_name, m = splitted
1269
+ if m:
1270
+ member_name = m + "." + member_name if m and member_name is not None else m
1271
+
1272
+ if result is not None:
1273
+ return ModuleSpec( # type: ignore
1274
+ name=result.name,
1275
+ origin=result.origin,
1276
+ submodule_search_locations=list(result.submodule_search_locations)
1277
+ if result.submodule_search_locations
1278
+ else None,
1279
+ member_name=member_name,
1280
+ )
1281
+ return None
1282
+
1283
+
1284
+ class KeywordWrapper:
1285
+ def __init__(self, kw: Any, source: str) -> None:
1286
+ self.kw = kw
1287
+ self.lib_source = source
1288
+
1289
+ @property
1290
+ def name(self) -> Any:
1291
+ return self.kw.name
1292
+
1293
+ @property
1294
+ def arguments(self) -> Any:
1295
+ return self.kw.arguments
1296
+
1297
+ @property
1298
+ def doc(self) -> Any:
1299
+ try:
1300
+ return self.kw.doc
1301
+ except (SystemExit, KeyboardInterrupt):
1302
+ raise
1303
+ except BaseException:
1304
+ return ""
1305
+
1306
+ @property
1307
+ def tags(self) -> Any:
1308
+ try:
1309
+ return self.kw.tags
1310
+ except (SystemExit, KeyboardInterrupt):
1311
+ raise
1312
+ except BaseException:
1313
+ return []
1314
+
1315
+ @property
1316
+ def source(self) -> Any:
1317
+ try:
1318
+ return str(self.kw.source) if self.kw.source is not None else self.source
1319
+ except (SystemExit, KeyboardInterrupt):
1320
+ raise
1321
+ except BaseException:
1322
+ return str(self.lib_source) if self.lib_source is not None else self.lib_source
1323
+
1324
+ @property
1325
+ def lineno(self) -> Any:
1326
+ try:
1327
+ return self.kw.lineno
1328
+ except (SystemExit, KeyboardInterrupt):
1329
+ raise
1330
+ except BaseException:
1331
+ return 0
1332
+
1333
+ @property
1334
+ def libname(self) -> Any:
1335
+ try:
1336
+ return self.kw.libname
1337
+ except (SystemExit, KeyboardInterrupt):
1338
+ raise
1339
+ except BaseException:
1340
+ return ""
1341
+
1342
+ @property
1343
+ def longname(self) -> Any:
1344
+ try:
1345
+ return self.kw.longname
1346
+ except (SystemExit, KeyboardInterrupt):
1347
+ raise
1348
+ except BaseException:
1349
+ return ""
1350
+
1351
+ @property
1352
+ def is_error_handler(self) -> bool:
1353
+ if get_robot_version() < (7, 0):
1354
+ from robot.running.usererrorhandler import UserErrorHandler
1355
+
1356
+ return isinstance(self.kw, UserErrorHandler)
1357
+
1358
+ from robot.running.invalidkeyword import InvalidKeyword
1359
+
1360
+ return isinstance(self.kw, InvalidKeyword)
1361
+
1362
+ @property
1363
+ def error_handler_message(self) -> Optional[str]:
1364
+ if self.is_error_handler:
1365
+ return str(self.kw.error)
1366
+
1367
+ return None
1368
+
1369
+ @property
1370
+ def error(self) -> Any:
1371
+ return self.kw.error
1372
+
1373
+ @property
1374
+ def args(self) -> Any:
1375
+ try:
1376
+ return self.kw.args
1377
+ except (SystemExit, KeyboardInterrupt):
1378
+ raise
1379
+ except BaseException:
1380
+ return None
1381
+
1382
+
1383
+ class MessageAndTraceback(NamedTuple):
1384
+ message: str
1385
+ traceback: List[SourceAndLineInfo]
1386
+
1387
+
1388
+ __RE_MESSAGE = re.compile(r"^Traceback.*$", re.MULTILINE)
1389
+ __RE_TRACEBACK = re.compile(r'^ +File +"(.*)", +line +(\d+).*$', re.MULTILINE)
1390
+
1391
+
1392
+ def get_message_and_traceback_from_exception_text(
1393
+ text: str,
1394
+ ) -> MessageAndTraceback:
1395
+ splitted = __RE_MESSAGE.split(text, 1)
1396
+
1397
+ return MessageAndTraceback(
1398
+ message=splitted[0].strip(),
1399
+ traceback=[SourceAndLineInfo(t.group(1), int(t.group(2))) for t in __RE_TRACEBACK.finditer(splitted[1])]
1400
+ if len(splitted) > 1
1401
+ else [],
1402
+ )
1403
+
1404
+
1405
+ def error_from_exception(
1406
+ ex: BaseException,
1407
+ default_source: Optional[str],
1408
+ default_line_no: Optional[int],
1409
+ ) -> Error:
1410
+ message_and_traceback = get_message_and_traceback_from_exception_text(str(ex))
1411
+ if message_and_traceback.traceback:
1412
+ tr = message_and_traceback.traceback[-1]
1413
+ return Error(
1414
+ message=str(ex),
1415
+ type_name=type(ex).__qualname__,
1416
+ source=tr.source,
1417
+ line_no=tr.line_no,
1418
+ message_traceback=message_and_traceback.traceback,
1419
+ )
1420
+
1421
+ exc_type, exc_value, tb_type = sys.exc_info()
1422
+ txt = "".join(traceback.format_exception(exc_type, exc_value, tb_type))
1423
+ message_and_traceback = get_message_and_traceback_from_exception_text(txt)
1424
+
1425
+ return Error(
1426
+ message=str(ex),
1427
+ type_name=type(ex).__qualname__,
1428
+ source=default_source,
1429
+ line_no=default_line_no,
1430
+ traceback=message_and_traceback.traceback if message_and_traceback else None,
1431
+ )
1432
+
1433
+
1434
+ @dataclass
1435
+ class _Variable:
1436
+ name: str
1437
+ value: Iterable[str]
1438
+ source: Optional[str] = None
1439
+ lineno: Optional[int] = None
1440
+ error: Optional[str] = None
1441
+ separator: Optional[str] = None
1442
+
1443
+ def report_invalid_syntax(self, message: str, level: str = "ERROR") -> None:
1444
+ pass
1445
+
1446
+
1447
+ __default_variables: Any = None
1448
+
1449
+
1450
+ def _get_default_variables() -> Any:
1451
+ from robot.variables import Variables
1452
+
1453
+ global __default_variables
1454
+ if __default_variables is None:
1455
+ __default_variables = Variables()
1456
+ for k, v in {
1457
+ "${TEMPDIR}": str(Path(tempfile.gettempdir()).resolve()),
1458
+ "${/}": os.sep,
1459
+ "${:}": os.pathsep,
1460
+ "${\\n}": os.linesep,
1461
+ "${SPACE}": " ",
1462
+ "${True}": True,
1463
+ "${False}": False,
1464
+ "${None}": None,
1465
+ "${null}": None,
1466
+ "${TEST NAME}": "",
1467
+ "@{TEST TAGS}": [],
1468
+ "${TEST DOCUMENTATION}": "",
1469
+ "${TEST STATUS}": "",
1470
+ "${TEST MESSAGE}": "",
1471
+ "${PREV TEST NAME}": "",
1472
+ "${PREV TEST STATUS}": "",
1473
+ "${PREV TEST MESSAGE}": "",
1474
+ "${SUITE NAME}": "",
1475
+ "${SUITE SOURCE}": "",
1476
+ "${SUITE DOCUMENTATION}": "",
1477
+ "&{SUITE METADATA}": {},
1478
+ "${SUITE STATUS}": "",
1479
+ "${SUITE MESSAGE}": "",
1480
+ "${KEYWORD STATUS}": "",
1481
+ "${KEYWORD MESSAGE}": "",
1482
+ "${LOG LEVEL}": "",
1483
+ "${OUTPUT FILE}": "",
1484
+ "${LOG FILE}": "",
1485
+ "${REPORT FILE}": "",
1486
+ "${DEBUG FILE}": "",
1487
+ "${OUTPUT DIR}": "",
1488
+ "${OPTIONS}": "",
1489
+ }.items():
1490
+ __default_variables[k] = v
1491
+
1492
+ return __default_variables
1493
+
1494
+
1495
+ def resolve_robot_variables(
1496
+ working_dir: str = ".",
1497
+ base_dir: str = ".",
1498
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1499
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1500
+ ) -> Any:
1501
+ from robot.variables import Variables
1502
+
1503
+ result: Variables = _get_default_variables().copy()
1504
+
1505
+ for k, v in {
1506
+ "${CURDIR}": str(Path(base_dir).absolute()),
1507
+ "${EXECDIR}": str(Path(working_dir).absolute()),
1508
+ }.items():
1509
+ result[k] = v
1510
+
1511
+ if command_line_variables:
1512
+ for k1, v1 in command_line_variables.items():
1513
+ result[k1] = v1
1514
+
1515
+ if variables is not None:
1516
+ vars = [_Variable(k, v) for k, v in variables.items() if v is not None and not isinstance(v, NativeValue)]
1517
+ if get_robot_version() < (7, 0):
1518
+ result.set_from_variable_table(vars)
1519
+ else:
1520
+ result.set_from_variable_section(vars)
1521
+
1522
+ for k2, v2 in variables.items():
1523
+ if isinstance(v2, NativeValue):
1524
+ result[k2] = v2.value
1525
+
1526
+ result.resolve_delayed()
1527
+
1528
+ return result
1529
+
1530
+
1531
+ def resolve_variable(
1532
+ name: str,
1533
+ working_dir: str = ".",
1534
+ base_dir: str = ".",
1535
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1536
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1537
+ ) -> Any:
1538
+ from robot.variables.finders import VariableFinder
1539
+
1540
+ _update_env(working_dir)
1541
+
1542
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
1543
+ if get_robot_version() >= (6, 1):
1544
+ return VariableFinder(robot_variables).find(name.replace("\\", "\\\\"))
1545
+
1546
+ return VariableFinder(robot_variables.store).find(name.replace("\\", "\\\\"))
1547
+
1548
+
1549
+ @contextmanager
1550
+ def _std_capture() -> Iterator[io.StringIO]:
1551
+ old__stdout__ = sys.__stdout__
1552
+ old__stderr__ = sys.__stderr__
1553
+ old_stdout = sys.stdout
1554
+ old_stderr = sys.stderr
1555
+
1556
+ capturer = sys.stdout = sys.__stdout__ = sys.stderr = sys.__stderr__ = io.StringIO() # type: ignore
1557
+
1558
+ try:
1559
+ yield capturer
1560
+ finally:
1561
+ sys.stderr = old_stderr
1562
+ sys.stdout = old_stdout
1563
+ sys.__stderr__ = old__stderr__ # type: ignore
1564
+ sys.__stdout__ = old__stdout__ # type: ignore
1565
+
1566
+
1567
+ class IgnoreEasterEggLibraryWarning(Exception): # noqa: N818
1568
+ pass
1569
+
1570
+
1571
+ def _find_library_internal(
1572
+ name: str,
1573
+ working_dir: str = ".",
1574
+ base_dir: str = ".",
1575
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1576
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1577
+ ) -> Tuple[str, Any]:
1578
+ from robot.errors import DataError
1579
+ from robot.libraries import STDLIBS
1580
+ from robot.utils.robotpath import find_file as robot_find_file
1581
+
1582
+ _update_env(working_dir)
1583
+
1584
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
1585
+
1586
+ try:
1587
+ name = robot_variables.replace_string(name, ignore_errors=False)
1588
+ except DataError as error:
1589
+ raise DataError(f"Replacing variables from setting 'Library' failed: {error}")
1590
+
1591
+ if name in STDLIBS:
1592
+ result = ROBOT_LIBRARY_PACKAGE + "." + name
1593
+ else:
1594
+ result = name
1595
+
1596
+ if is_library_by_path(result):
1597
+ result = robot_find_file(result, base_dir or ".", "Library")
1598
+
1599
+ return (result, robot_variables)
1600
+
1601
+
1602
+ def find_library(
1603
+ name: str,
1604
+ working_dir: str = ".",
1605
+ base_dir: str = ".",
1606
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1607
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1608
+ ) -> str:
1609
+ return _find_library_internal(name, working_dir, base_dir, command_line_variables, variables)[0]
1610
+
1611
+
1612
+ def get_robot_library_html_doc_str(
1613
+ name: str,
1614
+ args: Optional[str],
1615
+ working_dir: str = ".",
1616
+ base_dir: str = ".",
1617
+ theme: Optional[str] = None,
1618
+ ) -> str:
1619
+ from robot.libdocpkg import LibraryDocumentation
1620
+ from robot.libdocpkg.htmlwriter import LibdocHtmlWriter
1621
+
1622
+ _update_env(working_dir)
1623
+
1624
+ if Path(name).suffix.lower() in ALLOWED_RESOURCE_FILE_EXTENSIONS:
1625
+ name = find_file(name, working_dir, base_dir)
1626
+ elif Path(name).suffix.lower() in ALLOWED_LIBRARY_FILE_EXTENSIONS:
1627
+ name = find_file(name, working_dir, base_dir, file_type="Library")
1628
+
1629
+ robot_libdoc = LibraryDocumentation(name + ("::" + args if args else ""))
1630
+ robot_libdoc.convert_docs_to_html()
1631
+ with io.StringIO() as output:
1632
+ if get_robot_version() > (6, 0):
1633
+ writer = LibdocHtmlWriter(theme=theme)
1634
+ else:
1635
+ writer = LibdocHtmlWriter()
1636
+ writer.write(robot_libdoc, output)
1637
+
1638
+ return output.getvalue()
1639
+
1640
+
1641
+ def get_library_doc(
1642
+ name: str,
1643
+ args: Optional[Tuple[Any, ...]] = None,
1644
+ working_dir: str = ".",
1645
+ base_dir: str = ".",
1646
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
1647
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1648
+ ) -> LibraryDoc:
1649
+ import robot.running.testlibraries
1650
+ from robot.libdocpkg.robotbuilder import KeywordDocBuilder
1651
+ from robot.libraries import STDLIBS
1652
+ from robot.output import LOGGER
1653
+ from robot.output.loggerhelper import AbstractLogger
1654
+ from robot.running.outputcapture import OutputCapturer
1655
+ from robot.running.runkwregister import RUN_KW_REGISTER
1656
+ from robot.utils import Importer
1657
+
1658
+ class Logger(AbstractLogger):
1659
+ def __init__(self) -> None:
1660
+ super().__init__()
1661
+ self.messages: List[Tuple[str, str, bool]] = []
1662
+
1663
+ def write(self, message: str, level: str, html: bool = False) -> None:
1664
+ self.messages.append((message, level, html))
1665
+
1666
+ def import_test_library(name: str) -> Union[Any, Tuple[Any, str]]:
1667
+ with OutputCapturer(library_import=True):
1668
+ importer = Importer("test library", LOGGER)
1669
+ return importer.import_class_or_module(name, return_source=True)
1670
+
1671
+ def get_test_library(
1672
+ libcode: Any,
1673
+ source: str,
1674
+ name: str,
1675
+ args: Optional[Tuple[Any, ...]] = None,
1676
+ variables: Optional[Dict[str, Optional[Any]]] = None,
1677
+ create_handlers: bool = True,
1678
+ logger: Any = LOGGER,
1679
+ ) -> Any:
1680
+ if get_robot_version() < (7, 0):
1681
+ libclass = robot.running.testlibraries._get_lib_class(libcode)
1682
+ lib = libclass(libcode, name, args or [], source, logger, variables)
1683
+ if create_handlers:
1684
+ lib.create_handlers()
1685
+ else:
1686
+ lib = robot.running.testlibraries.TestLibrary.from_code(
1687
+ libcode,
1688
+ name,
1689
+ source=Path(source),
1690
+ args=args or [],
1691
+ variables=variables,
1692
+ create_keywords=create_handlers,
1693
+ logger=logger,
1694
+ )
1695
+
1696
+ return lib
1697
+
1698
+ with _std_capture() as std_capturer:
1699
+ import_name, robot_variables = _find_library_internal(
1700
+ name,
1701
+ working_dir=working_dir,
1702
+ base_dir=base_dir,
1703
+ command_line_variables=command_line_variables,
1704
+ variables=variables,
1705
+ )
1706
+
1707
+ module_spec: Optional[ModuleSpec] = None
1708
+ if not is_library_by_path(import_name):
1709
+ module_spec = get_module_spec(import_name)
1710
+
1711
+ # skip antigravity easter egg
1712
+ # see https://python-history.blogspot.com/2010/06/import-antigravity.html
1713
+ if import_name.lower() == "antigravity" or import_name.lower().endswith("antigravity.py"):
1714
+ raise IgnoreEasterEggLibraryWarning(f"Ignoring import for python easter egg '{import_name}'.")
1715
+
1716
+ errors: List[Error] = []
1717
+
1718
+ source = None
1719
+ try:
1720
+ libcode, source = import_test_library(import_name)
1721
+ except (SystemExit, KeyboardInterrupt):
1722
+ raise
1723
+ except BaseException as e:
1724
+ return LibraryDoc(
1725
+ name=name,
1726
+ source=source or module_spec.origin
1727
+ if module_spec is not None and module_spec.origin
1728
+ else import_name
1729
+ if is_library_by_path(import_name)
1730
+ else None,
1731
+ module_spec=module_spec,
1732
+ errors=[
1733
+ error_from_exception(
1734
+ e,
1735
+ source or module_spec.origin
1736
+ if module_spec is not None and module_spec.origin
1737
+ else import_name
1738
+ if is_library_by_path(import_name)
1739
+ else None,
1740
+ 1 if source is not None or module_spec is not None and module_spec.origin is not None else None,
1741
+ )
1742
+ ],
1743
+ python_path=sys.path,
1744
+ )
1745
+
1746
+ if name in STDLIBS and import_name.startswith(ROBOT_LIBRARY_PACKAGE + "."):
1747
+ library_name = name
1748
+ else:
1749
+ if import_name.startswith(ROBOT_LIBRARY_PACKAGE + "."):
1750
+ library_name = import_name[len(ROBOT_LIBRARY_PACKAGE + ".") :]
1751
+ else:
1752
+ library_name = import_name
1753
+
1754
+ library_name_path = Path(import_name)
1755
+ if library_name_path.exists():
1756
+ library_name = library_name_path.stem
1757
+
1758
+ lib = None
1759
+ try:
1760
+ lib = get_test_library(
1761
+ libcode,
1762
+ source,
1763
+ library_name,
1764
+ args,
1765
+ create_handlers=False,
1766
+ variables=robot_variables,
1767
+ )
1768
+ if get_robot_version() < (7, 0):
1769
+ _ = lib.get_instance()
1770
+ else:
1771
+ _ = lib.instance
1772
+ except (SystemExit, KeyboardInterrupt):
1773
+ raise
1774
+ except BaseException as e:
1775
+ lib = None
1776
+ errors.append(
1777
+ error_from_exception(
1778
+ e,
1779
+ source or module_spec.origin if module_spec is not None else None,
1780
+ 1 if source is not None or module_spec is not None and module_spec.origin is not None else None,
1781
+ )
1782
+ )
1783
+
1784
+ if args:
1785
+ try:
1786
+ lib = get_test_library(libcode, source, library_name, (), create_handlers=False)
1787
+ if get_robot_version() < (7, 0):
1788
+ _ = lib.get_instance()
1789
+ else:
1790
+ _ = lib.instance
1791
+ except (SystemExit, KeyboardInterrupt):
1792
+ raise
1793
+ except BaseException:
1794
+ lib = None
1795
+
1796
+ real_source = str(lib.source) if lib is not None else source
1797
+
1798
+ libdoc = LibraryDoc(
1799
+ name=library_name,
1800
+ source=real_source,
1801
+ module_spec=module_spec
1802
+ if module_spec is not None
1803
+ and module_spec.origin != real_source
1804
+ and module_spec.submodule_search_locations is None
1805
+ else None,
1806
+ python_path=sys.path,
1807
+ line_no=lib.lineno if lib is not None else -1,
1808
+ doc=str(lib.doc) if lib is not None else "",
1809
+ version=str(lib.version) if lib is not None else "",
1810
+ scope=str(lib.scope) if lib is not None else ROBOT_DEFAULT_SCOPE,
1811
+ doc_format=(str(lib.doc_format) or ROBOT_DOC_FORMAT) if lib is not None else ROBOT_DOC_FORMAT,
1812
+ member_name=module_spec.member_name if module_spec is not None else None,
1813
+ )
1814
+
1815
+ if lib is not None:
1816
+ try:
1817
+ if get_robot_version() < (7, 0):
1818
+ libdoc.has_listener = lib.has_listener
1819
+
1820
+ if isinstance(lib, robot.running.testlibraries._ModuleLibrary):
1821
+ libdoc.library_type = LibraryType.MODULE
1822
+ elif isinstance(lib, robot.running.testlibraries._ClassLibrary):
1823
+ libdoc.library_type = LibraryType.CLASS
1824
+ elif isinstance(lib, robot.running.testlibraries._DynamicLibrary):
1825
+ libdoc.library_type = LibraryType.DYNAMIC
1826
+ elif isinstance(lib, robot.running.testlibraries._HybridLibrary):
1827
+ libdoc.library_type = LibraryType.HYBRID
1828
+ else:
1829
+ libdoc.has_listener = bool(lib.listeners)
1830
+
1831
+ if isinstance(lib, robot.running.testlibraries.ModuleLibrary):
1832
+ libdoc.library_type = LibraryType.MODULE
1833
+ elif isinstance(lib, robot.running.testlibraries.ClassLibrary):
1834
+ libdoc.library_type = LibraryType.CLASS
1835
+ elif isinstance(lib, robot.running.testlibraries.DynamicLibrary):
1836
+ libdoc.library_type = LibraryType.DYNAMIC
1837
+ elif isinstance(lib, robot.running.testlibraries.HybridLibrary):
1838
+ libdoc.library_type = LibraryType.HYBRID
1839
+
1840
+ init_wrappers = [KeywordWrapper(lib.init, source)]
1841
+ init_keywords = [(KeywordDocBuilder().build_keyword(k), k) for k in init_wrappers]
1842
+ libdoc.inits = KeywordStore(
1843
+ keywords=[
1844
+ KeywordDoc(
1845
+ name=libdoc.name,
1846
+ arguments=[ArgumentInfo.from_robot(a) for a in kw[0].args],
1847
+ doc=kw[0].doc,
1848
+ tags=list(kw[0].tags),
1849
+ source=kw[0].source,
1850
+ line_no=kw[0].lineno if kw[0].lineno is not None else -1,
1851
+ col_offset=-1,
1852
+ end_col_offset=-1,
1853
+ end_line_no=-1,
1854
+ type="library",
1855
+ libname=libdoc.name,
1856
+ libtype=libdoc.type,
1857
+ longname=f"{libdoc.name}.{kw[0].name}",
1858
+ doc_format=str(lib.doc_format) or ROBOT_DOC_FORMAT,
1859
+ is_initializer=True,
1860
+ arguments_spec=ArgumentSpec.from_robot_argument_spec(
1861
+ kw[1].arguments if get_robot_version() < (7, 0) else kw[1].args
1862
+ ),
1863
+ )
1864
+ for kw in init_keywords
1865
+ ]
1866
+ )
1867
+
1868
+ logger = Logger()
1869
+ lib.logger = logger
1870
+
1871
+ if get_robot_version() < (7, 0):
1872
+ lib.create_handlers()
1873
+ else:
1874
+ lib.create_keywords()
1875
+
1876
+ for m in logger.messages:
1877
+ if m[1] == "ERROR":
1878
+ errors.append(
1879
+ Error(
1880
+ message=m[0],
1881
+ type_name=m[1],
1882
+ source=libdoc.source,
1883
+ line_no=libdoc.line_no,
1884
+ )
1885
+ )
1886
+
1887
+ if get_robot_version() < (7, 0):
1888
+ keyword_wrappers = [KeywordWrapper(k, source) for k in lib.handlers]
1889
+ else:
1890
+ keyword_wrappers = [KeywordWrapper(k, source) for k in lib.keywords]
1891
+
1892
+ def get_args_to_process(libdoc_name: str, kw_name: str) -> Any:
1893
+ result = RUN_KW_REGISTER.get_args_to_process(libdoc_name, kw_name)
1894
+ if result == -1:
1895
+ return None
1896
+
1897
+ return result
1898
+
1899
+ keyword_docs = [(KeywordDocBuilder().build_keyword(k), k) for k in keyword_wrappers]
1900
+ libdoc.keywords = KeywordStore(
1901
+ source=libdoc.name,
1902
+ source_type=libdoc.type,
1903
+ keywords=[
1904
+ KeywordDoc(
1905
+ name=kw[0].name,
1906
+ arguments=[ArgumentInfo.from_robot(a) for a in kw[0].args],
1907
+ doc=kw[0].doc,
1908
+ tags=list(kw[0].tags),
1909
+ source=kw[0].source,
1910
+ line_no=kw[0].lineno if kw[0].lineno is not None else -1,
1911
+ col_offset=-1,
1912
+ end_col_offset=-1,
1913
+ end_line_no=-1,
1914
+ libname=libdoc.name,
1915
+ libtype=libdoc.type,
1916
+ longname=f"{libdoc.name}.{kw[0].name}",
1917
+ is_embedded=is_embedded_keyword(kw[0].name),
1918
+ doc_format=str(lib.doc_format) or ROBOT_DOC_FORMAT,
1919
+ is_error_handler=kw[1].is_error_handler,
1920
+ error_handler_message=kw[1].error_handler_message,
1921
+ is_registered_run_keyword=RUN_KW_REGISTER.is_run_keyword(libdoc.name, kw[0].name),
1922
+ args_to_process=get_args_to_process(libdoc.name, kw[0].name),
1923
+ deprecated=kw[0].deprecated,
1924
+ arguments_spec=ArgumentSpec.from_robot_argument_spec(
1925
+ kw[1].arguments if get_robot_version() < (7, 0) else kw[1].args
1926
+ )
1927
+ if not kw[1].is_error_handler
1928
+ else None,
1929
+ return_type=str(kw[1].args.return_type) if get_robot_version() >= (7, 0) else None,
1930
+ )
1931
+ for kw in keyword_docs
1932
+ ],
1933
+ )
1934
+
1935
+ if get_robot_version() >= (6, 1):
1936
+ from robot.libdocpkg.datatypes import (
1937
+ TypeDoc as RobotTypeDoc,
1938
+ )
1939
+ from robot.running.arguments.argumentspec import TypeInfo
1940
+
1941
+ def _yield_type_info(info: TypeInfo) -> Iterable[TypeInfo]:
1942
+ if not info.is_union:
1943
+ yield info
1944
+ for nested in info.nested:
1945
+ yield from _yield_type_info(nested)
1946
+
1947
+ def _get_type_docs(keywords: List[Any], custom_converters: List[Any]) -> Set[RobotTypeDoc]:
1948
+ type_docs: Dict[RobotTypeDoc, Set[str]] = {}
1949
+ for kw in keywords:
1950
+ for arg in kw.args:
1951
+ kw.type_docs[arg.name] = {}
1952
+ for type_info in _yield_type_info(arg.type):
1953
+ if type_info.type is not None:
1954
+ if get_robot_version() < (7, 0):
1955
+ type_doc = RobotTypeDoc.for_type(
1956
+ type_info.type,
1957
+ custom_converters,
1958
+ )
1959
+ else:
1960
+ type_doc = RobotTypeDoc.for_type(type_info, custom_converters)
1961
+ if type_doc:
1962
+ kw.type_docs[arg.name][type_info.name] = type_doc.name
1963
+ type_docs.setdefault(type_doc, set()).add(kw.name)
1964
+ for type_doc, usages in type_docs.items():
1965
+ type_doc.usages = sorted(usages, key=str.lower)
1966
+ return set(type_docs)
1967
+
1968
+ libdoc.types = [
1969
+ TypeDoc(
1970
+ td.type,
1971
+ td.name,
1972
+ td.doc,
1973
+ td.accepts,
1974
+ td.usages,
1975
+ [EnumMember(m.name, m.value) for m in td.members] if td.members else None,
1976
+ [TypedDictItem(i.key, i.type, i.required) for i in td.items] if td.items else None,
1977
+ libname=libdoc.name,
1978
+ libtype=libdoc.type,
1979
+ doc_format=libdoc.doc_format,
1980
+ )
1981
+ for td in _get_type_docs(
1982
+ [kw[0] for kw in keyword_docs + init_keywords],
1983
+ lib.converters,
1984
+ )
1985
+ ]
1986
+
1987
+ except (SystemExit, KeyboardInterrupt):
1988
+ raise
1989
+ except BaseException as e:
1990
+ errors.append(
1991
+ error_from_exception(
1992
+ e,
1993
+ source or module_spec.origin if module_spec is not None else None,
1994
+ 1 if source is not None or module_spec is not None and module_spec.origin is not None else None,
1995
+ )
1996
+ )
1997
+
1998
+ if errors:
1999
+ libdoc.errors = errors
2000
+
2001
+ libdoc.stdout = std_capturer.getvalue()
2002
+
2003
+ return libdoc
2004
+
2005
+
2006
+ def _find_variables_internal(
2007
+ name: str,
2008
+ working_dir: str = ".",
2009
+ base_dir: str = ".",
2010
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2011
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2012
+ ) -> Tuple[str, Any]:
2013
+ from robot.errors import DataError
2014
+ from robot.utils.robotpath import find_file as robot_find_file
2015
+
2016
+ _update_env(working_dir)
2017
+
2018
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2019
+
2020
+ try:
2021
+ name = robot_variables.replace_string(name, ignore_errors=False)
2022
+ except DataError as error:
2023
+ raise DataError(f"Replacing variables from setting 'Variables' failed: {error}")
2024
+
2025
+ result = name
2026
+
2027
+ if is_variables_by_path(result):
2028
+ result = robot_find_file(result, base_dir or ".", "Variables")
2029
+
2030
+ return (result, robot_variables)
2031
+
2032
+
2033
+ def resolve_args(
2034
+ args: Tuple[Any, ...],
2035
+ working_dir: str = ".",
2036
+ base_dir: str = ".",
2037
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2038
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2039
+ ) -> Tuple[Any, ...]:
2040
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2041
+
2042
+ result = []
2043
+ for arg in args:
2044
+ if isinstance(arg, str):
2045
+ result.append(robot_variables.replace_string(arg, ignore_errors=True))
2046
+ else:
2047
+ result.append(arg)
2048
+
2049
+ return tuple(result)
2050
+
2051
+
2052
+ def find_variables(
2053
+ name: str,
2054
+ working_dir: str = ".",
2055
+ base_dir: str = ".",
2056
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2057
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2058
+ ) -> str:
2059
+ if get_robot_version() >= (5, 0):
2060
+ return _find_variables_internal(name, working_dir, base_dir, command_line_variables, variables)[0]
2061
+
2062
+ return find_file(
2063
+ name,
2064
+ working_dir,
2065
+ base_dir,
2066
+ command_line_variables,
2067
+ variables,
2068
+ file_type="Variables",
2069
+ )
2070
+
2071
+
2072
+ def get_variables_doc(
2073
+ name: str,
2074
+ args: Optional[Tuple[Any, ...]] = None,
2075
+ working_dir: str = ".",
2076
+ base_dir: str = ".",
2077
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2078
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2079
+ ) -> VariablesDoc:
2080
+ from robot.libdocpkg.robotbuilder import KeywordDocBuilder
2081
+ from robot.output import LOGGER
2082
+ from robot.utils.importer import Importer
2083
+ from robot.variables.filesetter import PythonImporter, YamlImporter
2084
+
2085
+ if get_robot_version() >= (6, 1):
2086
+ from robot.variables.filesetter import JsonImporter
2087
+
2088
+ import_name: str = name
2089
+ stem = Path(name).stem
2090
+ module_spec: Optional[ModuleSpec] = None
2091
+ source: Optional[str] = None
2092
+ try:
2093
+ python_import = False
2094
+
2095
+ with _std_capture() as std_capturer:
2096
+ import_name = find_variables(name, working_dir, base_dir, command_line_variables, variables)
2097
+ get_variables = None
2098
+
2099
+ if import_name.lower().endswith((".yaml", ".yml")):
2100
+ source = import_name
2101
+ importer = YamlImporter()
2102
+ elif get_robot_version() >= (6, 1) and import_name.lower().endswith(".json"):
2103
+ source = import_name
2104
+ importer = JsonImporter()
2105
+ else:
2106
+ python_import = True
2107
+
2108
+ if not is_variables_by_path(import_name):
2109
+ module_spec = get_module_spec(import_name)
2110
+
2111
+ # skip antigravity easter egg
2112
+ # see https://python-history.blogspot.com/2010/06/import-antigravity.html
2113
+ if import_name.lower() == "antigravity" or import_name.lower().endswith("antigravity.py"):
2114
+ raise IgnoreEasterEggLibraryWarning(f"Ignoring import for python easter egg '{import_name}'.")
2115
+
2116
+ class MyPythonImporter(PythonImporter):
2117
+ def __init__(self, var_file: Any) -> None:
2118
+ self.var_file = var_file
2119
+
2120
+ def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) -> Any:
2121
+ return self._get_variables(self.var_file, args)
2122
+
2123
+ def is_dynamic(self) -> bool:
2124
+ return hasattr(self.var_file, "get_variables") or hasattr(self.var_file, "getVariables")
2125
+
2126
+ module_importer = Importer("variable file", LOGGER)
2127
+
2128
+ if get_robot_version() >= (5, 0):
2129
+ libcode, source = module_importer.import_class_or_module(
2130
+ import_name,
2131
+ instantiate_with_args=(),
2132
+ return_source=True,
2133
+ )
2134
+ else:
2135
+ source = import_name
2136
+ libcode = module_importer.import_class_or_module_by_path(import_name, instantiate_with_args=())
2137
+
2138
+ importer = MyPythonImporter(libcode)
2139
+
2140
+ if importer.is_dynamic():
2141
+ get_variables = getattr(libcode, "get_variables", None) or getattr(libcode, "getVariables", None)
2142
+
2143
+ libdoc = VariablesDoc(
2144
+ name=stem,
2145
+ source=source or module_spec.origin if module_spec is not None else import_name,
2146
+ module_spec=module_spec,
2147
+ stdout=std_capturer.getvalue(),
2148
+ python_path=sys.path,
2149
+ )
2150
+
2151
+ if python_import:
2152
+ if get_variables is not None:
2153
+ if get_robot_version() >= (7, 0):
2154
+ # TODO: variables initializer for RF7
2155
+ # from robot.running.librarykeyword import StaticKeywordCreator, LibraryInitCreator
2156
+
2157
+ # class MyStaticKeywordCreator(StaticKeywordCreator):
2158
+ # def __init__(self, name: str, library: TestLibrary):
2159
+ # super().__init__(name, library)
2160
+ pass
2161
+ else:
2162
+ from robot.running.handlers import _PythonHandler
2163
+
2164
+ class VarHandler(_PythonHandler):
2165
+ def _get_name(self, handler_name: Any, handler_method: Any) -> Any:
2166
+ return get_variables.__name__ if get_variables is not None else ""
2167
+
2168
+ def _get_initial_handler(self, library: Any, name: Any, method: Any) -> Any:
2169
+ return None
2170
+
2171
+ vars_initializer = VarHandler(libdoc, get_variables.__name__, get_variables)
2172
+
2173
+ libdoc.inits = KeywordStore(
2174
+ keywords=[
2175
+ KeywordDoc(
2176
+ name=libdoc.name,
2177
+ arguments=[ArgumentInfo.from_robot(a) for a in kw[0].args],
2178
+ doc=kw[0].doc,
2179
+ source=kw[0].source,
2180
+ line_no=kw[0].lineno if kw[0].lineno is not None else -1,
2181
+ col_offset=-1,
2182
+ end_col_offset=-1,
2183
+ end_line_no=-1,
2184
+ type="variables",
2185
+ libname=libdoc.name,
2186
+ libtype=libdoc.type,
2187
+ longname=f"{libdoc.name}.{kw[0].name}",
2188
+ is_initializer=True,
2189
+ arguments_spec=ArgumentSpec.from_robot_argument_spec(kw[1].arguments),
2190
+ )
2191
+ for kw in [
2192
+ (KeywordDocBuilder().build_keyword(k), k)
2193
+ for k in [
2194
+ KeywordWrapper(
2195
+ vars_initializer,
2196
+ libdoc.source or "",
2197
+ )
2198
+ ]
2199
+ ]
2200
+ ]
2201
+ )
2202
+ else:
2203
+ if get_robot_version() >= (7, 0):
2204
+ # TODO: variables initializer for RF7
2205
+
2206
+ pass
2207
+ else:
2208
+ from robot.running.handlers import _PythonInitHandler
2209
+
2210
+ get_variables = getattr(libcode, "__init__", None) or getattr(libcode, "__init__", None)
2211
+
2212
+ class InitVarHandler(_PythonInitHandler):
2213
+ def _get_name(self, handler_name: Any, handler_method: Any) -> Any:
2214
+ return get_variables.__name__ if get_variables is not None else ""
2215
+
2216
+ def _get_initial_handler(self, library: Any, name: Any, method: Any) -> Any:
2217
+ return None
2218
+
2219
+ if get_variables is not None:
2220
+ vars_initializer = InitVarHandler(
2221
+ libdoc,
2222
+ get_variables.__name__,
2223
+ get_variables,
2224
+ None,
2225
+ )
2226
+
2227
+ libdoc.inits = KeywordStore(
2228
+ keywords=[
2229
+ KeywordDoc(
2230
+ name=libdoc.name,
2231
+ arguments=[],
2232
+ doc=kw[0].doc,
2233
+ source=kw[0].source,
2234
+ line_no=kw[0].lineno if kw[0].lineno is not None else -1,
2235
+ col_offset=-1,
2236
+ end_col_offset=-1,
2237
+ end_line_no=-1,
2238
+ type="variables",
2239
+ libname=libdoc.name,
2240
+ libtype=libdoc.type,
2241
+ longname=f"{libdoc.name}.{kw[0].name}",
2242
+ is_initializer=True,
2243
+ )
2244
+ for kw in [
2245
+ (
2246
+ KeywordDocBuilder().build_keyword(k),
2247
+ k,
2248
+ )
2249
+ for k in [
2250
+ KeywordWrapper(
2251
+ vars_initializer,
2252
+ libdoc.source or "",
2253
+ )
2254
+ ]
2255
+ ]
2256
+ ]
2257
+ )
2258
+ try:
2259
+ # TODO: add type information of the value including dict key names and member names
2260
+ libdoc.variables = [
2261
+ ImportedVariableDefinition(
2262
+ line_no=1,
2263
+ col_offset=0,
2264
+ end_line_no=1,
2265
+ end_col_offset=0,
2266
+ source=source or (module_spec.origin if module_spec is not None else None) or "",
2267
+ name=name if get_robot_version() < (7, 0) else f"${{{name}}}",
2268
+ name_token=None,
2269
+ value=NativeValue(value)
2270
+ if value is None or isinstance(value, (int, float, bool, str))
2271
+ else None,
2272
+ has_value=value is None or isinstance(value, (int, float, bool, str)),
2273
+ value_is_native=value is None or isinstance(value, (int, float, bool, str)),
2274
+ )
2275
+ for name, value in importer.import_variables(import_name, args)
2276
+ ]
2277
+ except (SystemExit, KeyboardInterrupt):
2278
+ raise
2279
+ except BaseException as e:
2280
+ libdoc.errors = [
2281
+ error_from_exception(
2282
+ e,
2283
+ source or module_spec.origin
2284
+ if module_spec is not None and module_spec.origin
2285
+ else import_name
2286
+ if is_variables_by_path(import_name)
2287
+ else None,
2288
+ 1 if source is not None or module_spec is not None and module_spec.origin is not None else None,
2289
+ )
2290
+ ]
2291
+
2292
+ return libdoc
2293
+ except (SystemExit, KeyboardInterrupt, IgnoreEasterEggLibraryWarning):
2294
+ raise
2295
+ except BaseException as e:
2296
+ return VariablesDoc(
2297
+ name=stem,
2298
+ source=source or module_spec.origin if module_spec is not None else import_name,
2299
+ module_spec=module_spec,
2300
+ errors=[
2301
+ error_from_exception(
2302
+ e,
2303
+ source or module_spec.origin
2304
+ if module_spec is not None and module_spec.origin
2305
+ else import_name
2306
+ if is_variables_by_path(import_name)
2307
+ else None,
2308
+ 1 if source is not None or module_spec is not None and module_spec.origin is not None else None,
2309
+ )
2310
+ ],
2311
+ python_path=sys.path,
2312
+ )
2313
+
2314
+
2315
+ def find_file(
2316
+ name: str,
2317
+ working_dir: str = ".",
2318
+ base_dir: str = ".",
2319
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2320
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2321
+ file_type: str = "Resource",
2322
+ ) -> str:
2323
+ from robot.errors import DataError
2324
+ from robot.utils.robotpath import find_file as robot_find_file
2325
+
2326
+ _update_env(working_dir)
2327
+
2328
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2329
+ try:
2330
+ name = robot_variables.replace_string(name, ignore_errors=False)
2331
+ except DataError as error:
2332
+ raise DataError(f"Replacing variables from setting '{file_type}' failed: {error}")
2333
+
2334
+ return cast(str, robot_find_file(name, base_dir or ".", file_type))
2335
+
2336
+
2337
+ class CompleteResultKind(Enum):
2338
+ MODULE_INTERNAL = "Module (Internal)"
2339
+ MODULE = "Module"
2340
+ FILE = "File"
2341
+ RESOURCE = "Resource"
2342
+ VARIABLES = "Variables"
2343
+ VARIABLES_MODULE = "Variables Module"
2344
+ FOLDER = "Directory"
2345
+ KEYWORD = "Keyword"
2346
+
2347
+
2348
+ class CompleteResult(NamedTuple):
2349
+ label: str
2350
+ kind: CompleteResultKind
2351
+
2352
+
2353
+ def is_file_like(name: Optional[str]) -> bool:
2354
+ if name is None:
2355
+ return False
2356
+
2357
+ base, filename = os.path.split(name)
2358
+ return name.startswith(".") or bool(base) and filename != name
2359
+
2360
+
2361
+ def iter_module_names(name: Optional[str] = None) -> Iterator[str]:
2362
+ if name is not None:
2363
+ spec = importlib.util.find_spec(name)
2364
+ if spec is None:
2365
+ return
2366
+ else:
2367
+ spec = None
2368
+
2369
+ if spec is None:
2370
+ for e in pkgutil.iter_modules():
2371
+ if not e.name.startswith(("_", ".")):
2372
+ yield e.name
2373
+ return
2374
+
2375
+ if spec.submodule_search_locations is None:
2376
+ return
2377
+
2378
+ for e in pkgutil.iter_modules(spec.submodule_search_locations):
2379
+ if not e.name.startswith(("_", ".")):
2380
+ yield e.name
2381
+
2382
+
2383
+ NOT_WANTED_DIR_EXTENSIONS = [".dist-info"]
2384
+
2385
+
2386
+ def iter_modules_from_python_path(
2387
+ path: Optional[str] = None,
2388
+ ) -> Iterator[CompleteResult]:
2389
+ path = path.replace(".", os.sep) if path is not None and not path.startswith((".", "/", os.sep)) else path
2390
+
2391
+ if path is None:
2392
+ paths = sys.path
2393
+ else:
2394
+ paths = [str(Path(s, path)) for s in sys.path]
2395
+
2396
+ for e in [Path(p) for p in set(paths)]:
2397
+ if e.is_dir():
2398
+ for f in e.iterdir():
2399
+ if not f.name.startswith(("_", ".")) and (
2400
+ f.is_file()
2401
+ and f.suffix in ALLOWED_LIBRARY_FILE_EXTENSIONS
2402
+ or f.is_dir()
2403
+ and f.suffix not in NOT_WANTED_DIR_EXTENSIONS
2404
+ ):
2405
+ if f.is_dir():
2406
+ yield CompleteResult(f.name, CompleteResultKind.MODULE)
2407
+
2408
+ if f.is_file():
2409
+ yield CompleteResult(f.stem, CompleteResultKind.MODULE)
2410
+
2411
+
2412
+ def complete_library_import(
2413
+ name: Optional[str],
2414
+ working_dir: str = ".",
2415
+ base_dir: str = ".",
2416
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2417
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2418
+ ) -> List[CompleteResult]:
2419
+ _update_env(working_dir)
2420
+
2421
+ result: List[CompleteResult] = []
2422
+
2423
+ if name is None:
2424
+ result += [
2425
+ CompleteResult(e, CompleteResultKind.MODULE_INTERNAL)
2426
+ for e in iter_module_names(ROBOT_LIBRARY_PACKAGE)
2427
+ if e not in DEFAULT_LIBRARIES
2428
+ ]
2429
+
2430
+ if name is not None:
2431
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2432
+
2433
+ name = robot_variables.replace_string(name, ignore_errors=True)
2434
+
2435
+ file_like = is_file_like(name)
2436
+
2437
+ if name is None or not file_like:
2438
+ result += list(iter_modules_from_python_path(name))
2439
+
2440
+ if name is None or file_like:
2441
+ name_path = Path(name if name else base_dir)
2442
+ if name and name_path.is_absolute():
2443
+ paths = [name_path]
2444
+ else:
2445
+ paths = [
2446
+ Path(base_dir, name) if name else Path(base_dir),
2447
+ *((Path(s) for s in sys.path) if not name else []),
2448
+ *((Path(s, name) for s in sys.path) if name and not name.startswith(".") else []),
2449
+ ]
2450
+
2451
+ for p in paths:
2452
+ path = p.resolve()
2453
+
2454
+ if path.exists() and path.is_dir():
2455
+ result += [
2456
+ CompleteResult(
2457
+ str(f.name),
2458
+ CompleteResultKind.FILE if f.is_file() else CompleteResultKind.FOLDER,
2459
+ )
2460
+ for f in path.iterdir()
2461
+ if not f.name.startswith(("_", "."))
2462
+ and (
2463
+ (f.is_file() and f.suffix in ALLOWED_LIBRARY_FILE_EXTENSIONS)
2464
+ or f.is_dir()
2465
+ and f.suffix not in NOT_WANTED_DIR_EXTENSIONS
2466
+ )
2467
+ ]
2468
+
2469
+ return list(set(result))
2470
+
2471
+
2472
+ def iter_resources_from_python_path(
2473
+ path: Optional[str] = None,
2474
+ ) -> Iterator[CompleteResult]:
2475
+ if path is None:
2476
+ paths = sys.path
2477
+ else:
2478
+ paths = [str(Path(s, path)) for s in sys.path]
2479
+
2480
+ for e in [Path(p) for p in set(paths)]:
2481
+ if e.is_dir():
2482
+ for f in e.iterdir():
2483
+ if not f.name.startswith(("_", ".")) and (
2484
+ f.is_file()
2485
+ and f.suffix in ALLOWED_RESOURCE_FILE_EXTENSIONS
2486
+ or f.is_dir()
2487
+ and f.suffix not in NOT_WANTED_DIR_EXTENSIONS
2488
+ ):
2489
+ yield CompleteResult(
2490
+ f.name,
2491
+ CompleteResultKind.RESOURCE if f.is_file() else CompleteResultKind.FOLDER,
2492
+ )
2493
+
2494
+
2495
+ def complete_resource_import(
2496
+ name: Optional[str],
2497
+ working_dir: str = ".",
2498
+ base_dir: str = ".",
2499
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2500
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2501
+ ) -> Optional[List[CompleteResult]]:
2502
+ _update_env(working_dir)
2503
+
2504
+ result: List[CompleteResult] = []
2505
+
2506
+ if name is not None:
2507
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2508
+
2509
+ name = robot_variables.replace_string(name, ignore_errors=True)
2510
+
2511
+ if name is None or not name.startswith(".") and not name.startswith("/") and not name.startswith(os.sep):
2512
+ result += list(iter_resources_from_python_path(name))
2513
+
2514
+ if name is None or name.startswith((".", "/", os.sep)):
2515
+ name_path = Path(name if name else base_dir)
2516
+ if name_path.is_absolute():
2517
+ path = name_path.resolve()
2518
+ else:
2519
+ path = Path(base_dir, name if name else base_dir).resolve()
2520
+
2521
+ if path.exists() and (path.is_dir()):
2522
+ result += [
2523
+ CompleteResult(
2524
+ str(f.name),
2525
+ CompleteResultKind.RESOURCE if f.is_file() else CompleteResultKind.FOLDER,
2526
+ )
2527
+ for f in path.iterdir()
2528
+ if not f.name.startswith(("_", "."))
2529
+ and (f.is_dir() or (f.is_file() and f.suffix in ALLOWED_RESOURCE_FILE_EXTENSIONS))
2530
+ ]
2531
+
2532
+ return list(set(result))
2533
+
2534
+
2535
+ def complete_variables_import(
2536
+ name: Optional[str],
2537
+ working_dir: str = ".",
2538
+ base_dir: str = ".",
2539
+ command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
2540
+ variables: Optional[Dict[str, Optional[Any]]] = None,
2541
+ ) -> Optional[List[CompleteResult]]:
2542
+ _update_env(working_dir)
2543
+
2544
+ result: List[CompleteResult] = []
2545
+
2546
+ if name is not None:
2547
+ robot_variables = resolve_robot_variables(working_dir, base_dir, command_line_variables, variables)
2548
+
2549
+ name = robot_variables.replace_string(name, ignore_errors=True)
2550
+
2551
+ file_like = get_robot_version() < (5, 0) or is_file_like(name)
2552
+
2553
+ if get_robot_version() >= (5, 0) and (name is None or not file_like):
2554
+ result += list(iter_modules_from_python_path(name))
2555
+
2556
+ if name is None or file_like:
2557
+ name_path = Path(name if name else base_dir)
2558
+ if name and name_path.is_absolute():
2559
+ paths = [name_path]
2560
+ else:
2561
+ paths = [
2562
+ Path(base_dir, name) if name else Path(base_dir),
2563
+ *((Path(s) for s in sys.path) if not name else []),
2564
+ *((Path(s, name) for s in sys.path) if name and not name.startswith(".") else []),
2565
+ ]
2566
+
2567
+ for p in paths:
2568
+ path = p.resolve()
2569
+
2570
+ if path.exists() and path.is_dir():
2571
+ result += [
2572
+ CompleteResult(
2573
+ str(f.name),
2574
+ CompleteResultKind.FILE if f.is_file() else CompleteResultKind.FOLDER,
2575
+ )
2576
+ for f in path.iterdir()
2577
+ if not f.name.startswith(("_", "."))
2578
+ and (
2579
+ (f.is_file() and f.suffix in ALLOWED_VARIABLES_FILE_EXTENSIONS)
2580
+ or f.is_dir()
2581
+ and f.suffix not in NOT_WANTED_DIR_EXTENSIONS
2582
+ )
2583
+ ]
2584
+
2585
+ return list(set(result))
2586
+
2587
+
2588
+ def get_model_doc(
2589
+ model: ast.AST,
2590
+ source: str,
2591
+ model_type: str = "RESOURCE",
2592
+ scope: str = "GLOBAL",
2593
+ append_model_errors: bool = True,
2594
+ ) -> LibraryDoc:
2595
+ from robot.errors import DataError, VariableError
2596
+ from robot.libdocpkg.robotbuilder import KeywordDocBuilder
2597
+ from robot.output.logger import LOGGER
2598
+ from robot.parsing.lexer.tokens import Token as RobotToken
2599
+ from robot.parsing.model.blocks import Keyword
2600
+ from robot.parsing.model.statements import Arguments, KeywordName
2601
+ from robot.running.builder.transformers import ResourceBuilder
2602
+
2603
+ if get_robot_version() < (7, 0):
2604
+ from robot.running.model import ResourceFile
2605
+ from robot.running.usererrorhandler import UserErrorHandler
2606
+ from robot.running.userkeyword import UserLibrary
2607
+ else:
2608
+ from robot.running.invalidkeyword import (
2609
+ InvalidKeyword as UserErrorHandler,
2610
+ )
2611
+ from robot.running.resourcemodel import ResourceFile
2612
+
2613
+ errors: List[Error] = []
2614
+ keyword_name_nodes: List[KeywordName] = []
2615
+ keywords_nodes: List[Keyword] = []
2616
+ for node in ast.walk(model):
2617
+ if isinstance(node, Keyword):
2618
+ keywords_nodes.append(node)
2619
+ if isinstance(node, KeywordName):
2620
+ keyword_name_nodes.append(node)
2621
+
2622
+ error = node.error if isinstance(node, HasError) else None
2623
+ if error is not None:
2624
+ errors.append(
2625
+ Error(
2626
+ message=error,
2627
+ type_name="ModelError",
2628
+ source=source,
2629
+ line_no=node.lineno,
2630
+ )
2631
+ )
2632
+ if append_model_errors:
2633
+ node_errors = node.errors if isinstance(node, HasErrors) else None
2634
+ if node_errors is not None:
2635
+ for e in node_errors:
2636
+ errors.append(
2637
+ Error(
2638
+ message=e,
2639
+ type_name="ModelError",
2640
+ source=source,
2641
+ line_no=node.lineno,
2642
+ )
2643
+ )
2644
+
2645
+ def get_keyword_name_token_from_line(line: int) -> Optional[Token]:
2646
+ for keyword_name in keyword_name_nodes:
2647
+ if keyword_name.lineno == line:
2648
+ return cast(Token, keyword_name.get_token(RobotToken.KEYWORD_NAME))
2649
+
2650
+ return None
2651
+
2652
+ def get_argument_definitions_from_line(
2653
+ line: int,
2654
+ ) -> List[ArgumentDefinition]:
2655
+ keyword_node = next((k for k in keywords_nodes if k.lineno == line), None)
2656
+ if keyword_node is None:
2657
+ return []
2658
+
2659
+ arguments_node = next(
2660
+ (n for n in ast.walk(keyword_node) if isinstance(n, Arguments)),
2661
+ None,
2662
+ )
2663
+ if arguments_node is None:
2664
+ return []
2665
+
2666
+ args: List[str] = []
2667
+ arguments = arguments_node.get_tokens(RobotToken.ARGUMENT)
2668
+ argument_definitions = []
2669
+
2670
+ for argument_token in (cast(RobotToken, e) for e in arguments):
2671
+ try:
2672
+ argument = get_variable_token(argument_token)
2673
+
2674
+ if argument is not None and argument.value != "@{}":
2675
+ if argument.value not in args:
2676
+ args.append(argument.value)
2677
+ arg_def = ArgumentDefinition(
2678
+ name=argument.value,
2679
+ name_token=strip_variable_token(argument),
2680
+ line_no=argument.lineno,
2681
+ col_offset=argument.col_offset,
2682
+ end_line_no=argument.lineno,
2683
+ end_col_offset=argument.end_col_offset,
2684
+ source=source,
2685
+ )
2686
+ argument_definitions.append(arg_def)
2687
+
2688
+ except VariableError:
2689
+ pass
2690
+
2691
+ return argument_definitions
2692
+
2693
+ res = ResourceFile(source=source)
2694
+
2695
+ with LOGGER.cache_only:
2696
+ ResourceBuilder(res).visit(model)
2697
+
2698
+ if get_robot_version() < (7, 0):
2699
+
2700
+ class MyUserLibrary(UserLibrary):
2701
+ current_kw: Any = None
2702
+
2703
+ def _log_creating_failed(self, handler: UserErrorHandler, error: BaseException) -> None:
2704
+ err = Error(
2705
+ message=f"Creating keyword '{handler.name}' failed: {error!s}",
2706
+ type_name=type(error).__qualname__,
2707
+ source=self.current_kw.source if self.current_kw is not None else None,
2708
+ line_no=self.current_kw.lineno if self.current_kw is not None else None,
2709
+ )
2710
+ errors.append(err)
2711
+
2712
+ def _create_handler(self, kw: Any) -> Any:
2713
+ self.current_kw = kw
2714
+ try:
2715
+ handler = super()._create_handler(kw)
2716
+ handler.errors = None
2717
+ except DataError as e:
2718
+ err = Error(
2719
+ message=str(e),
2720
+ type_name=type(e).__qualname__,
2721
+ source=kw.source,
2722
+ line_no=kw.lineno,
2723
+ )
2724
+ errors.append(err)
2725
+
2726
+ handler = UserErrorHandler(e, kw.name, self.name)
2727
+ handler.source = kw.source
2728
+ handler.lineno = kw.lineno
2729
+
2730
+ handler.errors = [err]
2731
+
2732
+ return handler
2733
+
2734
+ lib = MyUserLibrary(res)
2735
+ else:
2736
+ lib = res
2737
+
2738
+ libdoc = LibraryDoc(
2739
+ name=lib.name or "",
2740
+ doc=lib.doc,
2741
+ type=model_type,
2742
+ scope=scope,
2743
+ source=source,
2744
+ line_no=1,
2745
+ errors=errors,
2746
+ )
2747
+
2748
+ def get_kw_errors(kw: Any) -> Any:
2749
+ r = kw.errors if hasattr(kw, "errors") else None
2750
+ if get_robot_version() >= (7, 0) and kw.error:
2751
+ if not r:
2752
+ r = []
2753
+ r.append(
2754
+ Error(
2755
+ message=str(kw.error),
2756
+ type_name="KeywordError",
2757
+ source=kw.source,
2758
+ line_no=kw.lineno,
2759
+ )
2760
+ )
2761
+ return r
2762
+
2763
+ libdoc.keywords = KeywordStore(
2764
+ source=libdoc.name,
2765
+ source_type=libdoc.type,
2766
+ keywords=[
2767
+ KeywordDoc(
2768
+ name=kw[0].name,
2769
+ arguments=[ArgumentInfo.from_robot(a) for a in kw[0].args],
2770
+ doc=kw[0].doc,
2771
+ tags=list(kw[0].tags),
2772
+ source=str(kw[0].source),
2773
+ name_token=get_keyword_name_token_from_line(kw[0].lineno),
2774
+ line_no=kw[0].lineno if kw[0].lineno is not None else -1,
2775
+ col_offset=-1,
2776
+ end_col_offset=-1,
2777
+ end_line_no=-1,
2778
+ libname=libdoc.name,
2779
+ libtype=libdoc.type,
2780
+ longname=f"{libdoc.name}.{kw[0].name}",
2781
+ is_embedded=is_embedded_keyword(kw[0].name),
2782
+ errors=get_kw_errors(kw[1]),
2783
+ is_error_handler=isinstance(kw[1], UserErrorHandler),
2784
+ error_handler_message=str(cast(UserErrorHandler, kw[1]).error)
2785
+ if isinstance(kw[1], UserErrorHandler)
2786
+ else None,
2787
+ arguments_spec=ArgumentSpec.from_robot_argument_spec(
2788
+ kw[1].arguments if get_robot_version() < (7, 0) else kw[1].args
2789
+ ),
2790
+ argument_definitions=get_argument_definitions_from_line(kw[0].lineno),
2791
+ )
2792
+ for kw in [
2793
+ (KeywordDocBuilder(resource=True).build_keyword(lw), lw)
2794
+ for lw in (lib.handlers if get_robot_version() < (7, 0) else lib.keywords)
2795
+ ]
2796
+ ],
2797
+ )
2798
+
2799
+ return libdoc