robotcode-repl 2.9.0.dev87__tar.gz → 2.9.0.dev124__tar.gz

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.
Files changed (33) hide show
  1. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/PKG-INFO +2 -2
  2. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/pyproject.toml +1 -1
  3. robotcode_repl-2.9.0.dev124/src/robotcode/repl/__version__.py +1 -0
  4. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_debug/controller.py +50 -17
  5. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_debug/types.py +9 -1
  6. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/doc_viewer.py +10 -5
  7. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_session.py +5 -1
  8. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/base_interpreter.py +2 -0
  9. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/cli.py +17 -10
  10. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/console_interpreter.py +60 -48
  11. robotcode_repl-2.9.0.dev87/src/robotcode/repl/__version__.py +0 -1
  12. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/.gitignore +0 -0
  13. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/README.md +0 -0
  14. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/Repl/__init__.py +0 -0
  15. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/Repl/repl.py +0 -0
  16. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/__init__.py +0 -0
  17. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_backends.py +0 -0
  18. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_debug/__init__.py +0 -0
  19. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_indent.py +0 -0
  20. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_keyword_lookup.py +0 -0
  21. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_progress.py +0 -0
  22. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/__init__.py +0 -0
  23. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/completion.py +0 -0
  24. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/components.py +0 -0
  25. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/history.py +0 -0
  26. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_pt/lexer.py +0 -0
  27. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/_session_export.py +0 -0
  28. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/doc_cli.py +0 -0
  29. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/hooks.py +0 -0
  30. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/prompt_toolkit_interpreter.py +0 -0
  31. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/py.typed +0 -0
  32. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/run.py +0 -0
  33. {robotcode_repl-2.9.0.dev87 → robotcode_repl-2.9.0.dev124}/src/robotcode/repl/session_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: robotcode-repl
3
- Version: 2.9.0-dev.87
3
+ Version: 2.9.0-dev.124
4
4
  Summary: RobotCode REPL for Robot Framework
5
5
  Project-URL: Homepage, https://robotcode.io
6
6
  Project-URL: Donate, https://opencollective.com/robotcode
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: >=3.10
29
29
  Requires-Dist: prompt-toolkit!=3.0.52
30
- Requires-Dist: robotcode-runner==2.9.0-dev.87
30
+ Requires-Dist: robotcode-runner==2.9.0-dev.124
31
31
  Description-Content-Type: text/markdown
32
32
 
33
33
  # robotcode-repl
@@ -29,7 +29,7 @@ classifiers = [
29
29
  ]
30
30
  dynamic = ["version"]
31
31
  dependencies = [
32
- "robotcode-runner==2.9.0-dev.87",
32
+ "robotcode-runner==2.9.0-dev.124",
33
33
  "prompt_toolkit!=3.0.52"
34
34
  ]
35
35
 
@@ -0,0 +1 @@
1
+ __version__ = "2.9.0-dev.124"
@@ -121,6 +121,22 @@ def _normalize_source(source: str) -> str:
121
121
  return str(normalized_path(Path(source)))
122
122
 
123
123
 
124
+ def _breakpoint_source(source: str) -> str:
125
+ """What a line breakpoint matches by: a path with a root or a drive, or one that leaves the folder with `..`,
126
+ as an absolute path; any other as it is, see `_source_matches`."""
127
+ path = Path(source)
128
+ return _normalize_source(source) if path.anchor or ".." in path.parts else str(path)
129
+
130
+
131
+ def _source_matches(source: str, frame_source: str) -> bool:
132
+ """Whether a line breakpoint means the file of a frame: an absolute path exactly, any other by the
133
+ end of the path, as in gdb, so that `login.robot` finds `tests/login.robot`."""
134
+ given, actual = Path(source), Path(frame_source)
135
+ if given.is_absolute():
136
+ return given == actual
137
+ return Path(*actual.parts[-len(given.parts) :]) == given
138
+
139
+
124
140
  class DebugController:
125
141
  def __init__(self, frontend: Optional[Frontend] = None) -> None:
126
142
  self._stack: List[StackFrame] = []
@@ -159,6 +175,9 @@ class DebugController:
159
175
  # breakpoints/filters are kept so re-attaching resumes the same setup.
160
176
  self._detached = False
161
177
 
178
+ # set for a run that goes through a suite: at its end, say which breakpoints no keyword reached.
179
+ self._warn_about_unreached = False
180
+
162
181
  # --- configuration ------------------------------------------------------
163
182
 
164
183
  def set_frontend(self, frontend: Optional[Frontend]) -> None:
@@ -181,7 +200,8 @@ class DebugController:
181
200
  ) -> Breakpoint:
