robotcode-repl 2.6.2__py3-none-any.whl → 2.8.0.dev548__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/repl/Repl/repl.py +10 -1
- robotcode/repl/__version__.py +1 -1
- robotcode/repl/_debug/controller.py +47 -5
- robotcode/repl/_debug/types.py +6 -3
- robotcode/repl/_indent.py +8 -1
- robotcode/repl/_pt/components.py +2 -1
- robotcode/repl/_pt/doc_viewer.py +457 -84
- robotcode/repl/_pt/lexer.py +4 -1
- robotcode/repl/base_interpreter.py +281 -13
- robotcode/repl/cli.py +70 -13
- robotcode/repl/console_interpreter.py +356 -111
- robotcode/repl/doc_cli.py +651 -0
- robotcode/repl/hooks.py +2 -1
- robotcode/repl/prompt_toolkit_interpreter.py +35 -2
- robotcode/repl/run.py +51 -8
- {robotcode_repl-2.6.2.dist-info → robotcode_repl-2.8.0.dev548.dist-info}/METADATA +4 -3
- robotcode_repl-2.8.0.dev548.dist-info/RECORD +30 -0
- {robotcode_repl-2.6.2.dist-info → robotcode_repl-2.8.0.dev548.dist-info}/WHEEL +1 -1
- robotcode_repl-2.6.2.dist-info/RECORD +0 -29
- {robotcode_repl-2.6.2.dist-info → robotcode_repl-2.8.0.dev548.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"""The `robotcode doc` command group.
|
|
2
|
+
|
|
3
|
+
Shows the documentation of one library, resource file or suite file as the
|
|
4
|
+
project sees it: loaded anew on every call, with the `robot.toml`
|
|
5
|
+
configuration and the selected profiles, like `robot` loads it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
from robot.conf import RobotSettings
|
|
17
|
+
from robot.errors import INFO_PRINTED, DataError, Information
|
|
18
|
+
from robot.output import LOGGER
|
|
19
|
+
from robot.running import TestSuite
|
|
20
|
+
from robot.running.builder import ResourceFileBuilder, TestSuiteBuilder
|
|
21
|
+
from robot.running.importer import RESOURCE_EXTENSIONS
|
|
22
|
+
from robot.utils import MultiMatcher, getshortdoc
|
|
23
|
+
from robot.utils.recommendations import RecommendationFinder
|
|
24
|
+
from robot.variables.scopes import GlobalVariables
|
|
25
|
+
|
|
26
|
+
from robotcode.plugin import Application, OutputFormat, pass_application
|
|
27
|
+
from robotcode.plugin._agent_detection import is_running_in_ai_agent
|
|
28
|
+
from robotcode.plugin.click_helper.types import add_options
|
|
29
|
+
from robotcode.robot.diagnostics.library_doc import (
|
|
30
|
+
IgnoreEasterEggLibraryWarning,
|
|
31
|
+
KeywordDoc,
|
|
32
|
+
LibraryDoc,
|
|
33
|
+
find_file,
|
|
34
|
+
get_library_doc,
|
|
35
|
+
get_resource_doc_from_resource,
|
|
36
|
+
resolve_robot_variables,
|
|
37
|
+
)
|
|
38
|
+
from robotcode.robot.utils import RF_VERSION
|
|
39
|
+
from robotcode.robot.utils.markdown_docs import anchor_link_resolver, unique_reference_labels
|
|
40
|
+
from robotcode.robot.utils.match import normalize
|
|
41
|
+
from robotcode.robot.utils.variables import contains_variable
|
|
42
|
+
from robotcode.runner.cli.robot import RobotFrameworkEx, handle_robot_options
|
|
43
|
+
|
|
44
|
+
from .cli import VARIABLE_AND_PATH_OPTIONS, _is_interactive_stdin
|
|
45
|
+
|
|
46
|
+
if RF_VERSION >= (6, 0):
|
|
47
|
+
|
|
48
|
+
def _build_resource(path: str, settings: RobotSettings) -> Any:
|
|
49
|
+
return ResourceFileBuilder(lang=settings.languages, process_curdir=False).build(path)
|
|
50
|
+
|
|
51
|
+
def _build_suite(path: str, settings: RobotSettings, allow_empty_suite: bool = False) -> Any:
|
|
52
|
+
return TestSuiteBuilder(
|
|
53
|
+
lang=settings.languages, allow_empty_suite=allow_empty_suite, process_curdir=False
|
|
54
|
+
).build(path)
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
|
|
58
|
+
def _build_resource(path: str, settings: RobotSettings) -> Any:
|
|
59
|
+
return ResourceFileBuilder(process_curdir=False).build(path)
|
|
60
|
+
|
|
61
|
+
def _build_suite(path: str, settings: RobotSettings, allow_empty_suite: bool = False) -> Any:
|
|
62
|
+
return TestSuiteBuilder(allow_empty_suite=allow_empty_suite, process_curdir=False).build(path)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if RF_VERSION >= (6, 1):
|
|
66
|
+
|
|
67
|
+
def _suite_name_from_source(source: str) -> str:
|
|
68
|
+
return str(TestSuite.name_from_source(source))
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
from robot.running.builder.parsers import format_name
|
|
72
|
+
|
|
73
|
+
def _suite_name_from_source(source: str) -> str:
|
|
74
|
+
return str(format_name(source))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Robot Framework recognises a suite initialization file by this name, with any suite file extension
|
|
78
|
+
_INIT_FILE_NAME = "__init__"
|
|
79
|
+
# files with these extensions are resource files only, never suite files
|
|
80
|
+
_RESOURCE_ONLY_EXTENSIONS = {".resource", ".rsrc"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class KeywordEntry:
|
|
85
|
+
name: str
|
|
86
|
+
anchor: str
|
|
87
|
+
args: str
|
|
88
|
+
short_doc: str
|
|
89
|
+
tags: List[str]
|
|
90
|
+
doc: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class TypeEntry:
|
|
95
|
+
name: str
|
|
96
|
+
anchor: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class LibraryDocumentation:
|
|
101
|
+
name: str
|
|
102
|
+
type: str
|
|
103
|
+
version: str
|
|
104
|
+
scope: str
|
|
105
|
+
source: str
|
|
106
|
+
lineno: int
|
|
107
|
+
markdown: str
|
|
108
|
+
keywords: List[KeywordEntry]
|
|
109
|
+
types: List[TypeEntry]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class KeywordSelection:
|
|
114
|
+
name: str
|
|
115
|
+
type: str
|
|
116
|
+
version: str
|
|
117
|
+
scope: str
|
|
118
|
+
source: str
|
|
119
|
+
lineno: int
|
|
120
|
+
keywords: List[KeywordEntry]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
LANGUAGE_OPTION = click.option(
|
|
124
|
+
"--language",
|
|
125
|
+
metavar="LANG",
|
|
126
|
+
type=str,
|
|
127
|
+
multiple=True,
|
|
128
|
+
help="Activate localization in addition to the languages of the configuration. See `robot --language` option.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
BASE_DIR_OPTION = click.option(
|
|
132
|
+
"--base-dir",
|
|
133
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
134
|
+
help="The directory for `${CURDIR}` and relative paths in TARGET. Default: the current directory.",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
OUTPUT_OPTION = click.option(
|
|
138
|
+
"-o",
|
|
139
|
+
"--output",
|
|
140
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
141
|
+
help="Write the Markdown to this file instead of the terminal.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
TARGET_ARGUMENT = click.argument("target", metavar="TARGET")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@click.group(invoke_without_command=False)
|
|
148
|
+
def doc() -> None:
|
|
149
|
+
"""\
|
|
150
|
+
Show the documentation of a library, resource file or suite file.
|
|
151
|
+
|
|
152
|
+
TARGET is a library name, the path of a library, resource or suite file,
|
|
153
|
+
with import arguments appended as in Libdoc: `Name::arg1::arg2`. It is
|
|
154
|
+
loaded anew on every call, with the `robot.toml` configuration and the
|
|
155
|
+
selected profiles.
|
|
156
|
+
|
|
157
|
+
\b
|
|
158
|
+
Examples:
|
|
159
|
+
```
|
|
160
|
+
robotcode doc lib Collections
|
|
161
|
+
robotcode doc keywords Collections dictionary
|
|
162
|
+
robotcode doc keyword BuiltIn "Should Be*"
|
|
163
|
+
robotcode doc browse resources/common.resource
|
|
164
|
+
robotcode --format json doc lib 'Remote::http://${HOST}:8270'
|
|
165
|
+
```
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _abort(app: Application, messages: List[str]) -> None:
|
|
170
|
+
for message in messages:
|
|
171
|
+
app.error(message)
|
|
172
|
+
app.exit(1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _load_file(path: str, settings: RobotSettings) -> LibraryDoc:
|
|
176
|
+
file = Path(path)
|
|
177
|
+
|
|
178
|
+
if file.stem.lower() == _INIT_FILE_NAME and file.suffix.lower() not in _RESOURCE_ONLY_EXTENSIONS:
|
|
179
|
+
# documented as the suite of its directory, as Libdoc documents `__init__.robot`
|
|
180
|
+
suite = _build_suite(path, settings, allow_empty_suite=True)
|
|
181
|
+
name = suite.name
|
|
182
|
+
if name == _suite_name_from_source(path):
|
|
183
|
+
name = _suite_name_from_source(str(file.parent))
|
|
184
|
+
return _suite_doc(suite, path, name)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
resource = _build_resource(path, settings)
|
|
188
|
+
except DataError as resource_error:
|
|
189
|
+
if file.suffix.lower() in _RESOURCE_ONLY_EXTENSIONS:
|
|
190
|
+
# as Libdoc, a resource file is not tried as a suite file
|
|
191
|
+
raise
|
|
192
|
+
try:
|
|
193
|
+
suite = _build_suite(path, settings)
|
|
194
|
+
except DataError:
|
|
195
|
+
# as in Libdoc, the error of the file as a resource file
|
|
196
|
+
raise resource_error from None
|
|
197
|
+
return _suite_doc(suite, path, suite.name)
|
|
198
|
+
|
|
199
|
+
return get_resource_doc_from_resource(resource, path)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _suite_doc(suite: Any, path: str, name: str) -> LibraryDoc:
|
|
203
|
+
result = get_resource_doc_from_resource(suite.resource, path)
|
|
204
|
+
result.name = name
|
|
205
|
+
result.type = "SUITE"
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _RobotMessages:
|
|
210
|
+
"""Collects the errors and warnings Robot Framework logs while a target is loaded,
|
|
211
|
+
so that they are reported like the command's own messages."""
|
|
212
|
+
|
|
213
|
+
def __init__(self) -> None:
|
|
214
|
+
self.active = False
|
|
215
|
+
self.messages: List[Tuple[str, str]] = []
|
|
216
|
+
|
|
217
|
+
def message(self, msg: Any) -> None:
|
|
218
|
+
if self.active and msg.level in ("ERROR", "WARN"):
|
|
219
|
+
self.messages.append((msg.level, msg.message))
|
|
220
|
+
|
|
221
|
+
def report(self, app: Application) -> None:
|
|
222
|
+
for level, message in self.messages:
|
|
223
|
+
(app.error if level == "ERROR" else app.warning)(message)
|
|
224
|
+
self.messages = []
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _absolute_from(start_dir: Path, value: str) -> str:
|
|
228
|
+
"""`value`, a path that may be followed by `:` or `;` and arguments, with the path
|
|
229
|
+
made absolute against `start_dir` when it exists there."""
|
|
230
|
+
for end in (len(value), *(i for i, c in enumerate(value) if c in ":;")):
|
|
231
|
+
path = Path(value[:end])
|
|
232
|
+
if value[:end] and not path.is_absolute() and (start_dir / path).exists():
|
|
233
|
+
return str((start_dir / path).absolute()) + value[end:]
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _load(
|
|
238
|
+
app: Application,
|
|
239
|
+
target: str,
|
|
240
|
+
variable: Tuple[str, ...],
|
|
241
|
+
variablefile: Tuple[str, ...],
|
|
242
|
+
pythonpath: Tuple[str, ...],
|
|
243
|
+
language: Tuple[str, ...],
|
|
244
|
+
base_dir: Optional[Path],
|
|
245
|
+
) -> LibraryDoc:
|
|
246
|
+
"""Load TARGET like `robot` loads it, or abort like Libdoc when it cannot be loaded."""
|
|
247
|
+
name, *args = target.split("::")
|
|
248
|
+
if not name:
|
|
249
|
+
raise click.UsageError("TARGET must name a library, a resource file or a suite file.")
|
|
250
|
+
|
|
251
|
+
start_dir = Path.cwd()
|
|
252
|
+
base = str((base_dir or start_dir).absolute())
|
|
253
|
+
|
|
254
|
+
# paths given to the command are relative to where it was started, as TARGET and `-o` are
|
|
255
|
+
robot_options: Tuple[str, ...] = ()
|
|
256
|
+
for var in variable:
|
|
257
|
+
robot_options += ("--variable", var)
|
|
258
|
+
for varfile in variablefile:
|
|
259
|
+
robot_options += ("--variablefile", _absolute_from(start_dir, varfile))
|
|
260
|
+
for pypath in pythonpath:
|
|
261
|
+
robot_options += ("--pythonpath", _absolute_from(start_dir, pypath))
|
|
262
|
+
for lang in language:
|
|
263
|
+
robot_options += ("--language", lang)
|
|
264
|
+
|
|
265
|
+
root_folder, _, cmd_options = handle_robot_options(app, robot_options)
|
|
266
|
+
|
|
267
|
+
messages = _RobotMessages()
|
|
268
|
+
# Robot Framework's own console output is replaced by the command's, see `_RobotMessages`
|
|
269
|
+
LOGGER.unregister_console_logger()
|
|
270
|
+
LOGGER.register_logger(messages)
|
|
271
|
+
messages.active = True
|
|
272
|
+
|
|
273
|
+
result: Optional[LibraryDoc] = None
|
|
274
|
+
is_library = False
|
|
275
|
+
try:
|
|
276
|
+
with app.save_syspath(), app.chdir(root_folder) as orig_folder:
|
|
277
|
+
try:
|
|
278
|
+
# the options of the command follow the configuration's, so their languages add up
|
|
279
|
+
options, _ = RobotFrameworkEx(
|
|
280
|
+
app, ["."], False, root_folder=root_folder, orig_folder=orig_folder
|
|
281
|
+
).parse_arguments((*cmd_options, *robot_options))
|
|
282
|
+
if app.config.dry:
|
|
283
|
+
app.echo(
|
|
284
|
+
f"Dry run, not loading anything. Would document '{target}' with the following options:\n"
|
|
285
|
+
+ "\n".join(f"{k} = {v!r}" for k, v in options.items())
|
|
286
|
+
)
|
|
287
|
+
app.exit(INFO_PRINTED)
|
|
288
|
+
|
|
289
|
+
# a documentation writes no output files, so their directories are not created either
|
|
290
|
+
settings = RobotSettings(options, output=None, log=None, report=None, debugfile=None, xunit=None)
|
|
291
|
+
if settings.pythonpath:
|
|
292
|
+
sys.path = settings.pythonpath + sys.path
|
|
293
|
+
|
|
294
|
+
# variable files may print, but standard output belongs to the documentation
|
|
295
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
296
|
+
variables = GlobalVariables(settings).as_dict()
|
|
297
|
+
messages.report(app)
|
|
298
|
+
working_dir = str(root_folder or start_dir)
|
|
299
|
+
|
|
300
|
+
if contains_variable(name, "$@&%"):
|
|
301
|
+
# backslashes in TARGET are part of the text, such as in a path on Windows
|
|
302
|
+
name = resolve_robot_variables(working_dir, base, variables).replace_string(
|
|
303
|
+
name.replace("\\", "\\\\"), ignore_errors=False
|
|
304
|
+
)
|
|
305
|
+
if Path(name).suffix.lower() in RESOURCE_EXTENSIONS:
|
|
306
|
+
if args:
|
|
307
|
+
raise DataError(f"Resource and suite files take no import arguments: '{target}'.")
|
|
308
|
+
result = _load_file(find_file(name, working_dir, base, variables), settings)
|
|
309
|
+
else:
|
|
310
|
+
is_library = True
|
|
311
|
+
# a name that exists as a path is made absolute first, as Libdoc does
|
|
312
|
+
library_path = Path(base, name.replace("/", os.sep))
|
|
313
|
+
if library_path.exists():
|
|
314
|
+
name = str(library_path.absolute())
|
|
315
|
+
result = get_library_doc(
|
|
316
|
+
name,
|
|
317
|
+
tuple(arg.replace("\\", "\\\\") for arg in args),
|
|
318
|
+
working_dir=working_dir,
|
|
319
|
+
base_dir=base,
|
|
320
|
+
command_line_variables=variables,
|
|
321
|
+
)
|
|
322
|
+
except Information as err:
|
|
323
|
+
app.echo(str(err))
|
|
324
|
+
app.exit(INFO_PRINTED)
|
|
325
|
+
except (DataError, IgnoreEasterEggLibraryWarning) as err:
|
|
326
|
+
# the errors of a failed load belong to that load, as in Libdoc
|
|
327
|
+
messages.messages = []
|
|
328
|
+
_abort(app, [str(err)])
|
|
329
|
+
finally:
|
|
330
|
+
messages.active = False
|
|
331
|
+
LOGGER.unregister_logger(messages)
|
|
332
|
+
|
|
333
|
+
assert result is not None
|
|
334
|
+
|
|
335
|
+
errors = [e.message for e in result.errors or []]
|
|
336
|
+
if is_library and (not result.inits or result.loaded_without_arguments or (errors and not result.keywords)):
|
|
337
|
+
# no instance with the given arguments, or no keywords to read from it: Libdoc documents nothing either
|
|
338
|
+
_abort(app, errors or [f"Importing '{target}' failed."])
|
|
339
|
+
|
|
340
|
+
logged = [message for _, message in messages.messages]
|
|
341
|
+
messages.report(app)
|
|
342
|
+
for message in errors:
|
|
343
|
+
if not any(message in other for other in logged):
|
|
344
|
+
app.error(message)
|
|
345
|
+
for kw in result.keywords.values():
|
|
346
|
+
for error in kw.errors or []:
|
|
347
|
+
if not any(error.message in other for other in logged):
|
|
348
|
+
app.error(f"{kw.name}: {error.message}")
|
|
349
|
+
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _page(library: LibraryDoc) -> str:
|
|
354
|
+
return library.to_markdown(only_doc=False, header_level=0, link_resolver=anchor_link_resolver)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _check_output(app: Application, output: Optional[Path]) -> Optional[Path]:
|
|
358
|
+
if output is None:
|
|
359
|
+
return None
|
|
360
|
+
if app.config.output_format not in (None, OutputFormat.TEXT):
|
|
361
|
+
raise click.UsageError("`-o/--output` writes Markdown and cannot be used with `--format`.")
|
|
362
|
+
# relative to where the command was started, before anything changes the current directory
|
|
363
|
+
return output.absolute()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _is_text_format(app: Application) -> bool:
|
|
367
|
+
return app.config.output_format in (None, OutputFormat.TEXT)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _write_markdown(app: Application, text: str, output: Optional[Path]) -> None:
|
|
371
|
+
if output is None:
|
|
372
|
+
app.echo_as_markdown(text.rstrip("\n"))
|
|
373
|
+
return
|
|
374
|
+
try:
|
|
375
|
+
# as Libdoc, the directory of the file is created when it is missing
|
|
376
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
output.write_text(text, encoding="utf-8", newline="\n")
|
|
378
|
+
except OSError as e:
|
|
379
|
+
_abort(app, [f"Writing '{output}' failed: {e}"])
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _one_line(text: str) -> str:
|
|
383
|
+
return " ".join(line.strip() for line in text.splitlines() if line.strip())
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _keyword_entries(library: LibraryDoc, page: str, keywords: List[KeywordDoc]) -> List[KeywordEntry]:
|
|
387
|
+
page_keywords = library.get_page_keywords()
|
|
388
|
+
anchors = dict(zip((id(kw) for kw in page_keywords), library.get_page_anchors(page)[0]))
|
|
389
|
+
return [
|
|
390
|
+
KeywordEntry(
|
|
391
|
+
name=kw.name,
|
|
392
|
+
anchor=anchors.get(id(kw), ""),
|
|
393
|
+
args=kw.parameter_signature().replace("", ""),
|
|
394
|
+
short_doc=_one_line(kw.format_text(getshortdoc(kw.doc))),
|
|
395
|
+
tags=list(kw.tags),
|
|
396
|
+
doc=kw.format_text(kw.doc),
|
|
397
|
+
)
|
|
398
|
+
for kw in keywords
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _keyword_selection(library: LibraryDoc, keywords: List[KeywordDoc]) -> KeywordSelection:
|
|
403
|
+
return KeywordSelection(
|
|
404
|
+
name=library.name,
|
|
405
|
+
type=library.type,
|
|
406
|
+
version=library.version,
|
|
407
|
+
scope=library.scope,
|
|
408
|
+
source=library.source or "",
|
|
409
|
+
lineno=library.line_no,
|
|
410
|
+
keywords=_keyword_entries(library, _page(library), keywords),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _title(library: LibraryDoc) -> str:
|
|
415
|
+
return f"# {library.type.capitalize()} *{library.name}*"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@doc.command()
|
|
419
|
+
@TARGET_ARGUMENT
|
|
420
|
+
@add_options(*VARIABLE_AND_PATH_OPTIONS, LANGUAGE_OPTION, BASE_DIR_OPTION, OUTPUT_OPTION)
|
|
421
|
+
@pass_application
|
|
422
|
+
def lib(
|
|
423
|
+
app: Application,
|
|
424
|
+
target: str,
|
|
425
|
+
variable: Tuple[str, ...],
|
|
426
|
+
variablefile: Tuple[str, ...],
|
|
427
|
+
pythonpath: Tuple[str, ...],
|
|
428
|
+
language: Tuple[str, ...],
|
|
429
|
+
base_dir: Optional[Path],
|
|
430
|
+
output: Optional[Path],
|
|
431
|
+
) -> None:
|
|
432
|
+
"""\
|
|
433
|
+
Show the full documentation of TARGET.
|
|
434
|
+
|
|
435
|
+
The page has the introduction, the importing arguments, an index and
|
|
436
|
+
the documentation of every keyword, and the data types the keywords use.
|
|
437
|
+
|
|
438
|
+
\b
|
|
439
|
+
Examples:
|
|
440
|
+
```
|
|
441
|
+
robotcode doc lib Collections
|
|
442
|
+
robotcode doc lib -o common.md resources/common.resource
|
|
443
|
+
robotcode --format json doc lib 'MyLibrary::arg1'
|
|
444
|
+
```
|
|
445
|
+
"""
|
|
446
|
+
output = _check_output(app, output)
|
|
447
|
+
library = _load(app, target, variable, variablefile, pythonpath, language, base_dir)
|
|
448
|
+
page = _page(library)
|
|
449
|
+
|
|
450
|
+
if not _is_text_format(app):
|
|
451
|
+
_, type_anchors = library.get_page_anchors(page)
|
|
452
|
+
app.print_data(
|
|
453
|
+
LibraryDocumentation(
|
|
454
|
+
name=library.name,
|
|
455
|
+
type=library.type,
|
|
456
|
+
version=library.version,
|
|
457
|
+
scope=library.scope,
|
|
458
|
+
source=library.source or "",
|
|
459
|
+
lineno=library.line_no,
|
|
460
|
+
markdown=page,
|
|
461
|
+
keywords=_keyword_entries(library, page, library.get_page_keywords()),
|
|
462
|
+
types=[TypeEntry(t.name, type_anchors.get(t.name, "")) for t in library.get_page_types()],
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
_write_markdown(app, page, output)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@doc.command()
|
|
471
|
+
@TARGET_ARGUMENT
|
|
472
|
+
@click.argument("patterns", metavar="[PATTERN]...", nargs=-1)
|
|
473
|
+
@click.option(
|
|
474
|
+
"--tag",
|
|
475
|
+
"tags",
|
|
476
|
+
metavar="TAG",
|
|
477
|
+
multiple=True,
|
|
478
|
+
help="Only keywords with a tag that matches TAG. `*` and `?` are wildcards. Can be given more than once.",
|
|
479
|
+
)
|
|
480
|
+
@add_options(*VARIABLE_AND_PATH_OPTIONS, LANGUAGE_OPTION, BASE_DIR_OPTION, OUTPUT_OPTION)
|
|
481
|
+
@pass_application
|
|
482
|
+
def keywords(
|
|
483
|
+
app: Application,
|
|
484
|
+
target: str,
|
|
485
|
+
patterns: Tuple[str, ...],
|
|
486
|
+
tags: Tuple[str, ...],
|
|
487
|
+
variable: Tuple[str, ...],
|
|
488
|
+
variablefile: Tuple[str, ...],
|
|
489
|
+
pythonpath: Tuple[str, ...],
|
|
490
|
+
language: Tuple[str, ...],
|
|
491
|
+
base_dir: Optional[Path],
|
|
492
|
+
output: Optional[Path],
|
|
493
|
+
) -> None:
|
|
494
|
+
"""\
|
|
495
|
+
List the keywords of TARGET with their arguments and short documentation.
|
|
496
|
+
|
|
497
|
+
A PATTERN selects the keywords whose name contains it. `*` and `?` are
|
|
498
|
+
wildcards; case, spaces and underscores are ignored. Several patterns
|
|
499
|
+
select the keywords that match any of them.
|
|
500
|
+
|
|
501
|
+
\b
|
|
502
|
+
Examples:
|
|
503
|
+
```
|
|
504
|
+
robotcode doc keywords Collections
|
|
505
|
+
robotcode doc keywords Collections dictionary
|
|
506
|
+
robotcode doc keywords Collections "get*list"
|
|
507
|
+
robotcode doc keywords Collections append insert
|
|
508
|
+
robotcode doc keywords resources/common.resource --tag smoke
|
|
509
|
+
```
|
|
510
|
+
"""
|
|
511
|
+
output = _check_output(app, output)
|
|
512
|
+
library = _load(app, target, variable, variablefile, pythonpath, language, base_dir)
|
|
513
|
+
|
|
514
|
+
name_matcher = MultiMatcher([f"*{p}*" for p in patterns], ignore="_", match_if_no_patterns=True)
|
|
515
|
+
tag_matcher = MultiMatcher(tags, ignore="_", match_if_no_patterns=True)
|
|
516
|
+
selected = [
|
|
517
|
+
kw
|
|
518
|
+
for kw in library.get_page_keywords()
|
|
519
|
+
if name_matcher.match(kw.name) and (not tags or tag_matcher.match_any(kw.tags))
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
if not _is_text_format(app):
|
|
523
|
+
app.print_data(_keyword_selection(library, selected))
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
lines = [
|
|
527
|
+
f"- **{kw.name}** `{kw.parameter_signature().replace(chr(0x200D), '')}`"
|
|
528
|
+
+ (f" — {short_doc}" if (short_doc := _one_line(kw.format_text(getshortdoc(kw.doc)))) else "")
|
|
529
|
+
for kw in selected
|
|
530
|
+
]
|
|
531
|
+
_write_markdown(app, "\n".join([_title(library), "", *(lines or ["_(no keyword matches)_"])]) + "\n", output)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@doc.command()
|
|
535
|
+
@TARGET_ARGUMENT
|
|
536
|
+
@click.argument("names", metavar="NAME...", nargs=-1, required=True)
|
|
537
|
+
@add_options(*VARIABLE_AND_PATH_OPTIONS, LANGUAGE_OPTION, BASE_DIR_OPTION, OUTPUT_OPTION)
|
|
538
|
+
@pass_application
|
|
539
|
+
def keyword(
|
|
540
|
+
app: Application,
|
|
541
|
+
target: str,
|
|
542
|
+
names: Tuple[str, ...],
|
|
543
|
+
variable: Tuple[str, ...],
|
|
544
|
+
variablefile: Tuple[str, ...],
|
|
545
|
+
pythonpath: Tuple[str, ...],
|
|
546
|
+
language: Tuple[str, ...],
|
|
547
|
+
base_dir: Optional[Path],
|
|
548
|
+
output: Optional[Path],
|
|
549
|
+
) -> None:
|
|
550
|
+
"""\
|
|
551
|
+
Show the full documentation of keywords of TARGET.
|
|
552
|
+
|
|
553
|
+
A NAME selects the keywords whose name matches it exactly or as a pattern
|
|
554
|
+
with `*` and `?`, and the keywords with embedded arguments that match it.
|
|
555
|
+
Case, spaces and underscores are ignored. The data types that the
|
|
556
|
+
arguments use follow the keywords.
|
|
557
|
+
|
|
558
|
+
\b
|
|
559
|
+
Examples:
|
|
560
|
+
```
|
|
561
|
+
robotcode doc keyword Collections "Get Match Count"
|
|
562
|
+
robotcode doc keyword BuiltIn "Should Be*"
|
|
563
|
+
robotcode doc keyword resources/common.resource "Open Chrome Browser"
|
|
564
|
+
```
|
|
565
|
+
"""
|
|
566
|
+
output = _check_output(app, output)
|
|
567
|
+
library = _load(app, target, variable, variablefile, pythonpath, language, base_dir)
|
|
568
|
+
|
|
569
|
+
page_keywords = library.get_page_keywords()
|
|
570
|
+
selected_ids = set()
|
|
571
|
+
missing: List[str] = []
|
|
572
|
+
for name in names:
|
|
573
|
+
matcher = MultiMatcher([name], ignore="_")
|
|
574
|
+
matches = {id(kw) for kw in page_keywords if matcher.match(kw.name)}
|
|
575
|
+
matches.update(id(kw) for kw in library.keywords.iter_all(name))
|
|
576
|
+
matches &= {id(kw) for kw in page_keywords}
|
|
577
|
+
if not matches:
|
|
578
|
+
missing.append(name)
|
|
579
|
+
selected_ids |= matches
|
|
580
|
+
selected = [kw for kw in page_keywords if id(kw) in selected_ids]
|
|
581
|
+
|
|
582
|
+
if not _is_text_format(app):
|
|
583
|
+
app.print_data(_keyword_selection(library, selected))
|
|
584
|
+
elif selected:
|
|
585
|
+
type_docs: Dict[str, Any] = {}
|
|
586
|
+
for kw in selected:
|
|
587
|
+
for argument in kw.arguments:
|
|
588
|
+
for type_doc in library.get_types_for_argument(argument):
|
|
589
|
+
type_docs.setdefault(type_doc.name, type_doc)
|
|
590
|
+
parts = [kw.to_markdown() for kw in selected]
|
|
591
|
+
parts.extend(type_docs[t].to_markdown(header_level=1) for t in sorted(type_docs, key=lambda t: (t.lower(), t)))
|
|
592
|
+
# One document: a label that two parts define for other URLs would link both to the first URL.
|
|
593
|
+
labels: Dict[str, str] = {}
|
|
594
|
+
_write_markdown(
|
|
595
|
+
app,
|
|
596
|
+
"\n\n---\n\n".join(unique_reference_labels(part.strip(), labels) for part in parts) + "\n",
|
|
597
|
+
output,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
if missing:
|
|
601
|
+
finder = RecommendationFinder(normalize)
|
|
602
|
+
candidates = [kw.name for kw in page_keywords]
|
|
603
|
+
for name in missing:
|
|
604
|
+
app.error(finder.find_and_format(name, candidates, f"No keyword matches '{name}'."))
|
|
605
|
+
app.exit(1)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _is_interactive_stdout() -> bool:
|
|
609
|
+
try:
|
|
610
|
+
return sys.stdout.isatty()
|
|
611
|
+
except (AttributeError, ValueError, OSError):
|
|
612
|
+
return False
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@doc.command()
|
|
616
|
+
@TARGET_ARGUMENT
|
|
617
|
+
@add_options(*VARIABLE_AND_PATH_OPTIONS, LANGUAGE_OPTION, BASE_DIR_OPTION)
|
|
618
|
+
@pass_application
|
|
619
|
+
def browse(
|
|
620
|
+
app: Application,
|
|
621
|
+
target: str,
|
|
622
|
+
variable: Tuple[str, ...],
|
|
623
|
+
variablefile: Tuple[str, ...],
|
|
624
|
+
pythonpath: Tuple[str, ...],
|
|
625
|
+
language: Tuple[str, ...],
|
|
626
|
+
base_dir: Optional[Path],
|
|
627
|
+
) -> None:
|
|
628
|
+
"""\
|
|
629
|
+
Browse the full documentation of TARGET in the terminal.
|
|
630
|
+
|
|
631
|
+
Opens the documentation viewer of the REPL, with links, search and
|
|
632
|
+
back/forward navigation. In AI-agent sessions, in pipes and without a
|
|
633
|
+
terminal, the Markdown is printed instead, as `robotcode doc lib` prints it.
|
|
634
|
+
|
|
635
|
+
\b
|
|
636
|
+
Examples:
|
|
637
|
+
```
|
|
638
|
+
robotcode doc browse Collections
|
|
639
|
+
robotcode doc browse resources/common.resource
|
|
640
|
+
```
|
|
641
|
+
"""
|
|
642
|
+
library = _load(app, target, variable, variablefile, pythonpath, language, base_dir)
|
|
643
|
+
page = _page(library)
|
|
644
|
+
|
|
645
|
+
if _is_interactive_stdin() and _is_interactive_stdout() and not is_running_in_ai_agent():
|
|
646
|
+
from ._pt.doc_viewer import DocViewer
|
|
647
|
+
|
|
648
|
+
DocViewer().run(library.name, page, outline=True)
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
app.echo_as_markdown(page.rstrip("\n"))
|
robotcode/repl/hooks.py
CHANGED