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
robotcode/repl/_pt/lexer.py
CHANGED
|
@@ -37,6 +37,7 @@ from robot.api import get_tokens
|
|
|
37
37
|
from robotcode.robot.diagnostics.semantic_analyzer.enums import TokenKind
|
|
38
38
|
from robotcode.robot.diagnostics.semantic_analyzer.nodes import SemanticToken
|
|
39
39
|
from robotcode.robot.diagnostics.semantic_analyzer.variable_tokenizer import (
|
|
40
|
+
VariableSite,
|
|
40
41
|
build_variable_sub_tokens,
|
|
41
42
|
)
|
|
42
43
|
|
|
@@ -145,7 +146,9 @@ def _split_variable(value: str) -> Iterable[Tuple[str, str]]:
|
|
|
145
146
|
still renders (just without per-part colouring).
|
|
146
147
|
"""
|
|
147
148
|
try:
|
|
148
|
-
|
|
149
|
+
# The lexer cannot tell definitions from usages, so it splits like a
|
|
150
|
+
# definition to show the type hints of `VAR` and `FOR` variables.
|
|
151
|
+
sub_tokens = build_variable_sub_tokens(value, line=1, col_offset=0, site=VariableSite.DECLARATION)
|
|
149
152
|
except Exception:
|
|
150
153
|
yield (_VARIABLE_FALLBACK_STYLE, value)
|
|
151
154
|
return
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import contextlib
|
|
3
|
+
import os
|
|
3
4
|
import signal
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from datetime import datetime
|
|
5
8
|
from io import StringIO
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Protocol, Tuple, Union, cast
|
|
8
11
|
|
|
9
12
|
from robot.api import get_model
|
|
10
|
-
from robot.errors import ExecutionStatus
|
|
13
|
+
from robot.errors import ExecutionFailed, ExecutionPassed, ExecutionStatus
|
|
11
14
|
from robot.output import LOGGER
|
|
12
15
|
from robot.output import Message as OutputMessage
|
|
16
|
+
from robot.parsing.lexer.tokens import Token
|
|
17
|
+
from robot.parsing.model.blocks import For, If, Try, While
|
|
18
|
+
from robot.parsing.model.statements import Error, Statement
|
|
13
19
|
from robot.result import Keyword as ResultKeyword
|
|
14
20
|
from robot.running import Keyword, TestCase, TestSuite
|
|
15
21
|
from robot.running.context import EXECUTION_CONTEXTS
|
|
16
22
|
from robot.running.signalhandler import STOP_SIGNAL_MONITOR, _StopSignalMonitor
|
|
23
|
+
from robot.utils import normalize
|
|
17
24
|
|
|
18
25
|
from robotcode.robot.utils import RF_VERSION
|
|
19
26
|
from robotcode.robot.utils.ast import iter_nodes
|
|
@@ -54,13 +61,89 @@ def _register_signal_handler(self: Any, exsignum: Any) -> None:
|
|
|
54
61
|
pass
|
|
55
62
|
|
|
56
63
|
|
|
64
|
+
# Robot Framework's own handling, kept for the signals that end a run outside a REPL session (a robot-debug run).
|
|
65
|
+
_robot_monitor_call = _StopSignalMonitor.__call__
|
|
66
|
+
_robot_stop_execution_gracefully = _StopSignalMonitor._stop_execution_gracefully
|
|
67
|
+
|
|
68
|
+
# The interpreter of this process; the patched monitor hands the signals to it.
|
|
69
|
+
_signal_interpreter: Optional["BaseInterpreter"] = None
|
|
70
|
+
# Signals received so far that end the session or the run: SIGTERM, and SIGINT in a run without the
|
|
71
|
+
# debugger. Counted here: Robot Framework resets its count after every REPL statement.
|
|
72
|
+
_ending_signal_count = 0
|
|
73
|
+
# Set by such a signal outside a REPL session: stopping then ends the run as Robot Framework does.
|
|
74
|
+
_signal_ends_run = False
|
|
75
|
+
|
|
76
|
+
|
|
57
77
|
def _stop_signal_monitor_call(self: Any, signum: Any, frame: Any) -> None:
|
|
78
|
+
if signum == signal.SIGTERM:
|
|
79
|
+
_on_sigterm(self, signum, frame)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if _signal_interpreter is not None:
|
|
83
|
+
# SIGINT at a prompt ends the wait for input, as Ctrl-C does with prompt_toolkit.
|
|
84
|
+
_signal_interpreter._interrupt_waiting()
|
|
85
|
+
# With the debugger attached, SIGINT first pauses at the next keyword, as pdb does.
|
|
86
|
+
if _signal_interpreter._pause_on_interrupt():
|
|
87
|
+
return
|
|
88
|
+
# Without the debugger, SIGINT ends a robot-debug run as it ends a Robot Framework run.
|
|
89
|
+
if _signal_interpreter._interrupt_ends_run():
|
|
90
|
+
_count_ending_signal()
|
|
91
|
+
_end_run(self, signum, frame)
|
|
92
|
+
return
|
|
58
93
|
if self._running_keyword:
|
|
94
|
+
# SIGINT interrupts the running keyword; the session goes on.
|
|
59
95
|
self._stop_execution_gracefully()
|
|
60
96
|
|
|
61
97
|
|
|
98
|
+
def _count_ending_signal() -> None:
|
|
99
|
+
"""Count a signal that ends the session or the run; the second one ends the process at once."""
|
|
100
|
+
global _ending_signal_count
|
|
101
|
+
_ending_signal_count += 1
|
|
102
|
+
if _ending_signal_count > 1:
|
|
103
|
+
try:
|
|
104
|
+
if sys.__stderr__ is not None:
|
|
105
|
+
sys.__stderr__.write("Execution forcefully stopped.\n")
|
|
106
|
+
finally:
|
|
107
|
+
os._exit(253)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _on_sigterm(monitor: Any, signum: Any, frame: Any) -> None:
|
|
111
|
+
_count_ending_signal()
|
|
112
|
+
|
|
113
|
+
interpreter = _signal_interpreter
|
|
114
|
+
if interpreter is not None and interpreter._in_repl_run:
|
|
115
|
+
if interpreter._running_statement:
|
|
116
|
+
# As in a Robot Framework run, the next keyword that starts stops the statement;
|
|
117
|
+
# Robot Framework resets the count when the statement ends.
|
|
118
|
+
monitor._signal_count += 1
|
|
119
|
+
interpreter._terminate_session(monitor._running_keyword)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
_end_run(monitor, signum, frame)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _end_run(monitor: Any, signum: Any, frame: Any) -> None:
|
|
126
|
+
# A run, such as robot-debug: Robot Framework ends it, at once while a keyword runs
|
|
127
|
+
# (this raises), otherwise at the next keyword; the interpreter only stops waiting.
|
|
128
|
+
global _signal_ends_run
|
|
129
|
+
_signal_ends_run = True
|
|
130
|
+
interpreter = _signal_interpreter
|
|
131
|
+
if interpreter is not None:
|
|
132
|
+
interpreter._termination_requested = True
|
|
133
|
+
try:
|
|
134
|
+
_robot_monitor_call(monitor, signum, frame)
|
|
135
|
+
finally:
|
|
136
|
+
if interpreter is not None:
|
|
137
|
+
interpreter._end_waiting()
|
|
138
|
+
|
|
139
|
+
|
|
62
140
|
def _stop_signal_monitor_stop_execution_gracefully(self: Any) -> None:
|
|
63
|
-
|
|
141
|
+
if _signal_ends_run:
|
|
142
|
+
_robot_stop_execution_gracefully(self)
|
|
143
|
+
elif _ending_signal_count:
|
|
144
|
+
raise ExecutionInterrupted("Execution terminated by signal")
|
|
145
|
+
else:
|
|
146
|
+
raise ExecutionInterrupted("Execution interrupted")
|
|
64
147
|
|
|
65
148
|
|
|
66
149
|
_patched = False
|
|
@@ -80,7 +163,14 @@ def _patch() -> None:
|
|
|
80
163
|
if RF_VERSION >= (7, 0):
|
|
81
164
|
|
|
82
165
|
def _run_keyword(kw: Keyword, context: Any) -> Any:
|
|
83
|
-
|
|
166
|
+
# A keyword typed at a stop becomes a child of the running step. At a stop
|
|
167
|
+
# where no step runs any more, such as the end of a test, it goes to the
|
|
168
|
+
# test, and without a test to a detached result.
|
|
169
|
+
if context.steps:
|
|
170
|
+
parent = context.steps[-1][1]
|
|
171
|
+
else:
|
|
172
|
+
parent = context.test if context.test is not None else ResultKeyword()
|
|
173
|
+
return kw.run(parent, context)
|
|
84
174
|
|
|
85
175
|
else:
|
|
86
176
|
|
|
@@ -109,6 +199,80 @@ def result_qualified_name(result: Any) -> Optional[str]:
|
|
|
109
199
|
return _keyword_qualified_name(result) if isinstance(result, ResultKeyword) else None
|
|
110
200
|
|
|
111
201
|
|
|
202
|
+
# The definition name of a keyword with embedded arguments (`Select ${animal} From List`),
|
|
203
|
+
# bare and qualified with its owner. Robot Framework sets it before the keyword starts, as
|
|
204
|
+
# `source_name` from version 7 on and as `sourcename` before; other keywords have none.
|
|
205
|
+
if RF_VERSION >= (7, 0):
|
|
206
|
+
|
|
207
|
+
def _keyword_definition_names(keyword: ResultKeyword) -> Tuple[str, ...]:
|
|
208
|
+
if not keyword.source_name:
|
|
209
|
+
return ()
|
|
210
|
+
if not keyword.owner:
|
|
211
|
+
return (keyword.source_name,)
|
|
212
|
+
return (keyword.source_name, f"{keyword.owner}.{keyword.source_name}")
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
|
|
216
|
+
def _keyword_definition_names(keyword: ResultKeyword) -> Tuple[str, ...]:
|
|
217
|
+
if not keyword.sourcename:
|
|
218
|
+
return ()
|
|
219
|
+
if not keyword.libname:
|
|
220
|
+
return (keyword.sourcename,)
|
|
221
|
+
return (keyword.sourcename, f"{keyword.libname}.{keyword.sourcename}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def result_definition_names(result: Any) -> Tuple[str, ...]:
|
|
225
|
+
return _keyword_definition_names(result) if isinstance(result, ResultKeyword) else ()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Blocks that stay open until their END. Their branches (ELSE, EXCEPT, …) are blocks of
|
|
229
|
+
# the same classes with another header and never get an END of their own.
|
|
230
|
+
if RF_VERSION >= (7, 2):
|
|
231
|
+
from robot.parsing.model.blocks import Group
|
|
232
|
+
|
|
233
|
+
_END_BLOCKS: Tuple[Any, ...] = (For, While, If, Try, Group)
|
|
234
|
+
_BLOCK_OPENERS = frozenset({Token.FOR, Token.WHILE, Token.IF, Token.TRY, Token.GROUP})
|
|
235
|
+
else:
|
|
236
|
+
_END_BLOCKS = (For, While, If, Try)
|
|
237
|
+
_BLOCK_OPENERS = frozenset({Token.FOR, Token.WHILE, Token.IF, Token.TRY})
|
|
238
|
+
|
|
239
|
+
# A REPL input runs as the body of the session test, so it can't have settings: neither
|
|
240
|
+
# test settings nor the keyword settings that a test reports as not allowed.
|
|
241
|
+
_SETTING_TYPES = frozenset(
|
|
242
|
+
{Token.DOCUMENTATION, Token.TAGS, Token.SETUP, Token.TEARDOWN, Token.TEMPLATE, Token.TIMEOUT}
|
|
243
|
+
)
|
|
244
|
+
_SETTING_NAMES = frozenset({"documentation", "tags", "setup", "teardown", "template", "timeout", "arguments", "return"})
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _mark_settings_as_errors(test: Any) -> None:
|
|
248
|
+
"""Mark the settings of a REPL input as errors, as Robot Framework's lexer marks a
|
|
249
|
+
setting a test can't have, so that Robot Framework reports them like a non-existing
|
|
250
|
+
setting."""
|
|
251
|
+
for i, node in enumerate(test.body):
|
|
252
|
+
if not isinstance(node, Statement):
|
|
253
|
+
continue
|
|
254
|
+
if node.type in _SETTING_TYPES:
|
|
255
|
+
node = test.body[i] = Error(
|
|
256
|
+
[Token(Token.ERROR, t.value, t.lineno, t.col_offset) if t.type == node.type else t for t in node.tokens]
|
|
257
|
+
)
|
|
258
|
+
if isinstance(node, Error):
|
|
259
|
+
for token in node.get_tokens(Token.ERROR):
|
|
260
|
+
name = token.value[1:-1].strip() if token.value.startswith("[") and token.value.endswith("]") else ""
|
|
261
|
+
if normalize(name) in _SETTING_NAMES:
|
|
262
|
+
token.error = f"Setting '{name}' is not allowed in the REPL."
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass
|
|
266
|
+
class ReplInput:
|
|
267
|
+
"""REPL input parsed as a test body by `BaseInterpreter.parse_input`."""
|
|
268
|
+
|
|
269
|
+
test: TestCase
|
|
270
|
+
# The parse errors of the input.
|
|
271
|
+
errors: List[str]
|
|
272
|
+
# True while a block of the input lacks its END, so more lines can complete it.
|
|
273
|
+
incomplete: bool
|
|
274
|
+
|
|
275
|
+
|
|
112
276
|
if RF_VERSION < (7, 0):
|
|
113
277
|
|
|
114
278
|
class InterpreterLogger:
|
|
@@ -231,9 +395,24 @@ else:
|
|
|
231
395
|
self.interpreter._notify_end_suite(data, result)
|
|
232
396
|
|
|
233
397
|
|
|
398
|
+
# The interpreter whose prompt loop started last. `_maybe_trigger_repl` sets it when
|
|
399
|
+
# the `repl` marker keyword starts; the marker's body takes it once the loop has returned.
|
|
400
|
+
_active_interpreter: Optional["BaseInterpreter"] = None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def take_active_interpreter() -> Optional["BaseInterpreter"]:
|
|
404
|
+
"""Return the interpreter whose prompt loop started last and forget it, so a later
|
|
405
|
+
session in the same process never sees it."""
|
|
406
|
+
global _active_interpreter
|
|
407
|
+
interpreter, _active_interpreter = _active_interpreter, None
|
|
408
|
+
return interpreter
|
|
409
|
+
|
|
410
|
+
|
|
234
411
|
class BaseInterpreter(abc.ABC):
|
|
235
412
|
def __init__(self) -> None:
|
|
236
413
|
_patch()
|
|
414
|
+
global _signal_interpreter
|
|
415
|
+
_signal_interpreter = self
|
|
237
416
|
|
|
238
417
|
self._logger = InterpreterLogger(self)
|
|
239
418
|
LOGGER.register_logger(self._logger)
|
|
@@ -248,6 +427,17 @@ class BaseInterpreter(abc.ABC):
|
|
|
248
427
|
# The interpreter's own start_keyword/end_keyword hooks stay the primary
|
|
249
428
|
# consumer; observers run alongside them.
|
|
250
429
|
self._observers: List["ExecutionObserver"] = []
|
|
430
|
+
# Unhandled failures of the prompt loop, recorded only while `record_failures`
|
|
431
|
+
# is set (by `robotcode repl`). The `repl` marker keyword fails the session test
|
|
432
|
+
# with them.
|
|
433
|
+
self.record_failures = False
|
|
434
|
+
self.failures: List[ExecutionFailed] = []
|
|
435
|
+
# The code given to `.exit`/`.quit`, None without one.
|
|
436
|
+
self.exit_code: Optional[int] = None
|
|
437
|
+
# Set by a signal that ends the session or the run.
|
|
438
|
+
self._termination_requested = False
|
|
439
|
+
# Set while a statement of the input runs: SIGTERM then stops it at its next keyword.
|
|
440
|
+
self._running_statement = False
|
|
251
441
|
|
|
252
442
|
@property
|
|
253
443
|
def curdir(self) -> Path:
|
|
@@ -269,7 +459,7 @@ class BaseInterpreter(abc.ABC):
|
|
|
269
459
|
|
|
270
460
|
return []
|
|
271
461
|
|
|
272
|
-
def
|
|
462
|
+
def _parse_test_body(self, command: str) -> Tuple[Any, TestCase]:
|
|
273
463
|
suite_str = (
|
|
274
464
|
"*** Test Cases ***\nDummyTestCase423141592653589793\n "
|
|
275
465
|
+ ("\n ".join(command.split("\n")) if "\n" in command else command)
|
|
@@ -278,16 +468,45 @@ class BaseInterpreter(abc.ABC):
|
|
|
278
468
|
with StringIO(suite_str) as source:
|
|
279
469
|
model = get_model(source, curdir=str(self.curdir).replace("\\", "\\\\"))
|
|
280
470
|
|
|
281
|
-
|
|
471
|
+
# RF < 6.1 expects a `str` source; RF 6.1+ converts it to a `Path` itself.
|
|
472
|
+
model.source = str(self.source) if self.source is not None else None
|
|
473
|
+
_mark_settings_as_errors(model.sections[0].body[0])
|
|
282
474
|
|
|
283
475
|
suite: TestSuite = TestSuite.from_model(model)
|
|
284
476
|
|
|
477
|
+
return model, cast(TestCase, suite.tests[0])
|
|
478
|
+
|
|
479
|
+
def get_test_body_from_string(self, command: str) -> Tuple[TestCase, List[str]]:
|
|
480
|
+
model, test = self._parse_test_body(command)
|
|
481
|
+
|
|
285
482
|
errors: List[str] = []
|
|
286
483
|
|
|
287
484
|
for node in iter_nodes(model):
|
|
288
485
|
errors.extend(self.check_for_errors(node))
|
|
289
486
|
|
|
290
|
-
return
|
|
487
|
+
return test, errors
|
|
488
|
+
|
|
489
|
+
def parse_input(self, text: str) -> ReplInput:
|
|
490
|
+
"""Parse REPL input as a test body without raising on errors.
|
|
491
|
+
|
|
492
|
+
Unlike `get_test_body_from_string`, whose `SyntaxError` for token errors
|
|
493
|
+
`repl_server` relies on, invalid statements are left to Robot Framework,
|
|
494
|
+
which fails them when execution reaches them.
|
|
495
|
+
"""
|
|
496
|
+
model, test = self._parse_test_body(text)
|
|
497
|
+
|
|
498
|
+
errors: List[str] = []
|
|
499
|
+
incomplete = False
|
|
500
|
+
|
|
501
|
+
for node in iter_nodes(model):
|
|
502
|
+
try:
|
|
503
|
+
errors.extend(self.check_for_errors(node))
|
|
504
|
+
except SyntaxError as e:
|
|
505
|
+
errors.append(str(e))
|
|
506
|
+
if isinstance(node, _END_BLOCKS) and node.header.type in _BLOCK_OPENERS and node.end is None:
|
|
507
|
+
incomplete = True
|
|
508
|
+
|
|
509
|
+
return ReplInput(test=test, errors=errors, incomplete=incomplete)
|
|
291
510
|
|
|
292
511
|
@abc.abstractmethod
|
|
293
512
|
def get_input(self) -> Iterator[Optional[Keyword]]: ...
|
|
@@ -302,17 +521,50 @@ class BaseInterpreter(abc.ABC):
|
|
|
302
521
|
except ExecutionStatus:
|
|
303
522
|
raise
|
|
304
523
|
except BaseException as e:
|
|
305
|
-
|
|
524
|
+
message = f"{type(e)}: {e}"
|
|
525
|
+
self.log_message(message, "ERROR", timestamp=datetime.now()) # noqa: DTZ005
|
|
526
|
+
self._record_failure(ExecutionFailed(message))
|
|
527
|
+
|
|
528
|
+
def _record_failure(self, error: ExecutionFailed) -> None:
|
|
529
|
+
if self.record_failures:
|
|
530
|
+
# Split like Robot Framework's body runner does, so the combined message
|
|
531
|
+
# numbers each failure.
|
|
532
|
+
self.failures.extend(error.get_errors())
|
|
306
533
|
|
|
307
534
|
def interrupt(self) -> None:
|
|
308
535
|
signal.raise_signal(signal.SIGINT)
|
|
309
536
|
|
|
537
|
+
def _terminate_session(self, keyword_running: bool) -> None:
|
|
538
|
+
"""End the REPL session on SIGTERM, from the signal handler: a running keyword is
|
|
539
|
+
interrupted, then the session ends as at the end of the input, with exit code 253."""
|
|
540
|
+
self._termination_requested = True
|
|
541
|
+
self.exit_code = 253
|
|
542
|
+
self._end_waiting()
|
|
543
|
+
if keyword_running:
|
|
544
|
+
raise ExecutionInterrupted("Execution terminated by signal")
|
|
545
|
+
|
|
546
|
+
def _end_waiting(self) -> None:
|
|
547
|
+
"""Stop waiting for input, from the signal handler. Each interpreter knows how it waits."""
|
|
548
|
+
|
|
549
|
+
def _interrupt_waiting(self) -> None:
|
|
550
|
+
"""Interrupt a wait for input after SIGINT, from the signal handler. Each interpreter knows how it waits."""
|
|
551
|
+
|
|
552
|
+
def _pause_on_interrupt(self) -> bool:
|
|
553
|
+
"""Pause at the next keyword after SIGINT instead of interrupting the keyword that runs, from the signal
|
|
554
|
+
handler. Returns whether it does; only an interpreter with a debugger can pause."""
|
|
555
|
+
return False
|
|
556
|
+
|
|
557
|
+
def _interrupt_ends_run(self) -> bool:
|
|
558
|
+
"""Whether SIGINT ends the run as Robot Framework ends one, from the signal handler. Only an interpreter that
|
|
559
|
+
runs a suite, not a session, can end a run this way."""
|
|
560
|
+
return False
|
|
561
|
+
|
|
310
562
|
def run(self) -> Any:
|
|
311
563
|
self._logger.enabled = True
|
|
312
564
|
|
|
313
565
|
has_input = True
|
|
314
566
|
try:
|
|
315
|
-
while has_input:
|
|
567
|
+
while has_input and not self._termination_requested:
|
|
316
568
|
try:
|
|
317
569
|
self.run_input()
|
|
318
570
|
except EOFError:
|
|
@@ -321,19 +573,33 @@ class BaseInterpreter(abc.ABC):
|
|
|
321
573
|
break
|
|
322
574
|
except ExecutionInterrupted as e:
|
|
323
575
|
self.log_message(str(e), "ERROR", timestamp=datetime.now()) # noqa: DTZ005
|
|
324
|
-
|
|
325
|
-
|
|
576
|
+
self._record_failure(ExecutionFailed(str(e)))
|
|
577
|
+
except ExecutionPassed as e:
|
|
578
|
+
# Failures continued before `Pass Execution` still fail a test in Robot Framework.
|
|
579
|
+
if e.earlier_failures:
|
|
580
|
+
self._record_failure(e.earlier_failures)
|
|
581
|
+
except ExecutionFailed as e:
|
|
582
|
+
# As in Robot Framework, a skip counts as skipped even when failures
|
|
583
|
+
# were continued before it.
|
|
584
|
+
if not e.skip:
|
|
585
|
+
self._record_failure(e)
|
|
326
586
|
except BaseException as e:
|
|
327
587
|
self.log_message(str(e), "ERROR", timestamp=datetime.now()) # noqa: DTZ005
|
|
588
|
+
self._record_failure(ExecutionFailed(str(e)))
|
|
328
589
|
finally:
|
|
329
590
|
self._logger.enabled = False
|
|
330
591
|
|
|
331
592
|
def run_input(self) -> None:
|
|
332
593
|
for kw in self.get_input():
|
|
333
594
|
with STOP_SIGNAL_MONITOR:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
595
|
+
self._running_statement = True
|
|
596
|
+
try:
|
|
597
|
+
# Checked after marking the statement: a SIGTERM in between can't be missed.
|
|
598
|
+
if kw is None or self._termination_requested:
|
|
599
|
+
break
|
|
600
|
+
self.set_last_result(self.run_keyword(kw))
|
|
601
|
+
finally:
|
|
602
|
+
self._running_statement = False
|
|
337
603
|
|
|
338
604
|
def set_last_result(self, result: Any) -> None:
|
|
339
605
|
self.last_result = result
|
|
@@ -444,6 +710,8 @@ class BaseInterpreter(abc.ABC):
|
|
|
444
710
|
"""
|
|
445
711
|
if self._in_repl_run or not self._is_repl_marker(result):
|
|
446
712
|
return False
|
|
713
|
+
global _active_interpreter
|
|
714
|
+
_active_interpreter = self
|
|
447
715
|
self._in_repl_run = True
|
|
448
716
|
try:
|
|
449
717
|
self.run()
|
robotcode/repl/cli.py
CHANGED
|
@@ -9,6 +9,7 @@ from robotcode.plugin import Application, pass_application
|
|
|
9
9
|
from robotcode.plugin._agent_detection import is_running_in_ai_agent
|
|
10
10
|
from robotcode.plugin.click_helper.aliases import AliasedCommand
|
|
11
11
|
from robotcode.plugin.click_helper.types import add_options
|
|
12
|
+
from robotcode.plugin.click_helper.wrappable import wrappable
|
|
12
13
|
from robotcode.runner.cli.robot import ROBOT_OPTIONS, ROBOT_VERSION_OPTIONS
|
|
13
14
|
|
|
14
15
|
from .__version__ import __version__
|
|
@@ -16,7 +17,7 @@ from ._backends import BACKEND_CHOICES
|
|
|
16
17
|
from ._debug import DebugController, DebugTerminated
|
|
17
18
|
from .console_interpreter import _LINE_BREAK_RE, ConsoleInterpreter
|
|
18
19
|
from .prompt_toolkit_interpreter import PromptToolkitConsoleInterpreter
|
|
19
|
-
from .run import run_repl
|
|
20
|
+
from .run import ReplResult, run_repl
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _pick_interpreter(
|
|
@@ -78,6 +79,8 @@ def _invoke_runner(
|
|
|
78
79
|
ctx: click.Context,
|
|
79
80
|
by_longname: Tuple[str, ...],
|
|
80
81
|
exclude_by_longname: Tuple[str, ...],
|
|
82
|
+
by_test_metadata: Tuple[str, ...],
|
|
83
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
81
84
|
robot_options_and_args: Tuple[str, ...],
|
|
82
85
|
) -> None:
|
|
83
86
|
"""Run the real suite via the normal `robotcode robot` runner."""
|
|
@@ -88,6 +91,10 @@ def _invoke_runner(
|
|
|
88
91
|
forwarded += ["--by-longname", value]
|
|
89
92
|
for value in exclude_by_longname:
|
|
90
93
|
forwarded += ["--exclude-by-longname", value]
|
|
94
|
+
for value in by_test_metadata:
|
|
95
|
+
forwarded += ["--by-test-metadata", value]
|
|
96
|
+
for value in exclude_by_test_metadata:
|
|
97
|
+
forwarded += ["--exclude-by-test-metadata", value]
|
|
91
98
|
forwarded += list(robot_options_and_args)
|
|
92
99
|
|
|
93
100
|
robot_ctx = robot_command.make_context("robot", forwarded, parent=ctx)
|
|
@@ -106,6 +113,21 @@ def _is_interactive_stdin() -> bool:
|
|
|
106
113
|
return False
|
|
107
114
|
|
|
108
115
|
|
|
116
|
+
def _session_exit_code(result: ReplResult, *, statusrc: Optional[bool], interactive: bool) -> int:
|
|
117
|
+
"""The exit code of a `repl` session.
|
|
118
|
+
|
|
119
|
+
A code given to `.exit`/`.quit` wins. Otherwise Robot's return code, which
|
|
120
|
+
already honors `statusrc` from the configuration, is used when `--statusrc`/
|
|
121
|
+
`--nostatusrc` was given or the session is not interactive; an interactive
|
|
122
|
+
session exits with 0.
|
|
123
|
+
"""
|
|
124
|
+
if result.exit_code is not None:
|
|
125
|
+
return result.exit_code
|
|
126
|
+
if statusrc is not None or not interactive:
|
|
127
|
+
return result.return_code
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
109
131
|
def _resolve_backend(plain: bool, backend: str) -> str:
|
|
110
132
|
"""The effective input backend.
|
|
111
133
|
|
|
@@ -234,8 +256,8 @@ REPL_BASE_OPTIONS = [
|
|
|
234
256
|
),
|
|
235
257
|
]
|
|
236
258
|
|
|
237
|
-
#
|
|
238
|
-
|
|
259
|
+
# Variables and the python path, shared by `repl` and `doc`.
|
|
260
|
+
VARIABLE_AND_PATH_OPTIONS = [
|
|
239
261
|
click.option(
|
|
240
262
|
"-v",
|
|
241
263
|
"--variable",
|
|
@@ -261,6 +283,11 @@ SHELL_OPTIONS = [
|
|
|
261
283
|
help="Additional locations where to search test libraries"
|
|
262
284
|
" and other extensions when they are imported. See `robot --pythonpath` option.",
|
|
263
285
|
),
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
# `repl`-only options (interactive session setup + snippet files).
|
|
289
|
+
SHELL_OPTIONS = [
|
|
290
|
+
*VARIABLE_AND_PATH_OPTIONS,
|
|
264
291
|
click.option(
|
|
265
292
|
"-k",
|
|
266
293
|
"--show-keywords",
|
|
@@ -315,11 +342,20 @@ SHELL_OPTIONS = [
|
|
|
315
342
|
"--source",
|
|
316
343
|
type=click.Path(path_type=Path),
|
|
317
344
|
metavar="FILE",
|
|
318
|
-
help="
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"need to exist.",
|
|
345
|
+
help="Run the session as if its keywords were written in FILE: `${CURDIR}` is the "
|
|
346
|
+
"directory of FILE, `${SUITE SOURCE}` is FILE and, on Robot Framework 7.4 and newer, "
|
|
347
|
+
"relative paths in `Import Resource`, `Import Library`, and `Import Variables` resolve "
|
|
348
|
+
"against that directory. A relative FILE is resolved against the directory the command "
|
|
349
|
+
"is started from. The file itself is never read or written, so the path doesn't need to exist.",
|
|
350
|
+
),
|
|
351
|
+
click.option(
|
|
352
|
+
"--statusrc/--nostatusrc",
|
|
353
|
+
default=None,
|
|
354
|
+
help="Exit with 1 if a statement failed without being handled, otherwise 0, in any session "
|
|
355
|
+
"(`--statusrc`), or exit with 0 regardless of failures (`--nostatusrc`). Both override `no-status-rc` "
|
|
356
|
+
"from the configuration. Without either option only non-interactive sessions (piped input, or FILES "
|
|
357
|
+
"without `--inspect`) exit with 1 after such a failure, unless the configuration switches this off. "
|
|
358
|
+
"A code given to `.exit` takes precedence.",
|
|
323
359
|
),
|
|
324
360
|
click.argument(
|
|
325
361
|
"files",
|
|
@@ -349,7 +385,8 @@ DEBUG_OPTIONS = [
|
|
|
349
385
|
"break_at",
|
|
350
386
|
metavar="LOCATION",
|
|
351
387
|
multiple=True,
|
|
352
|
-
help="Break at LOCATION — a `file:line
|
|
388
|
+
help="Break at LOCATION — a `file:line`, or a keyword name or glob pattern "
|
|
389
|
+
"matched like Robot Framework. Repeatable.",
|
|
353
390
|
),
|
|
354
391
|
click.option(
|
|
355
392
|
"--break-on-exception/--no-break-on-exception",
|
|
@@ -379,6 +416,7 @@ DEBUG_OPTIONS = [
|
|
|
379
416
|
]
|
|
380
417
|
|
|
381
418
|
|
|
419
|
+
@wrappable
|
|
382
420
|
@click.command(cls=AliasedCommand, aliases=["shell"], add_help_option=True)
|
|
383
421
|
@add_options(*REPL_BASE_OPTIONS)
|
|
384
422
|
@add_options(*SHELL_OPTIONS)
|
|
@@ -401,6 +439,7 @@ def repl(
|
|
|
401
439
|
log: Optional[str],
|
|
402
440
|
xunit: Optional[str],
|
|
403
441
|
source: Optional[Path],
|
|
442
|
+
statusrc: Optional[bool],
|
|
404
443
|
files: Tuple[Path, ...],
|
|
405
444
|
debugger_attached: Optional[bool],
|
|
406
445
|
break_at: Tuple[str, ...],
|
|
@@ -439,6 +478,9 @@ def repl(
|
|
|
439
478
|
no_history=no_history,
|
|
440
479
|
backend=backend,
|
|
441
480
|
)
|
|
481
|
+
# Only `repl` fails its session test on unhandled failures — not `robot-debug`,
|
|
482
|
+
# which builds its interpreter the same way.
|
|
483
|
+
interpreter.record_failures = True
|
|
442
484
|
# The debugger stays wired for the whole interactive session and is torn down
|
|
443
485
|
# with the interpreter when the session ends — so, unlike `robot`, there is no
|
|
444
486
|
# per-run unregister here. `attached` decides whether it actually pauses.
|
|
@@ -452,7 +494,7 @@ def repl(
|
|
|
452
494
|
break_on_failed_suite=break_on_failed_suite,
|
|
453
495
|
)
|
|
454
496
|
|
|
455
|
-
run_repl(
|
|
497
|
+
result = run_repl(
|
|
456
498
|
interpreter=interpreter,
|
|
457
499
|
app=app,
|
|
458
500
|
variable=variable,
|
|
@@ -465,9 +507,15 @@ def repl(
|
|
|
465
507
|
xunit=xunit,
|
|
466
508
|
source=source,
|
|
467
509
|
files=files,
|
|
510
|
+
statusrc=statusrc,
|
|
468
511
|
)
|
|
469
512
|
|
|
513
|
+
# Script files without `--inspect` end on their own, like piped input.
|
|
514
|
+
interactive = _is_interactive_stdin() and (not files or inspect)
|
|
515
|
+
app.exit(_session_exit_code(result, statusrc=statusrc, interactive=interactive))
|
|
516
|
+
|
|
470
517
|
|
|
518
|
+
@wrappable
|
|
471
519
|
@click.command(
|
|
472
520
|
cls=AliasedCommand,
|
|
473
521
|
aliases=["run-debug"],
|
|
@@ -476,13 +524,13 @@ def repl(
|
|
|
476
524
|
epilog="Use `-- --help` to see `robot` help.",
|
|
477
525
|
)
|
|
478
526
|
@add_options(*REPL_BASE_OPTIONS)
|
|
479
|
-
@add_options(*(ROBOT_OPTIONS
|
|
527
|
+
@add_options(*(option for option in ROBOT_OPTIONS if option not in ROBOT_VERSION_OPTIONS))
|
|
480
528
|
@add_options(*DEBUG_OPTIONS)
|
|
481
529
|
@click.option(
|
|
482
530
|
"--stop-on-entry",
|
|
483
531
|
is_flag=True,
|
|
484
532
|
default=False,
|
|
485
|
-
help="Break at the very first
|
|
533
|
+
help="Break at the very first step.",
|
|
486
534
|
)
|
|
487
535
|
@add_options(*ROBOT_VERSION_OPTIONS)
|
|
488
536
|
@pass_application
|
|
@@ -492,6 +540,8 @@ def robot_debug(
|
|
|
492
540
|
app: Application,
|
|
493
541
|
by_longname: Tuple[str, ...],
|
|
494
542
|
exclude_by_longname: Tuple[str, ...],
|
|
543
|
+
by_test_metadata: Tuple[str, ...],
|
|
544
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
495
545
|
robot_options_and_args: Tuple[str, ...],
|
|
496
546
|
no_history: bool,
|
|
497
547
|
backend: str,
|
|
@@ -537,7 +587,14 @@ def robot_debug(
|
|
|
537
587
|
# default (or the user's `--console`), not forced.
|
|
538
588
|
try:
|
|
539
589
|
with interpreter.forward_events(echo_messages=False):
|
|
540
|
-
_invoke_runner(
|
|
590
|
+
_invoke_runner(
|
|
591
|
+
ctx,
|
|
592
|
+
by_longname,
|
|
593
|
+
exclude_by_longname,
|
|
594
|
+
by_test_metadata,
|
|
595
|
+
exclude_by_test_metadata,
|
|
596
|
+
robot_options_and_args,
|
|
597
|
+
)
|
|
541
598
|
except DebugTerminated:
|
|
542
599
|
app.echo("Debugger: run terminated.")
|
|
543
600
|
finally:
|