182
201
  return self._add_breakpoint(
183
202
  kind="line",
184
- source=_normalize_source(source),
203
+ source=_breakpoint_source(source),
204
+ location=source,
185
205
  line=line,
186
206
  condition=condition,
187
207
  log_message=log_message,
@@ -202,7 +222,7 @@ class DebugController:
202
222
 
203
223
  def set_line_breakpoints(self, source: str, lines: List[int]) -> None:
204
224
  """Replace the line breakpoints for `source` (DAP-style per-source set)."""
205
- norm = _normalize_source(source)
225
+ norm = _breakpoint_source(source)
206
226
  self._breakpoints = [bp for bp in self._breakpoints if not (bp.kind == "line" and bp.source == norm)]
207
227
  for line in lines:
208
228
  self.add_line_breakpoint(source, line)
@@ -213,6 +233,11 @@ class DebugController:
213
233
  for name in names:
214
234
  self.add_keyword_breakpoint(name)
215
235
 
236
+ def warn_about_unreached_breakpoints(self) -> None:
237
+ """Warn at the end of the run about the breakpoints no keyword reached: from the outside, a mistyped
238
+ path looks like a run that simply went through."""
239
+ self._warn_about_unreached = True
240
+
216
241
  def get_breakpoint(self, bp_id: int) -> Optional[Breakpoint]:
217
242
  return next((bp for bp in self._breakpoints if bp.id == bp_id), None)
218
243
 
@@ -230,7 +255,7 @@ class DebugController:
230
255
  def restore_breakpoints(self, state: BreakpointState) -> None:
231
256
  """Take over the breakpoints of `breakpoint_state`, in place of those set so far. Their hit counts start at 0,
232
257
  as `.ignore` counts the hits of a run."""
233
- self._breakpoints = [dataclasses.replace(bp, hits=0) for bp in state.breakpoints]
258
+ self._breakpoints = [dataclasses.replace(bp, hits=0, reached=False) for bp in state.breakpoints]
234
259
  self._next_bp_id = state.next_id
235
260
  self._exception_filters = set(state.exception_filters)
236
261
 
@@ -288,7 +313,7 @@ class DebugController:
288
313
 
289
314
  @property
290
315
  def line_breakpoints(self) -> Dict[str, Set[int]]:
291
- """Active line breakpoints, keyed by normalised source path (derived)."""
316
+ """Active line breakpoints, keyed by the path they match by (derived)."""
292
317
  out: Dict[str, Set[int]] = {}
293
318
  for bp in self._breakpoints:
294
319
  if bp.kind == "line" and bp.source is not None and bp.line is not None:
@@ -551,6 +576,10 @@ class DebugController:
551
576
 
552
577
  def end_suite(self, data: "running.TestSuite", result: "result.TestSuite") -> None:
553
578
  self._end_exec(result, "failed_suite")
579
+ if self._warn_about_unreached and not self._stack and not self._detached and self._frontend is not None:
580
+ for bp in self._breakpoints:
581
+ if bp.enabled and not bp.reached:
582
+ self._frontend.on_output(f"[ WARN ] Breakpoint {bp.where} was never reached.")
554
583
 
555
584
  def _end_exec(self, result: Any, filter_id: str) -> None:
556
585
  # The SUITE/TEST frame pushed at the matching start is on top of the stack.
@@ -685,20 +714,24 @@ class DebugController:
685
714
  return True
686
715
 
687
716
  def _match_breakpoint(self, frame: StackFrame) -> Optional[Breakpoint]:
717
+ matched: Optional[Breakpoint] = None
688
718
  for bp in self._breakpoints:
689
- if not bp.enabled:
690
- continue
691
- if bp.kind == "keyword" and bp.name is not None and self._keyword_matches(bp.name, frame):
692
- return bp
693
- if (
694
- bp.kind == "line"
695
- and frame.source is not None
696
- and frame.line is not None
697
- and bp.source == _normalize_source(frame.source)
698
- and bp.line == frame.line
699
- ):
700
- return bp
701
- return None
719
+ if bp.enabled and self._is_at_breakpoint(bp, frame):
720
+ # Whether its condition holds or not: the warning is about the place.
721
+ bp.reached = True
722
+ matched = matched or bp
723
+ return matched
724
+
725
+ def _is_at_breakpoint(self, bp: Breakpoint, frame: StackFrame) -> bool:
726
+ if bp.kind == "keyword":
727
+ return bp.name is not None and self._keyword_matches(bp.name, frame)
728
+ return (
729
+ frame.source is not None
730
+ and frame.line is not None
731
+ and bp.source is not None
732
+ and bp.line == frame.line
733
+ and _source_matches(bp.source, _normalize_source(frame.source))
734
+ )
702
735
 
703
736
  @staticmethod
704
737
  def _keyword_matches(name: str, frame: StackFrame) -> bool:
@@ -67,7 +67,8 @@ class StackFrame:
67
67
  class Breakpoint:
68
68
  """A registered breakpoint with optional attributes (the shared CLI + DAP model).
69
69
 
70
- `kind` is ``"line"`` (`source`+`line`) or ``"keyword"`` (`name`). The optional
70
+ `kind` is ``"line"`` (`source`+`line`, with `location` as it was given) or
71
+ ``"keyword"`` (`name`). The optional
71
72
  attributes drive the hit-time decision: `condition` (stop only if the
72
73
  expression is truthy — a *failing* condition stops too, past `ignore_count`
73
74
  and `temporary`, so the breakage is noticed), `ignore_count` (skip the next N triggering hits), `log_message`
@@ -88,6 +89,13 @@ class Breakpoint:
88
89
  enabled: bool = True
89
90
  commands: List[str] = field(default_factory=list)
90
91
  hits: int = 0
92
+ location: Optional[str] = None
93
+ reached: bool = False
94
+
95
+ @property
96
+ def where(self) -> str:
97
+ """Where the breakpoint was set, as it was given."""
98
+ return f"{self.name}" if self.kind == "keyword" else f"{self.location}:{self.line}"
91
99
 
92
100
 
93
101
  @dataclass
@@ -59,7 +59,7 @@ from rich.console import Console
59
59
  from rich.markdown import Markdown
60
60
  from robot.utils import MultiMatcher
61
61
 
62
- from robotcode.robot.utils.markdown_docs import heading_anchors
62
+ from robotcode.robot.utils.markdown_docs import code_lines_before_headings, heading_anchors
63
63
 
64
64
 
65
65
  def render_markdown_to_ansi(text: str, *, width: int = 80) -> str:
@@ -240,12 +240,12 @@ _SIDEBAR_HINTS = " type: filter · ↑/↓: select · wheel: scroll · Enter or
240
240
  # The rendered-line lookup runs a heading's title through `_strip_md_emphasis`
241
241
  # because `rich` consumes those markers when it paints.
242
242
 
243
- _MD_EMPHASIS_RE = re.compile(r"[*_`]")
243
+ _MD_EMPHASIS_RE = re.compile(r"\\([!-/:-@\[-`{-~])|[*_`]")
244
244
 
245
245
 
246
246
  def _strip_md_emphasis(text: str) -> str:
247
- """Drop `*`, `_`, `` ` `` markers — `rich` consumes them in rendering."""
248
- return _MD_EMPHASIS_RE.sub("", text)
247
+ """Drop `*`, `_`, `` ` `` markers and the backslash of escapes — `rich` consumes them in rendering."""
248
+ return _MD_EMPHASIS_RE.sub(lambda m: m.group(1) or "", text)
249
249
 
250
250
 
251
251
  def _build_anchor_to_line_map(md_source: str, rendered_plain: str) -> Dict[str, int]:
@@ -267,10 +267,15 @@ def _build_anchor_to_line_map(md_source: str, rendered_plain: str) -> Dict[str,
267
267
  cursor = 0
268
268
  # `heading_anchors` skips `#` lines of fenced code blocks, which are no headings,
269
269
  # and numbers repeated anchors as the links of the page do
270
- for _, raw_title, anchor in heading_anchors(md_source):
270
+ for (_, raw_title, anchor), code_lines in zip(heading_anchors(md_source), code_lines_before_headings(md_source)):
271
271
  display = _strip_md_emphasis(raw_title)
272
+ # a line of a code block can read like the heading after it
273
+ skip = code_lines.count(display)
272
274
  for j in range(cursor, len(rendered_lines)):
273
275
  if rendered_lines[j].strip() == display:
276
+ if skip:
277
+ skip -= 1
278
+ continue
274
279
  result[anchor] = j
275
280
  cursor = j + 1
276
281
  break
@@ -1178,7 +1178,7 @@ class SessionInterpreter(ConsoleInterpreter):
1178
1178
  @property
1179
1179
  def termination_requested(self) -> bool:
1180
1180
  """Whether a signal has asked the REPL or the run to end, as SIGTERM does."""
1181
- return self._termination_requested
1181
+ return self._termination_requested and not self._aborted
1182
1182
 
1183
1183
  def read_line(
1184
1184
  self, prompt: str, *, multiline_continuation: bool = False, prefill: str = "", completer: Any = None
@@ -1221,6 +1221,10 @@ class SessionInterpreter(ConsoleInterpreter):
1221
1221
  if not self._server.at_line_start():
1222
1222
  super()._end_console_line()
1223
1223
 
1224
+ def _dot_command_failed(self, text: str) -> None:
1225
+ self._server.line_failed()
1226
+ super()._dot_command_failed(text)
1227
+
1224
1228
  def _evaluate_at_stop(self, line: str) -> bool:
1225
1229
  # A keyword at a stop fails the input as at the REPL prompt.
1226
1230
  try:
@@ -436,6 +436,8 @@ class BaseInterpreter(abc.ABC):
436
436
  self.exit_code: Optional[int] = None
437
437
  # Set by a signal that ends the session or the run.
438
438
  self._termination_requested = False
439
+ # Set by `.abort`, which ends the run by the same way as a signal but isn't one from outside.
440
+ self._aborted = False
439
441
  # Set while a statement of the input runs: SIGTERM then stops it at its next keyword.
440
442
  self._running_statement = False
441
443
 
@@ -460,8 +460,8 @@ DEBUG_OPTIONS = [
460
460
  "break_at",
461
461
  metavar="LOCATION",
462
462
  multiple=True,
463
- help="Break at LOCATION — a `file:line`, or a keyword name or glob pattern "
464
- "matched like Robot Framework. Repeatable.",
463
+ help="Break at LOCATION — a `file:line`, where the end of the path is enough, or a keyword name "
464
+ "or glob pattern matched like Robot Framework. Repeatable.",
465
465
  ),
466
466
  click.option(
467
467
  "--break-on-exception/--no-break-on-exception",
@@ -694,6 +694,7 @@ def robot_debug(
694
694
  )
695
695
  if server is not None:
696
696
  server.keep_breakpoints(controller)
697
+ controller.warn_about_unreached_breakpoints()
697
698
 
698
699
  # `robot-debug` produces the same console output as `robotcode robot` — the run
699
700
  # is fully visible. At a stop the debug prompt is interleaved with Robot's live
@@ -704,14 +705,20 @@ def robot_debug(
704
705
  session: ContextManager[object] = server if server is not None else contextlib.nullcontext()
705
706
  try:
706
707
  with session, interpreter.forward_events(echo_messages=False):
707
- _invoke_runner(
708
- ctx,
709
- by_longname,
710
- exclude_by_longname,
711
- by_test_metadata,
712
- exclude_by_test_metadata,
713
- robot_options_and_args,
714
- )
708
+ try:
709
+ _invoke_runner(
710
+ ctx,
711
+ by_longname,
712
+ exclude_by_longname,
713
+ by_test_metadata,
714
+ exclude_by_test_metadata,
715
+ robot_options_and_args,
716
+ )
717
+ except SystemExit:
718
+ # After `.abort` the code says that, not how many tests failed.
719
+ if interpreter.exit_code is None:
720
+ raise
721
+ raise SystemExit(interpreter.exit_code) from None
715
722
  except DebugTerminated:
716
723
  app.echo("Debugger: run terminated.")
717
724
  finally:
@@ -21,6 +21,7 @@ import pprint
21
21
  import re
22
22
  import reprlib
23
23
  import shlex
24
+ import signal
24
25
  import sys
25
26
  from datetime import datetime
26
27
  from pathlib import Path
@@ -31,6 +32,7 @@ from robot import result, running
31
32
  from robot.errors import ExecutionFailed, ExecutionPassed, ExecutionStatus
32
33
  from robot.running import Keyword
33
34
  from robot.running.context import EXECUTION_CONTEXTS
35
+ from robot.running.signalhandler import STOP_SIGNAL_MONITOR
34
36
  from robot.utils import normalize
35
37
 
36
38
  from robotcode.plugin import Application
@@ -471,8 +473,7 @@ class ConsoleInterpreter(BaseInterpreter):
471
473
  arg = (m.group(2) or "").strip()
472
474
  attr_name, error = self._resolve_dot_command(name)
473
475
  if attr_name is None:
474
- if self.app is not None:
475
- self.app.echo(error or f"Unknown dot-command: .{name}. Try .help.")
476
+ self._dot_command_failed(error or f"Unknown dot-command: .{name}. Try .help.")
476
477
  return True
477
478
  getattr(self, attr_name)(arg)
478
479
  return True
@@ -667,9 +668,9 @@ class ConsoleInterpreter(BaseInterpreter):
667
668
  silent = bool(bp is not None and bp.commands and bp.commands[0] == "silent" and not stop.description)
668
669
  if not silent:
669
670
  self._render_stop(stop)
670
- # try/finally so the stop state is always cleared, even if `.abort`
671
- # (SystemExit) or a Ctrl-C during an at-stop evaluation unwinds through
672
- # the loop — otherwise a stale `_stop` would leave the interactive shell
671
+ # try/finally so the stop state is always cleared, even if a Ctrl-C
672
+ # during an at-stop evaluation unwinds through the loop — otherwise a
673
+ # stale `_stop` would leave the interactive shell
673
674
  # prompt wedged in a phantom debug state after the run returns.
674
675
  try:
675
676
  if bp is not None and bp.commands:
@@ -698,9 +699,9 @@ class ConsoleInterpreter(BaseInterpreter):
698
699
  # already; the REPL prompt adds nothing to it either.
699
700
  pass
700
701
  except Exception as e: # a throwing command must not eject the debug prompt
701
- # `.abort` (SystemExit), Ctrl-C, and DebugTerminated are
702
- # BaseException, not Exception, so they still propagate.
703
- self._echo(f"! {e}")
702
+ # Ctrl-C and DebugTerminated are BaseException, not
703
+ # Exception, so they still propagate.
704
+ self._dot_command_failed(f"! {e}")
704
705
  return self._pending_action
705
706
  finally:
706
707
  self._stop = None
@@ -736,10 +737,14 @@ class ConsoleInterpreter(BaseInterpreter):
736
737
  if self.app is not None:
737
738
  self.app.echo(text)
738
739
 
740
+ def _dot_command_failed(self, text: str) -> None:
741
+ """Say that a dot-command couldn't do what was asked."""
742
+ self._echo(text)
743
+
739
744
  def _require_stop(self) -> bool:
740
745
  """Guard for commands that only mean something while paused."""
741
746
  if self._stop is None:
742
- self._echo("not at a breakpoint")
747
+ self._dot_command_failed("not at a breakpoint")
743
748
  return False
744
749
  return True
745
750
 
@@ -747,7 +752,7 @@ class ConsoleInterpreter(BaseInterpreter):
747
752
  """Guard for commands that need a wired debug controller. (Distinct from
748
753
  the attach/detach *state* — a detached controller is still wired.)"""
749
754
  if self._controller is None:
750
- self._echo("no debugger available")
755
+ self._dot_command_failed("no debugger available")
751
756
  return False
752
757
  return True
753
758
 
@@ -1377,12 +1382,12 @@ class ConsoleInterpreter(BaseInterpreter):
1377
1382
  if self.app is None:
1378
1383
  return
1379
1384
  if not arg:
1380
- self.app.echo("Usage: .doc <library-or-resource-name>")
1385
+ self._dot_command_failed("Usage: .doc <library-or-resource-name>")
1381
1386
  return
1382
1387
 
1383
1388
  lib_doc, error = self._resolve_doc_target(arg)
1384
1389
  if lib_doc is None:
1385
- self.app.echo(error or f"Could not load {arg!r}.")
1390
+ self._dot_command_failed(error or f"Could not load {arg!r}.")
1386
1391
  return
1387
1392
 
1388
1393
  self.show_doc(
@@ -1445,7 +1450,7 @@ class ConsoleInterpreter(BaseInterpreter):
1445
1450
  try:
1446
1451
  self.app.echo(str(Path.cwd()))
1447
1452
  except OSError as e:
1448
- self.app.echo(f"(working directory unavailable: {e})")
1453
+ self._dot_command_failed(f"(working directory unavailable: {e})")
1449
1454
 
1450
1455
  @dot_command("exit", "quit")
1451
1456
  def _exit(self, arg: str) -> None:
@@ -1465,13 +1470,13 @@ class ConsoleInterpreter(BaseInterpreter):
1465
1470
  .exit 3
1466
1471
  """
1467
1472
  if self._stop is not None:
1468
- self._echo("at a debug stop — use .continue or .detach to resume, or .abort to quit the run")
1473
+ self._dot_command_failed("at a debug stop — use .continue or .detach to resume, or .abort to quit the run")
1469
1474
  return
1470
1475
  if arg:
1471
1476
  try:
1472
1477
  self.exit_code = int(arg)
1473
1478
  except ValueError:
1474
- self._echo("Usage: .exit [CODE]")
1479
+ self._dot_command_failed("Usage: .exit [CODE]")
1475
1480
  return
1476
1481
  raise EOFError
1477
1482
 
@@ -1515,7 +1520,7 @@ class ConsoleInterpreter(BaseInterpreter):
1515
1520
  try:
1516
1521
  opts = parser.parse_args(_split_command_args(arg))
1517
1522
  except (argparse.ArgumentError, SystemExit, ValueError):
1518
- self.app.echo("Usage: .save [-a] [-t NAME] [--keep-failed] FILENAME")
1523
+ self._dot_command_failed("Usage: .save [-a] [-t NAME] [--keep-failed] FILENAME")
1519
1524
  return
1520
1525
 
1521
1526
  recorded: List[str] = list(self._session_lines or [])
@@ -1541,7 +1546,7 @@ class ConsoleInterpreter(BaseInterpreter):
1541
1546
  else:
1542
1547
  path.write_text(content, encoding="utf-8")
1543
1548
  except OSError as e:
1544
- self.app.echo(f"Could not write {opts.filename}: {e}")
1549
+ self._dot_command_failed(f"Could not write {opts.filename}: {e}")
1545
1550
  return
1546
1551
 
1547
1552
  summary = f"{len(lines)} {'entry' if len(lines) == 1 else 'entries'}"
@@ -1656,7 +1661,7 @@ class ConsoleInterpreter(BaseInterpreter):
1656
1661
  (default 10) — `.source Open Browser 25`.
1657
1662
  """
1658
1663
  if not arg:
1659
- self._echo("usage: .source <keyword-name> [lines]")
1664
+ self._dot_command_failed("usage: .source <keyword-name> [lines]")
1660
1665
  return
1661
1666
  # Split off a trailing line-count (the plain-backend window size). Keyword
1662
1667
  # names contain spaces, so only a trailing integer token counts.
@@ -1666,7 +1671,7 @@ class ConsoleInterpreter(BaseInterpreter):
1666
1671
  name, count = head[0], max(1, int(head[1]))
1667
1672
  found = lookup_keyword_owner(name)
1668
1673
  if found is None:
1669
- self._echo(f"keyword {name!r} not found")
1674
+ self._dot_command_failed(f"keyword {name!r} not found")
1670
1675
  return
1671
1676
  owner, runtime_kw, is_resource = found
1672
1677
  kw_name = getattr(runtime_kw, "name", name)
@@ -1676,7 +1681,7 @@ class ConsoleInterpreter(BaseInterpreter):
1676
1681
  if source is None:
1677
1682
  source = getattr(runtime_kw, "source", None)
1678
1683
  if source is None or line is None:
1679
- self._echo(f"(no source available for {kw_name})")
1684
+ self._dot_command_failed(f"(no source available for {kw_name})")
1680
1685
  return
1681
1686
  source, line = str(source), int(line)
1682
1687
  header = f"{kw_name} ({os.path.basename(source)}:{line})"
@@ -1707,7 +1712,7 @@ class ConsoleInterpreter(BaseInterpreter):
1707
1712
  target = int(arg)
1708
1713
  except ValueError:
1709
1714
  top = len(self._stop.stack) - 1 if self._stop else 0
1710
- self._echo(f"usage: .frame <n> (0..{top})")
1715
+ self._dot_command_failed(f"usage: .frame <n> (0..{top})")
1711
1716
  return
1712
1717
  self._move_frame(target)
1713
1718
 
@@ -1717,12 +1722,12 @@ class ConsoleInterpreter(BaseInterpreter):
1717
1722
  if not self._require_stop():
1718
1723
  return
1719
1724
  if not arg:
1720
- self._echo("usage: .print <variable-or-expression>")
1725
+ self._dot_command_failed("usage: .print <variable-or-expression>")
1721
1726
  return
1722
1727
  try:
1723
1728
  value = self._controller.evaluate_expression(self._selected_frame(), arg) # type: ignore[union-attr]
1724
1729
  except Exception as e: # surface lookup/eval failures to the prompt
1725
- self._echo(f"! {e}")
1730
+ self._dot_command_failed(f"! {e}")
1726
1731
  return
1727
1732
  self._echo(f"{arg} = {_repr.repr(value)}")
1728
1733
 
@@ -1732,12 +1737,12 @@ class ConsoleInterpreter(BaseInterpreter):
1732
1737
  if not self._require_stop():
1733
1738
  return
1734
1739
  if not arg:
1735
- self._echo("usage: .pprint <variable-or-expression>")
1740
+ self._dot_command_failed("usage: .pprint <variable-or-expression>")
1736
1741
  return
1737
1742
  try:
1738
1743
  value = self._controller.evaluate_expression(self._selected_frame(), arg) # type: ignore[union-attr]
1739
1744
  except Exception as e: # surface lookup/eval failures to the prompt
1740
- self._echo(f"! {e}")
1745
+ self._dot_command_failed(f"! {e}")
1741
1746
  return
1742
1747
  self._echo(pprint.pformat(value))
1743
1748
 
@@ -1747,12 +1752,12 @@ class ConsoleInterpreter(BaseInterpreter):
1747
1752
  if not self._require_stop():
1748
1753
  return
1749
1754
  if not arg:
1750
- self._echo("usage: .whatis <variable-or-expression>")
1755
+ self._dot_command_failed("usage: .whatis <variable-or-expression>")
1751
1756
  return
1752
1757
  try:
1753
1758
  value = self._controller.evaluate_expression(self._selected_frame(), arg) # type: ignore[union-attr]
1754
1759
  except Exception as e: # surface lookup/eval failures to the prompt
1755
- self._echo(f"! {e}")
1760
+ self._dot_command_failed(f"! {e}")
1756
1761
  return
1757
1762
  self._echo(f"{arg}: {type(value).__name__}")
1758
1763
 
@@ -1784,7 +1789,7 @@ class ConsoleInterpreter(BaseInterpreter):
1784
1789
  self._display_exprs.remove(arg)
1785
1790
  self._echo(f"no longer displaying {arg}")
1786
1791
  else:
1787
- self._echo(f"not displaying: {arg}")
1792
+ self._dot_command_failed(f"not displaying: {arg}")
1788
1793
 
1789
1794
  @dot_command("set", group="Debugger")
1790
1795
  def _set(self, arg: str) -> None:
@@ -1803,18 +1808,18 @@ class ConsoleInterpreter(BaseInterpreter):
1803
1808
  return
1804
1809
  parts = arg.split(None, 1)
1805
1810
  if len(parts) < 2:
1806
- self._echo("usage: .set ${name} <value>")
1811
+ self._dot_command_failed("usage: .set ${name} <value>")
1807
1812
  return
1808
1813
  name, value = parts[0], parts[1]
1809
1814
  if not (name.startswith("${") and name.endswith("}")) or "[" in name:
1810
- self._echo(f"! .set only supports whole scalar variables (${{...}}), not {name}")
1815
+ self._dot_command_failed(f"! .set only supports whole scalar variables (${{...}}), not {name}")
1811
1816
  return
1812
1817
  try:
1813
1818
  new_repr = self._controller.set_variable( # type: ignore[union-attr]
1814
1819
  self._selected_frame(), name, value, evaluate=False
1815
1820
  )
1816
1821
  except Exception as e: # surface NameError / unknown-variable to the prompt
1817
- self._echo(f"! {e}")
1822
+ self._dot_command_failed(f"! {e}")
1818
1823
  return
1819
1824
  self._echo(f"{name} = {new_repr}")
1820
1825
 
@@ -1822,7 +1827,7 @@ class ConsoleInterpreter(BaseInterpreter):
1822
1827
  if not self._require_controller():
1823
1828
  return
1824
1829
  if not arg:
1825
- self._echo("usage: .break <file:line | keyword name> [, <condition>]")
1830
+ self._dot_command_failed("usage: .break <file:line | keyword name> [, <condition>]")
1826
1831
  return
1827
1832
  spec, condition = arg, None
1828
1833
  if "," in arg: # `.break <loc>, <condition>`
@@ -1863,7 +1868,6 @@ class ConsoleInterpreter(BaseInterpreter):
1863
1868
  assert controller is not None # narrowed by _require_controller
1864
1869
  bps = controller.breakpoints
1865
1870
  for bp in bps:
1866
- loc = bp.name if bp.kind == "keyword" else f"{os.path.basename(bp.source or '?')}:{bp.line}"
1867
1871
  flags: List[str] = []
1868
1872
  if not bp.enabled:
1869
1873
  flags.append("disabled")
@@ -1878,7 +1882,7 @@ class ConsoleInterpreter(BaseInterpreter):
1878
1882
  if bp.commands:
1879
1883
  flags.append(f"{len(bp.commands)} {'cmd' if len(bp.commands) == 1 else 'cmds'}")
1880
1884
  suffix = (" " + ", ".join(flags)) if flags else ""
1881
- self._echo(f" #{bp.id} {bp.kind:<7} {loc}{suffix}")
1885
+ self._echo(f" #{bp.id} {bp.kind:<7} {bp.where}{suffix}")
1882
1886
  filters = controller.exception_filters
1883
1887
  if filters:
1884
1888
  self._echo(f" catch {', '.join(sorted(filters))}")
@@ -1892,11 +1896,11 @@ class ConsoleInterpreter(BaseInterpreter):
1892
1896
  try:
1893
1897
  bp_id = int(token)
1894
1898
  except (TypeError, ValueError):
1895
- self._echo(f"not a breakpoint number: {token}")
1899
+ self._dot_command_failed(f"not a breakpoint number: {token}")
1896
1900
  return None
1897
1901
  bp = controller.get_breakpoint(bp_id)
1898
1902
  if bp is None:
1899
- self._echo(f"no breakpoint {bp_id}")
1903
+ self._dot_command_failed(f"no breakpoint {bp_id}")
1900
1904
  return bp
1901
1905
 
1902
1906
  @dot_command("condition", group="Debugger")
@@ -1906,7 +1910,7 @@ class ConsoleInterpreter(BaseInterpreter):
1906
1910
  return
1907
1911
  parts = arg.split(None, 1)
1908
1912
  if not parts:
1909
- self._echo("usage: .condition <breakpoint-number> [<expression>]")
1913
+ self._dot_command_failed("usage: .condition <breakpoint-number> [<expression>]")
1910
1914
  return
1911
1915
  bp = self._bp_by_arg(parts[0])
1912
1916
  if bp is None:
@@ -1924,7 +1928,7 @@ class ConsoleInterpreter(BaseInterpreter):
1924
1928
  return
1925
1929
  parts = arg.split()
1926
1930
  if len(parts) != 2:
1927
- self._echo("usage: .ignore <breakpoint-number> <count>")
1931
+ self._dot_command_failed("usage: .ignore <breakpoint-number> <count>")
1928
1932
  return
1929
1933
  bp = self._bp_by_arg(parts[0])
1930
1934
  if bp is None:
@@ -1932,7 +1936,7 @@ class ConsoleInterpreter(BaseInterpreter):
1932
1936
  try:
1933
1937
  count = int(parts[1])
1934
1938
  except ValueError:
1935
- self._echo(f"not a count: {parts[1]}")
1939
+ self._dot_command_failed(f"not a count: {parts[1]}")
1936
1940
  return
1937
1941
  bp.ignore_count = max(0, count)
1938
1942
  bp.hits = 0 # the next `count` triggering hits are skipped from now
@@ -1997,7 +2001,7 @@ class ConsoleInterpreter(BaseInterpreter):
1997
2001
  if not self._require_controller():
1998
2002
  return
1999
2003
  if not arg.strip():
2000
- self._echo("usage: .commands <breakpoint-number>")
2004
+ self._dot_command_failed("usage: .commands <breakpoint-number>")
2001
2005
  return
2002
2006
  bp = self._bp_by_arg(arg.strip())
2003
2007
  if bp is None:
@@ -2045,7 +2049,7 @@ class ConsoleInterpreter(BaseInterpreter):
2045
2049
  for token in tokens:
2046
2050
  target = _CATCH_SHORTHANDS.get(token.lower())
2047
2051
  if target is None:
2048
- self._echo(f"unknown catch filter: {token}; try uncaught, all, test, suite, off")
2052
+ self._dot_command_failed(f"unknown catch filter: {token}; try uncaught, all, test, suite, off")
2049
2053
  return
2050
2054
  mapped.add(target)
2051
2055
  controller.set_exception_breakpoints(mapped)
@@ -2077,7 +2081,7 @@ class ConsoleInterpreter(BaseInterpreter):
2077
2081
  controller.set_attached(False)
2078
2082
  self._echo("debugger: detached")
2079
2083
  else:
2080
- self._echo("usage: .debug on|off")
2084
+ self._dot_command_failed("usage: .debug on|off")
2081
2085
 
2082
2086
  @dot_command("detach", group="Debugger")
2083
2087
  def _detach(self, arg: str) -> None:
@@ -2099,13 +2103,21 @@ class ConsoleInterpreter(BaseInterpreter):
2099
2103
 
2100
2104
  @dot_command("abort", group="Debugger")
2101
2105
  def _abort(self, arg: str) -> None:
2102
- """Abort the run and exit. Use `.detach` to let the suite finish instead."""
2106
+ """End the run: the current test fails, the rest don't run, teardowns run, the output is written.
2107
+
2108
+ Use `.detach` to let the suite finish instead.
2109
+ """
2103
2110
  del arg
2104
2111
  if self._stop is None:
2105
- self._echo("no run to abort")
2112
+ self._dot_command_failed("no run to abort")
2106
2113
  return
2107
- # Robot can't be cleanly stopped mid-keyword from a logger callback — a
2108
- # raised exception is swallowed by suite.run() — so exit via SystemExit,
2109
- # the one exception Robot does propagate.
2110
2114
  self._echo("aborting the run")
2111
- sys.exit(1)
2115
+ self._pending_action = ResumeAction.CONTINUE
2116
+ self.exit_code = 253
2117
+ self._aborted = True
2118
+ try:
2119
+ # As SIGTERM does, so that Robot Framework writes the output files to their end.
2120
+ STOP_SIGNAL_MONITOR(signal.SIGTERM, None)
2121
+ except ExecutionStatus:
2122
+ # For a keyword that runs: from here it would get lost, the next keyword that starts stops the run.
2123
+ pass
@@ -1 +0,0 @@
1
- __version__ = "2.9.0-dev.87"