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
|
@@ -24,28 +24,33 @@ import shlex
|
|
|
24
24
|
import sys
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, TypeVar, Union
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, TypeVar, Union, cast
|
|
28
28
|
|
|
29
29
|
import click
|
|
30
30
|
from robot import result, running
|
|
31
|
+
from robot.errors import ExecutionFailed, ExecutionPassed
|
|
31
32
|
from robot.running import Keyword
|
|
32
33
|
from robot.running.context import EXECUTION_CONTEXTS
|
|
34
|
+
from robot.utils import normalize
|
|
33
35
|
|
|
34
36
|
from robotcode.plugin import Application
|
|
35
37
|
from robotcode.robot.diagnostics.library_doc import (
|
|
38
|
+
MARKDOWN_DOC_FORMAT,
|
|
36
39
|
REST_DOC_FORMAT,
|
|
37
40
|
ROBOT_DOC_FORMAT,
|
|
38
41
|
LibraryDoc,
|
|
39
42
|
convert_from_rest,
|
|
43
|
+
get_docstring_info,
|
|
40
44
|
get_library_doc_from_library,
|
|
41
45
|
get_resource_doc_from_resource,
|
|
42
46
|
)
|
|
43
|
-
from robotcode.robot.utils import get_robot_version_str
|
|
47
|
+
from robotcode.robot.utils import RF_VERSION, get_robot_version_str
|
|
48
|
+
from robotcode.robot.utils.markdown_docs import LinkResolver, anchor_link_resolver, normalize_markdown_doc
|
|
44
49
|
from robotcode.robot.utils.markdownformatter import MarkDownFormatter
|
|
45
50
|
|
|
46
51
|
from .__version__ import __version__
|
|
47
|
-
from ._debug.types import Breakpoint, ResumeAction, StackFrame, StopEvent
|
|
48
|
-
from ._indent import compute_indent
|
|
52
|
+
from ._debug.types import Breakpoint, ResumeAction, StackFrame, StopEvent, StopReason
|
|
53
|
+
from ._indent import compute_indent, starts_with_continuation
|
|
49
54
|
from ._keyword_lookup import (
|
|
50
55
|
_LIB_KEYWORDS_ATTR,
|
|
51
56
|
iter_keyword_owners,
|
|
@@ -54,7 +59,7 @@ from ._keyword_lookup import (
|
|
|
54
59
|
lookup_resource,
|
|
55
60
|
)
|
|
56
61
|
from ._session_export import render_robot_file
|
|
57
|
-
from .base_interpreter import BaseInterpreter, is_true
|
|
62
|
+
from .base_interpreter import BaseInterpreter, ReplInput, is_true
|
|
58
63
|
|
|
59
64
|
if TYPE_CHECKING:
|
|
60
65
|
from ._debug.controller import DebugController
|
|
@@ -91,6 +96,19 @@ _SETTING_IMPORT_ALIASES = {
|
|
|
91
96
|
"variables": "Import Variables",
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
# Robot Framework 5.0 fails an invalid TRY without logging why; later versions log it.
|
|
100
|
+
if RF_VERSION < (6, 0):
|
|
101
|
+
from robot.running.model import Try as _RunningTry
|
|
102
|
+
|
|
103
|
+
def _unlogged_error(item: Any) -> Optional[str]:
|
|
104
|
+
return cast(Optional[str], item.error) if isinstance(item, _RunningTry) else None
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
|
|
108
|
+
def _unlogged_error(item: Any) -> Optional[str]:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
94
112
|
# `.help` groups, in display order. A command's group is set on `@dot_command`.
|
|
95
113
|
_GROUP_ORDER = ("Session", "Debugger")
|
|
96
114
|
|
|
@@ -120,36 +138,58 @@ def dot_command(name: str, *aliases: str, group: str = "Session") -> Callable[[F
|
|
|
120
138
|
return decorator
|
|
121
139
|
|
|
122
140
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
# The variables Robot Framework sets itself, normalized the way Robot compares
|
|
142
|
+
# variable names (case, spaces and underscores ignored). `.vars --user` hides
|
|
143
|
+
# them so the listing focuses on what the session or the suite defined.
|
|
144
|
+
# `${\n}` is a backslash followed by `n`.
|
|
145
|
+
_ROBOT_BUILTIN_VARIABLES = frozenset(
|
|
146
|
+
normalize(name, ignore="_")
|
|
147
|
+
for name in (
|
|
148
|
+
"/",
|
|
149
|
+
":",
|
|
150
|
+
"\\n",
|
|
151
|
+
"SPACE",
|
|
152
|
+
"True",
|
|
153
|
+
"False",
|
|
154
|
+
"None",
|
|
155
|
+
"null",
|
|
156
|
+
"TEMPDIR",
|
|
157
|
+
"EXECDIR",
|
|
158
|
+
"OPTIONS",
|
|
159
|
+
"OUTPUT_DIR",
|
|
160
|
+
"OUTPUT_FILE",
|
|
161
|
+
"REPORT_FILE",
|
|
162
|
+
"LOG_FILE",
|
|
163
|
+
"DEBUG_FILE",
|
|
164
|
+
"LOG_LEVEL",
|
|
165
|
+
"PREV_TEST_NAME",
|
|
166
|
+
"PREV_TEST_STATUS",
|
|
167
|
+
"PREV_TEST_MESSAGE",
|
|
168
|
+
"SUITE_NAME",
|
|
169
|
+
"SUITE_SOURCE",
|
|
170
|
+
"SUITE_DOCUMENTATION",
|
|
171
|
+
"SUITE_METADATA",
|
|
172
|
+
"SUITE_STATUS",
|
|
173
|
+
"SUITE_MESSAGE",
|
|
174
|
+
"TEST_NAME",
|
|
175
|
+
"TEST_TAGS",
|
|
176
|
+
"TEST_DOCUMENTATION",
|
|
177
|
+
"TEST_METADATA",
|
|
178
|
+
"TEST_STATUS",
|
|
179
|
+
"TEST_MESSAGE",
|
|
180
|
+
"KEYWORD_STATUS",
|
|
181
|
+
"KEYWORD_MESSAGE",
|
|
182
|
+
)
|
|
147
183
|
)
|
|
148
184
|
|
|
149
185
|
|
|
150
|
-
def _is_robot_internal(
|
|
151
|
-
"""Whether a
|
|
152
|
-
|
|
186
|
+
def _is_robot_internal(name: str) -> bool:
|
|
187
|
+
"""Whether a variable such as `${SUITE_NAME}` is one that Robot Framework sets itself,
|
|
188
|
+
or the REPL's result variable `${_}` (see `set_last_result`)."""
|
|
189
|
+
bare = name[2:-1] if len(name) > 2 and name[0] in "$@&%" and name[1] == "{" and name[-1] == "}" else name
|
|
190
|
+
normalized = normalize(bare, ignore="_")
|
|
191
|
+
# `${_}` normalizes to an empty name.
|
|
192
|
+
return not normalized or normalized in _ROBOT_BUILTIN_VARIABLES
|
|
153
193
|
|
|
154
194
|
|
|
155
195
|
# Destructive commands that must never be reachable via prefix abbreviation — only
|
|
@@ -167,6 +207,8 @@ def _format_doc_to_md(text: str, doc_format: str) -> str:
|
|
|
167
207
|
return MarkDownFormatter().format(text)
|
|
168
208
|
if doc_format == REST_DOC_FORMAT:
|
|
169
209
|
return convert_from_rest(text)
|
|
210
|
+
if doc_format == MARKDOWN_DOC_FORMAT:
|
|
211
|
+
return normalize_markdown_doc(text)
|
|
170
212
|
return text
|
|
171
213
|
|
|
172
214
|
|
|
@@ -194,13 +236,18 @@ def _diagnostics_keyword_doc(owner: Any, is_resource: bool, kw_name: str) -> Opt
|
|
|
194
236
|
return matches[0] if matches else None
|
|
195
237
|
|
|
196
238
|
|
|
197
|
-
def _render_runtime_keyword_md(kw: Any, kw_name: str) -> str:
|
|
239
|
+
def _render_runtime_keyword_md(kw: Any, kw_name: str, is_resource: bool = False) -> str:
|
|
198
240
|
"""Hand-built fallback keyword page for resources / dynamic libs.
|
|
199
241
|
|
|
200
242
|
Used only when the diagnostics loader can't surface a `KeywordDoc`
|
|
201
243
|
for this keyword. Renders name + signature + tags + doc + source
|
|
202
244
|
in a shape close to what `KeywordDoc.to_markdown` produces.
|
|
203
245
|
"""
|
|
246
|
+
try:
|
|
247
|
+
# splits the `Tags:` and, with Robot Framework 7.5, the `Args:`/`Returns:`/`Raises:` sections off
|
|
248
|
+
info = get_docstring_info(kw, kw, is_resource)
|
|
249
|
+
except Exception:
|
|
250
|
+
info = None
|
|
204
251
|
md: List[str] = [f"### {kw_name}", ""]
|
|
205
252
|
|
|
206
253
|
spec = getattr(kw, "args", None) or getattr(kw, "arguments", None)
|
|
@@ -213,12 +260,22 @@ def _render_runtime_keyword_md(kw: Any, kw_name: str) -> str:
|
|
|
213
260
|
md.append(f"`{kw_name} {sig}`")
|
|
214
261
|
md.append("")
|
|
215
262
|
|
|
216
|
-
|
|
263
|
+
if info is not None:
|
|
264
|
+
for name, description in (info.argument_docs or {}).items():
|
|
265
|
+
md.append(f"- `{name}`: {description}")
|
|
266
|
+
if info.return_doc:
|
|
267
|
+
md.extend(["", f"**Returns**: {info.return_doc}"])
|
|
268
|
+
for name, description in info.raises or []:
|
|
269
|
+
md.extend(["", f"**Raises** `{name}`: {description}"])
|
|
270
|
+
if info.argument_docs or info.return_doc or info.raises:
|
|
271
|
+
md.append("")
|
|
272
|
+
|
|
273
|
+
tags = info.tags if info is not None else list(getattr(kw, "tags", None) or [])
|
|
217
274
|
if tags:
|
|
218
275
|
md.append("_Tags: " + ", ".join(str(t) for t in tags) + "_")
|
|
219
276
|
md.append("")
|
|
220
277
|
|
|
221
|
-
doc = getattr(kw, "doc", None) or ""
|
|
278
|
+
doc = (info.doc if info is not None else getattr(kw, "doc", None)) or ""
|
|
222
279
|
if doc:
|
|
223
280
|
md.append(_format_doc_to_md(doc, getattr(kw, "doc_format", ROBOT_DOC_FORMAT)))
|
|
224
281
|
|
|
@@ -257,6 +314,7 @@ def _build_save_parser() -> argparse.ArgumentParser:
|
|
|
257
314
|
p = argparse.ArgumentParser(prog=".save", add_help=False, exit_on_error=False)
|
|
258
315
|
p.add_argument("-a", "--append", action="store_true", help="Append to FILENAME instead of overwriting.")
|
|
259
316
|
p.add_argument("-t", "--test-name", default="", help="Override the generated test-case name.")
|
|
317
|
+
p.add_argument("--keep-failed", action="store_true", help="Also export inputs whose run failed.")
|
|
260
318
|
p.add_argument("filename")
|
|
261
319
|
return p
|
|
262
320
|
|
|
@@ -303,6 +361,15 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
303
361
|
# REPL inputs that parsed cleanly — `.save` exports them as a
|
|
304
362
|
# runnable `.robot` file. Each entry may be multi-line.
|
|
305
363
|
self._session_lines: List[str] = []
|
|
364
|
+
# Indexes into `_session_lines` of inputs whose run failed; `.save`
|
|
365
|
+
# leaves them out unless `--keep-failed` is given.
|
|
366
|
+
self._failed_session_lines: Set[int] = set()
|
|
367
|
+
# Set when the input ended inside an unfinished block: that block still
|
|
368
|
+
# runs, then the session ends without reading again — on a terminal, a
|
|
369
|
+
# read after Ctrl-D waits for more input instead of ending.
|
|
370
|
+
self._input_ended = False
|
|
371
|
+
# Set while a prompt waits for input: SIGTERM then ends the wait (`_end_waiting`).
|
|
372
|
+
self._reading = False
|
|
306
373
|
|
|
307
374
|
# Debug-frontend state. `_controller`/`_debug_completer` are wired by
|
|
308
375
|
# `set_controller` when a debugger is attached; `_stop`/`_frame_no`/
|
|
@@ -416,6 +483,78 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
416
483
|
del multiline_continuation, prefill, completer # no editor/prefill/completion in plain mode
|
|
417
484
|
return input(prompt)
|
|
418
485
|
|
|
486
|
+
def _read(self, prompt: str, **kwargs: Any) -> str:
|
|
487
|
+
"""`read_line` that SIGTERM can end: it raises `EOFError` once the session or run
|
|
488
|
+
is to end, also while it waits."""
|
|
489
|
+
self._reading = True
|
|
490
|
+
try:
|
|
491
|
+
# Checked after marking the read: a SIGTERM in between can't be missed.
|
|
492
|
+
if self._termination_requested:
|
|
493
|
+
raise EOFError
|
|
494
|
+
return self.read_line(prompt, **kwargs)
|
|
495
|
+
finally:
|
|
496
|
+
self._reading = False
|
|
497
|
+
|
|
498
|
+
def _end_waiting(self) -> None:
|
|
499
|
+
# Nothing pauses any more, so a robot-debug run goes on to its end.
|
|
500
|
+
if self._controller is not None:
|
|
501
|
+
self._controller.set_attached(False)
|
|
502
|
+
if self._reading:
|
|
503
|
+
self._end_read()
|
|
504
|
+
|
|
505
|
+
def _end_read(self) -> None:
|
|
506
|
+
"""End the read that waits for input, from the signal handler."""
|
|
507
|
+
raise EOFError
|
|
508
|
+
|
|
509
|
+
def _interrupt_waiting(self) -> None:
|
|
510
|
+
# The plain backend reads with `input()`, which sees Ctrl-C only as this signal; the prompts expect
|
|
511
|
+
# KeyboardInterrupt from Ctrl-C.
|
|
512
|
+
if self._reading:
|
|
513
|
+
raise KeyboardInterrupt
|
|
514
|
+
|
|
515
|
+
def _pause_on_interrupt(self) -> bool:
|
|
516
|
+
controller = self._controller
|
|
517
|
+
if (
|
|
518
|
+
controller is None
|
|
519
|
+
or not controller.attached
|
|
520
|
+
or controller.pause_requested # a second Ctrl-C interrupts the keyword that runs
|
|
521
|
+
or self._stop is not None # at a stop, Ctrl-C interrupts a keyword typed there
|
|
522
|
+
# The debugger sees the keywords of a robot-debug run, and in a REPL session those of a statement.
|
|
523
|
+
or not self._logger.enabled
|
|
524
|
+
or (self._in_repl_run and not self._running_statement)
|
|
525
|
+
):
|
|
526
|
+
return False
|
|
527
|
+
controller.request_pause()
|
|
528
|
+
if self.app is not None:
|
|
529
|
+
try:
|
|
530
|
+
# To the console, past Robot Framework's capture of what a running keyword prints, and on a line of
|
|
531
|
+
# its own, as the console output of the run can be in the middle of one.
|
|
532
|
+
self.app.echo(
|
|
533
|
+
"\nPausing at the next keyword (Ctrl-C again interrupts the keyword that runs).",
|
|
534
|
+
file=sys.__stdout__,
|
|
535
|
+
)
|
|
536
|
+
except Exception:
|
|
537
|
+
# The signal can interrupt a write to the same stream, which then refuses this one; the pause stays
|
|
538
|
+
# requested.
|
|
539
|
+
pass
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
def _interrupt_ends_run(self) -> bool:
|
|
543
|
+
# A robot-debug run (events forwarded outside a REPL session) without the debugger ends as a robot run
|
|
544
|
+
# does; REPL sessions, also while they start, keep their handling.
|
|
545
|
+
return (
|
|
546
|
+
self._logger.enabled
|
|
547
|
+
and not self._in_repl_run
|
|
548
|
+
and (self._controller is None or not self._controller.attached)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def _report_interrupt(self, prompt: str) -> None:
|
|
552
|
+
"""Report Ctrl-C at a prompt as Python's REPL does. `input()` leaves the cursor after the prompt, so the
|
|
553
|
+
line of a shown prompt is ended first."""
|
|
554
|
+
if prompt:
|
|
555
|
+
self._echo("")
|
|
556
|
+
self._echo("KeyboardInterrupt")
|
|
557
|
+
|
|
419
558
|
def make_completer(
|
|
420
559
|
self, command_names: List[str], context_provider: Callable[[], Tuple[Any, Any]]
|
|
421
560
|
) -> Optional[Any]:
|
|
@@ -424,17 +563,18 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
424
563
|
del command_names, context_provider
|
|
425
564
|
return None
|
|
426
565
|
|
|
427
|
-
def show_doc(self, title: str, markdown: str, *, scroll_to: Optional[str] = None) -> None:
|
|
566
|
+
def show_doc(self, title: str, markdown: str, *, scroll_to: Optional[str] = None, outline: bool = False) -> None:
|
|
428
567
|
"""Display markdown to the user.
|
|
429
568
|
|
|
430
569
|
Plain mode pages the raw markdown source with colour off — the
|
|
431
570
|
backend choice signals "low-fi terminal", so we don't surface
|
|
432
571
|
rich-rendered markdown the user explicitly opted out of. The
|
|
433
572
|
prompt_toolkit interpreter overrides this with the scrollable
|
|
434
|
-
doc-viewer Float. ``scroll_to``
|
|
435
|
-
|
|
573
|
+
doc-viewer Float. ``scroll_to`` and ``outline`` (a sidebar with the
|
|
574
|
+
headings of a library page) are honoured only by that viewer; the
|
|
575
|
+
pager ignores them.
|
|
436
576
|
"""
|
|
437
|
-
del scroll_to # only the scrollable viewer (prompt_toolkit) uses
|
|
577
|
+
del scroll_to, outline # only the scrollable viewer (prompt_toolkit) uses them
|
|
438
578
|
body = f"{title}\n{'=' * len(title)}\n\n{markdown}"
|
|
439
579
|
if self.app is not None:
|
|
440
580
|
self.app.echo_via_pager(body, color=False)
|
|
@@ -464,6 +604,25 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
464
604
|
self.set_last_result(None)
|
|
465
605
|
return super().run()
|
|
466
606
|
|
|
607
|
+
def run_input(self) -> None:
|
|
608
|
+
recorded = len(self._session_lines)
|
|
609
|
+
try:
|
|
610
|
+
super().run_input()
|
|
611
|
+
except ExecutionPassed as e:
|
|
612
|
+
# `Pass Execution` ends the input successfully, unless failures were
|
|
613
|
+
# continued before it — Robot Framework fails the test then.
|
|
614
|
+
if e.earlier_failures:
|
|
615
|
+
self._failed_session_lines.update(range(recorded, len(self._session_lines)))
|
|
616
|
+
raise
|
|
617
|
+
except BaseException:
|
|
618
|
+
# A failure, `Skip` or an interrupt: the input didn't run through.
|
|
619
|
+
self._failed_session_lines.update(range(recorded, len(self._session_lines)))
|
|
620
|
+
raise
|
|
621
|
+
finally:
|
|
622
|
+
# A pause that Ctrl-C asked for and the input didn't reach must not stop the next input.
|
|
623
|
+
if self._controller is not None:
|
|
624
|
+
self._controller.cancel_pause()
|
|
625
|
+
|
|
467
626
|
# ------------------------------------------------------------------
|
|
468
627
|
# Debug frontend — the interpreter *is* the debug core's `Frontend`.
|
|
469
628
|
# A pause calls `wait_at_stop`, which renders the stop and runs a
|
|
@@ -488,15 +647,17 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
488
647
|
Dot-commands route through the shared dispatcher; the debugger
|
|
489
648
|
commands (`.continue`/`.step`/…) set `_pending_action`, which ends
|
|
490
649
|
the loop. Anything else is evaluated as a keyword in the paused
|
|
491
|
-
context. EOF
|
|
650
|
+
context. EOF (Ctrl-D) at the prompt continues the run; Ctrl-C drops
|
|
651
|
+
the typed input and prompts again, as in pdb.
|
|
492
652
|
"""
|
|
493
653
|
self._stop = stop
|
|
494
654
|
self._frame_no = 0 # innermost frame selected
|
|
495
655
|
self._pending_action = None
|
|
496
656
|
# A breakpoint with attached `.commands` replays them at the hit; a
|
|
497
|
-
# leading `silent` suppresses the stop banner (pdb semantics)
|
|
657
|
+
# leading `silent` suppresses the stop banner (pdb semantics), except
|
|
658
|
+
# when it carries a condition error (a breakpoint stop's only description).
|
|
498
659
|
bp = stop.breakpoint
|
|
499
|
-
silent = bool(bp is not None and bp.commands and bp.commands[0] == "silent")
|
|
660
|
+
silent = bool(bp is not None and bp.commands and bp.commands[0] == "silent" and not stop.description)
|
|
500
661
|
if not silent:
|
|
501
662
|
self._render_stop(stop)
|
|
502
663
|
# try/finally so the stop state is always cleared, even if `.abort`
|
|
@@ -508,8 +669,12 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
508
669
|
self._replay_commands(bp.commands)
|
|
509
670
|
while self._pending_action is None:
|
|
510
671
|
try:
|
|
511
|
-
line = self.
|
|
512
|
-
except
|
|
672
|
+
line = self._read(self.DEBUG_PROMPT, completer=self._debug_completer)
|
|
673
|
+
except KeyboardInterrupt:
|
|
674
|
+
# Ctrl-C drops what was typed, as in pdb; the run stays paused.
|
|
675
|
+
self._report_interrupt(self.DEBUG_PROMPT)
|
|
676
|
+
continue
|
|
677
|
+
except EOFError:
|
|
513
678
|
self._echo("")
|
|
514
679
|
self._pending_action = ResumeAction.CONTINUE
|
|
515
680
|
break
|
|
@@ -679,6 +844,11 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
679
844
|
don't re-trigger a stop, and in `forward_events(echo_messages=True)` so
|
|
680
845
|
its log output surfaces at the prompt (suite output is muted otherwise).
|
|
681
846
|
"""
|
|
847
|
+
# At the end of a suite Robot Framework has already closed the suite in
|
|
848
|
+
# output.xml when we stop; a keyword written after it makes the file unreadable.
|
|
849
|
+
if self._stop is not None and self._stop.reason == StopReason.EXCEPTION and self._stop.frame.type == "SUITE":
|
|
850
|
+
self._echo("! Keywords can't run at the end of a suite: its output is already closed.")
|
|
851
|
+
return
|
|
682
852
|
try:
|
|
683
853
|
test, errors = self.get_test_body_from_string(line)
|
|
684
854
|
except Exception as e: # surface any parse failure to the prompt
|
|
@@ -690,9 +860,14 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
690
860
|
return
|
|
691
861
|
if not test.body:
|
|
692
862
|
return
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
863
|
+
# Keywords evaluated at a stop don't count for the session status.
|
|
864
|
+
record_failures, self.record_failures = self.record_failures, False
|
|
865
|
+
try:
|
|
866
|
+
with self._controller.suppress_pausing(), self.forward_events(echo_messages=True): # type: ignore[union-attr]
|
|
867
|
+
for kw in test.body:
|
|
868
|
+
self.set_last_result(self.run_keyword(kw))
|
|
869
|
+
finally:
|
|
870
|
+
self.record_failures = record_failures
|
|
696
871
|
self._echo(f"=> {_repr.repr(self.last_result)}")
|
|
697
872
|
|
|
698
873
|
# ------------------------------------------------------------------
|
|
@@ -733,6 +908,15 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
733
908
|
if target is not None:
|
|
734
909
|
item.name = target
|
|
735
910
|
|
|
911
|
+
def _body_to_run(self, parsed: ReplInput) -> Iterator[Keyword]:
|
|
912
|
+
"""The statements of `parsed` to run, with the setting-import aliases applied."""
|
|
913
|
+
self._alias_setting_imports(parsed.test)
|
|
914
|
+
for item in parsed.test.body:
|
|
915
|
+
error = _unlogged_error(item)
|
|
916
|
+
if error:
|
|
917
|
+
self.log_message(error, "FAIL")
|
|
918
|
+
yield item
|
|
919
|
+
|
|
736
920
|
def get_input(self) -> Iterator[Optional[Keyword]]:
|
|
737
921
|
if self.executed_files and not self.files and not self.inspect:
|
|
738
922
|
raise EOFError
|
|
@@ -744,14 +928,12 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
744
928
|
|
|
745
929
|
text = file.read_text(encoding="utf-8")
|
|
746
930
|
|
|
747
|
-
test
|
|
748
|
-
|
|
749
|
-
return
|
|
750
|
-
|
|
751
|
-
self._alias_setting_imports(test)
|
|
752
|
-
for kw in test.body:
|
|
753
|
-
yield kw
|
|
931
|
+
# Run like a test body: an invalid statement fails when execution reaches it.
|
|
932
|
+
yield from self._body_to_run(self.parse_input(text))
|
|
754
933
|
else:
|
|
934
|
+
if self._input_ended:
|
|
935
|
+
raise EOFError
|
|
936
|
+
|
|
755
937
|
lines: List[str] = []
|
|
756
938
|
last_one = False
|
|
757
939
|
while True:
|
|
@@ -761,7 +943,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
761
943
|
|
|
762
944
|
prefill = compute_indent(lines) if lines else ""
|
|
763
945
|
try:
|
|
764
|
-
text = self.
|
|
946
|
+
text = self._read(
|
|
765
947
|
prompt,
|
|
766
948
|
multiline_continuation=bool(lines),
|
|
767
949
|
prefill=prefill,
|
|
@@ -775,31 +957,47 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
775
957
|
if self._dispatch_dot_command(text):
|
|
776
958
|
return
|
|
777
959
|
except KeyboardInterrupt:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
960
|
+
# Ctrl-C drops what was typed, as in Python's REPL; the session goes on.
|
|
961
|
+
self._report_interrupt(prompt)
|
|
962
|
+
lines = []
|
|
963
|
+
last_one = False
|
|
964
|
+
continue
|
|
965
|
+
except EOFError:
|
|
966
|
+
if not lines or self._termination_requested:
|
|
967
|
+
raise
|
|
968
|
+
# Handle the unfinished block like after a final empty line, so
|
|
969
|
+
# Robot Framework reports it, and end the session afterwards.
|
|
970
|
+
self._input_ended = True
|
|
971
|
+
text = ""
|
|
972
|
+
# The report starts on a line of its own, not after the prompt.
|
|
973
|
+
if prompt:
|
|
974
|
+
self._echo("")
|
|
975
|
+
|
|
976
|
+
if not lines and starts_with_continuation(text):
|
|
977
|
+
# At the start of an input there is nothing left to continue.
|
|
978
|
+
message = "The line starting with '...' does not continue a statement, it starts a new input."
|
|
979
|
+
self.log_message(message, "FAIL")
|
|
980
|
+
raise ExecutionFailed(message)
|
|
783
981
|
|
|
784
982
|
lines.append(text)
|
|
785
983
|
|
|
786
|
-
|
|
984
|
+
parsed = self.parse_input("\n".join(lines))
|
|
787
985
|
|
|
788
986
|
if len(lines) > 1 and lines[-1] == "" and text == "":
|
|
789
987
|
last_one = True
|
|
790
988
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
989
|
+
# Only an unfinished block waits for more lines; complete input runs
|
|
990
|
+
# even when it is invalid, so Robot Framework reports it.
|
|
991
|
+
if parsed.incomplete and not last_one:
|
|
992
|
+
continue
|
|
794
993
|
|
|
795
|
-
# Record cleanly-parsed inputs for `.save
|
|
796
|
-
#
|
|
797
|
-
|
|
994
|
+
# Record cleanly-parsed inputs for `.save` (`run_input` marks the
|
|
995
|
+
# ones whose run fails). Inputs with errors are skipped so the
|
|
996
|
+
# exported file stays runnable.
|
|
997
|
+
if parsed.test.body and not parsed.errors:
|
|
798
998
|
self._session_lines.append("\n".join(lines))
|
|
799
999
|
|
|
800
|
-
self.
|
|
801
|
-
for kw in test.body:
|
|
802
|
-
yield kw
|
|
1000
|
+
yield from self._body_to_run(parsed)
|
|
803
1001
|
|
|
804
1002
|
break
|
|
805
1003
|
|
|
@@ -973,28 +1171,26 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
973
1171
|
|
|
974
1172
|
Options:
|
|
975
1173
|
--user Hide Robot's built-in variables (`${SUITE_NAME}`,
|
|
976
|
-
`${OUTPUT_DIR}`, `${TEMPDIR}`, …)
|
|
977
|
-
|
|
978
|
-
|
|
1174
|
+
`${OUTPUT_DIR}`, `${TEMPDIR}`, `${True}`, …) and the
|
|
1175
|
+
result variable `${_}` so the listing focuses on what the
|
|
1176
|
+
session or the suite defined. At a debug stop it filters
|
|
1177
|
+
every scope of the listing.
|
|
979
1178
|
"""
|
|
980
1179
|
if self.app is None:
|
|
981
1180
|
return
|
|
1181
|
+
only_user = "--user" in arg.split()
|
|
982
1182
|
if self._stop is not None:
|
|
983
|
-
self._show_frame_scopes()
|
|
1183
|
+
self._show_frame_scopes(only_user)
|
|
984
1184
|
return
|
|
985
1185
|
ctx = EXECUTION_CONTEXTS.current
|
|
986
1186
|
if ctx is None:
|
|
987
1187
|
self.app.echo("(no active context)")
|
|
988
1188
|
return
|
|
989
|
-
only_user = "--user" in arg.split()
|
|
990
1189
|
|
|
991
1190
|
rows: List[Tuple[str, str]] = []
|
|
992
1191
|
for decorated, value in ctx.variables.as_dict().items():
|
|
993
1192
|
name = str(decorated)
|
|
994
|
-
|
|
995
|
-
if len(name) > 2 and name[0] in "$@&%" and name[1] == "{" and name[-1] == "}":
|
|
996
|
-
bare = name[2:-1]
|
|
997
|
-
if only_user and _is_robot_internal(bare):
|
|
1193
|
+
if only_user and _is_robot_internal(name):
|
|
998
1194
|
continue
|
|
999
1195
|
try:
|
|
1000
1196
|
rep = repr(value)
|
|
@@ -1011,7 +1207,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1011
1207
|
for name, rep in rows:
|
|
1012
1208
|
self.app.echo(f" {name:<{name_w}} {rep}")
|
|
1013
1209
|
|
|
1014
|
-
def _show_frame_scopes(self) -> None:
|
|
1210
|
+
def _show_frame_scopes(self, only_user: bool) -> None:
|
|
1015
1211
|
"""`.vars` at a debug stop — the selected frame's scopes, via the controller."""
|
|
1016
1212
|
scopes = self._controller.get_scopes(self._selected_frame()) if self._controller is not None else []
|
|
1017
1213
|
if not scopes:
|
|
@@ -1019,9 +1215,10 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1019
1215
|
return
|
|
1020
1216
|
for scope in scopes:
|
|
1021
1217
|
self._echo(f"{scope.name}:")
|
|
1022
|
-
if not
|
|
1218
|
+
variables = [v for v in scope.variables if not (only_user and _is_robot_internal(v.name))]
|
|
1219
|
+
if not variables:
|
|
1023
1220
|
self._echo(" (none)")
|
|
1024
|
-
for var in
|
|
1221
|
+
for var in variables:
|
|
1025
1222
|
self._echo(f" {var.name} = {var.value}")
|
|
1026
1223
|
|
|
1027
1224
|
@dot_command("kw")
|
|
@@ -1076,8 +1273,16 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1076
1273
|
kw_name = getattr(runtime_kw, "name", name)
|
|
1077
1274
|
diag_kw = _diagnostics_keyword_doc(owner, is_resource, kw_name)
|
|
1078
1275
|
if diag_kw is not None:
|
|
1079
|
-
|
|
1080
|
-
|
|
1276
|
+
link_resolver = self._keyword_link_resolver(str(getattr(owner, "name", "")))
|
|
1277
|
+
return kw_name, diag_kw.to_markdown(header_level=1, link_resolver=link_resolver)
|
|
1278
|
+
return kw_name, _render_runtime_keyword_md(runtime_kw, kw_name, is_resource)
|
|
1279
|
+
|
|
1280
|
+
def _keyword_link_resolver(self, owner_name: str) -> Optional[LinkResolver]:
|
|
1281
|
+
"""Where references in a keyword page go. Nothing can be followed here,
|
|
1282
|
+
so they stay inline code; the prompt_toolkit backend overrides this to
|
|
1283
|
+
link to the pages of other keywords."""
|
|
1284
|
+
del owner_name
|
|
1285
|
+
return None
|
|
1081
1286
|
|
|
1082
1287
|
def _list_keywords(self, pattern: Optional[str]) -> None:
|
|
1083
1288
|
"""List loaded keywords grouped by owner, optionally filtered by a
|
|
@@ -1149,7 +1354,11 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1149
1354
|
self.app.echo(error or f"Could not load {arg!r}.")
|
|
1150
1355
|
return
|
|
1151
1356
|
|
|
1152
|
-
self.show_doc(
|
|
1357
|
+
self.show_doc(
|
|
1358
|
+
lib_doc.name,
|
|
1359
|
+
lib_doc.to_markdown(only_doc=False, header_level=0, link_resolver=anchor_link_resolver),
|
|
1360
|
+
outline=True,
|
|
1361
|
+
)
|
|
1153
1362
|
|
|
1154
1363
|
def _resolve_doc_target(self, arg: str) -> Tuple[Optional[LibraryDoc], Optional[str]]:
|
|
1155
1364
|
"""Build the `LibraryDoc` for an imported library or resource.
|
|
@@ -1193,9 +1402,11 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1193
1402
|
def _cwd(self, arg: str) -> None:
|
|
1194
1403
|
"""Show the current working directory.
|
|
1195
1404
|
|
|
1196
|
-
Print the
|
|
1197
|
-
|
|
1198
|
-
|
|
1405
|
+
Print the working directory of the REPL process — the project root.
|
|
1406
|
+
Relative file paths passed to keywords such as `File Should Exist`
|
|
1407
|
+
resolve against it. `${CURDIR}` and, on Robot Framework 7.4 and newer,
|
|
1408
|
+
relative imports use the directory of `--source` instead, or the
|
|
1409
|
+
directory the REPL was started from if `--source` isn't given.
|
|
1199
1410
|
"""
|
|
1200
1411
|
del arg
|
|
1201
1412
|
if self.app is None:
|
|
@@ -1207,32 +1418,48 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1207
1418
|
|
|
1208
1419
|
@dot_command("exit", "quit")
|
|
1209
1420
|
def _exit(self, arg: str) -> None:
|
|
1210
|
-
"""Exit the REPL.
|
|
1421
|
+
"""Exit the REPL: .exit [CODE]
|
|
1211
1422
|
|
|
1212
|
-
Leave the REPL.
|
|
1423
|
+
Leave the REPL. Without CODE this is the same as pressing Ctrl-D on
|
|
1424
|
+
an empty prompt. With CODE, `robotcode repl` exits with that exit code
|
|
1425
|
+
instead of the one it derives from the session status.
|
|
1213
1426
|
`.exit` and `.quit` are aliases. At a debug stop "exit" is ambiguous,
|
|
1214
1427
|
so it points you at the resume/abort commands instead of leaving.
|
|
1428
|
+
|
|
1429
|
+
Usage:
|
|
1430
|
+
.exit [CODE]
|
|
1431
|
+
|
|
1432
|
+
Examples:
|
|
1433
|
+
.exit
|
|
1434
|
+
.exit 3
|
|
1215
1435
|
"""
|
|
1216
|
-
del arg
|
|
1217
1436
|
if self._stop is not None:
|
|
1218
1437
|
self._echo("at a debug stop — use .continue or .detach to resume, or .abort to quit the run")
|
|
1219
1438
|
return
|
|
1439
|
+
if arg:
|
|
1440
|
+
try:
|
|
1441
|
+
self.exit_code = int(arg)
|
|
1442
|
+
except ValueError:
|
|
1443
|
+
self._echo("Usage: .exit [CODE]")
|
|
1444
|
+
return
|
|
1220
1445
|
raise EOFError
|
|
1221
1446
|
|
|
1222
1447
|
@dot_command("save")
|
|
1223
1448
|
def _save(self, arg: str) -> None:
|
|
1224
|
-
"""Save session as a .robot file: .save [-a] [-t NAME] FILENAME
|
|
1449
|
+
"""Save session as a .robot file: .save [-a] [-t NAME] [--keep-failed] FILENAME
|
|
1225
1450
|
|
|
1226
|
-
Save the current REPL session as a runnable `.robot` file.
|
|
1227
|
-
|
|
1228
|
-
skipped so the result
|
|
1451
|
+
Save the current REPL session as a runnable `.robot` file. Inputs
|
|
1452
|
+
with syntax errors are never exported. Inputs whose run failed or
|
|
1453
|
+
was skipped are left out too, so the result runs with
|
|
1454
|
+
`robot <filename>` the way the session did; `--keep-failed` keeps
|
|
1455
|
+
them.
|
|
1229
1456
|
|
|
1230
1457
|
Imports become a `*** Settings ***` section (so
|
|
1231
1458
|
`Import Library Collections` becomes `Library Collections`),
|
|
1232
1459
|
and everything else goes into a single `*** Test Cases ***` block.
|
|
1233
1460
|
|
|
1234
1461
|
Usage:
|
|
1235
|
-
.save [-a] [-t NAME] FILENAME
|
|
1462
|
+
.save [-a] [-t NAME] [--keep-failed] FILENAME
|
|
1236
1463
|
|
|
1237
1464
|
Options:
|
|
1238
1465
|
-a, --append Append to FILENAME instead of overwriting.
|
|
@@ -1243,6 +1470,8 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1243
1470
|
Override the default test-case name. By
|
|
1244
1471
|
default the test is called
|
|
1245
1472
|
`REPL Session <ISO-timestamp>`.
|
|
1473
|
+
--keep-failed Also export inputs whose run failed,
|
|
1474
|
+
e.g. to reproduce a failing check.
|
|
1246
1475
|
|
|
1247
1476
|
Examples:
|
|
1248
1477
|
.save scratch.robot
|
|
@@ -1255,13 +1484,17 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1255
1484
|
try:
|
|
1256
1485
|
opts = parser.parse_args(_split_command_args(arg))
|
|
1257
1486
|
except (argparse.ArgumentError, SystemExit, ValueError):
|
|
1258
|
-
self.app.echo("Usage: .save [-a] [-t NAME] FILENAME")
|
|
1487
|
+
self.app.echo("Usage: .save [-a] [-t NAME] [--keep-failed] FILENAME")
|
|
1259
1488
|
return
|
|
1260
1489
|
|
|
1261
|
-
|
|
1262
|
-
if not
|
|
1490
|
+
recorded: List[str] = list(self._session_lines or [])
|
|
1491
|
+
if not recorded:
|
|
1263
1492
|
self.app.echo("Nothing to save — the session has no recorded inputs yet.")
|
|
1264
1493
|
return
|
|
1494
|
+
lines = [line for i, line in enumerate(recorded) if opts.keep_failed or i not in self._failed_session_lines]
|
|
1495
|
+
if not lines:
|
|
1496
|
+
self.app.echo("Nothing to save — every recorded input failed; `--keep-failed` exports them anyway.")
|
|
1497
|
+
return
|
|
1265
1498
|
|
|
1266
1499
|
content = render_robot_file(lines, test_name=opts.test_name)
|
|
1267
1500
|
path = Path(opts.filename)
|
|
@@ -1280,7 +1513,11 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1280
1513
|
self.app.echo(f"Could not write {opts.filename}: {e}")
|
|
1281
1514
|
return
|
|
1282
1515
|
|
|
1283
|
-
|
|
1516
|
+
summary = f"{len(lines)} {'entry' if len(lines) == 1 else 'entries'}"
|
|
1517
|
+
left_out = len(recorded) - len(lines)
|
|
1518
|
+
if left_out:
|
|
1519
|
+
summary += f"; {left_out} failed {'input' if left_out == 1 else 'inputs'} left out, see --keep-failed"
|
|
1520
|
+
self.app.echo(f"Wrote {path} ({summary})")
|
|
1284
1521
|
|
|
1285
1522
|
# ------------------------------------------------------------------
|
|
1286
1523
|
# Debugger dot-commands — active while paused at a stop (`group=
|
|
@@ -1520,13 +1757,16 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1520
1757
|
|
|
1521
1758
|
@dot_command("set", group="Debugger")
|
|
1522
1759
|
def _set(self, arg: str) -> None:
|
|
1523
|
-
"""
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
the
|
|
1760
|
+
"""Change an existing scalar variable in the selected frame — `.set ${x} <value>`.
|
|
1761
|
+
|
|
1762
|
+
Only variables that already exist can be changed; create a new one with
|
|
1763
|
+
`${x}= Set Variable …` at the prompt. The value is
|
|
1764
|
+
variable-substituted and stored as a string: `.set ${name} hello` sets
|
|
1765
|
+
`hello`, and `.set ${count} 7` sets the string `7`. For other types,
|
|
1766
|
+
assign at the prompt, e.g. `${items}= Evaluate [1, 2]`. Only whole
|
|
1767
|
+
scalar variables (`${...}`) are supported — not list/dict variables
|
|
1768
|
+
(`@{...}`/`&{...}`) or item access (`${x}[0]`). The value lands in the
|
|
1769
|
+
selected frame's local scope.
|
|
1530
1770
|
"""
|
|
1531
1771
|
if not self._require_stop():
|
|
1532
1772
|
return
|
|
@@ -1574,7 +1814,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1574
1814
|
|
|
1575
1815
|
@dot_command("break", "b", group="Debugger")
|
|
1576
1816
|
def _break(self, arg: str) -> None:
|
|
1577
|
-
"""Add a breakpoint — `.break file:line` / `.break Keyword Name` / `.break <loc>, <condition>`."""
|
|
1817
|
+
"""Add a breakpoint — `.break file:line` / `.break Keyword Name or pattern` / `.break <loc>, <condition>`."""
|
|
1578
1818
|
self._add_breakpoint(arg, temporary=False)
|
|
1579
1819
|
|
|
1580
1820
|
@dot_command("tbreak", group="Debugger")
|
|
@@ -1605,7 +1845,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1605
1845
|
if bp.log_message is not None:
|
|
1606
1846
|
flags.append("logpoint")
|
|
1607
1847
|
if bp.commands:
|
|
1608
|
-
flags.append(f"{len(bp.commands)} cmds")
|
|
1848
|
+
flags.append(f"{len(bp.commands)} {'cmd' if len(bp.commands) == 1 else 'cmds'}")
|
|
1609
1849
|
suffix = (" " + ", ".join(flags)) if flags else ""
|
|
1610
1850
|
self._echo(f" #{bp.id} {bp.kind:<7} {loc}{suffix}")
|
|
1611
1851
|
filters = controller.exception_filters
|
|
@@ -1735,8 +1975,13 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
1735
1975
|
collected: List[str] = []
|
|
1736
1976
|
while True:
|
|
1737
1977
|
try:
|
|
1738
|
-
line = self.
|
|
1739
|
-
except
|
|
1978
|
+
line = self._read("(com) ")
|
|
1979
|
+
except KeyboardInterrupt:
|
|
1980
|
+
# Ctrl-C aborts the entry, as in pdb; the breakpoint keeps its commands.
|
|
1981
|
+
self._report_interrupt("(com) ")
|
|
1982
|
+
self._echo(f"breakpoint {bp.id}: commands unchanged")
|
|
1983
|
+
return
|
|
1984
|
+
except EOFError:
|
|
1740
1985
|
break
|
|
1741
1986
|
line = line.strip()
|
|
1742
1987
|
if line == "end":
|