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.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/config/loader.py +20 -6
- robotcode/robot/config/model.py +120 -62
- robotcode/robot/config/utils.py +20 -8
- robotcode/robot/diagnostics/__init__.py +0 -0
- robotcode/robot/diagnostics/entities.py +377 -0
- robotcode/robot/diagnostics/library_doc.py +2799 -0
- robotcode/robot/utils/__init__.py +1 -2
- robotcode/robot/utils/ast.py +34 -29
- robotcode/robot/utils/markdownformatter.py +2 -2
- robotcode/robot/utils/match.py +23 -0
- robotcode/robot/utils/robot_path.py +70 -0
- robotcode/robot/utils/stubs.py +24 -0
- robotcode/robot/utils/variables.py +37 -0
- robotcode/robot/utils/{visitors.py → visitor.py} +10 -21
- {robotcode_robot-0.68.0.dist-info → robotcode_robot-0.68.2.dist-info}/METADATA +4 -4
- robotcode_robot-0.68.2.dist-info/RECORD +22 -0
- robotcode_robot-0.68.0.dist-info/RECORD +0 -15
- {robotcode_robot-0.68.0.dist-info → robotcode_robot-0.68.2.dist-info}/WHEEL +0 -0
- {robotcode_robot-0.68.0.dist-info → robotcode_robot-0.68.2.dist-info}/licenses/LICENSE.txt +0 -0
@@ -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
|