robotcode-repl 2.9.0.dev68__py3-none-any.whl → 2.9.0.dev74__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/__version__.py +1 -1
- robotcode/repl/_debug/controller.py +17 -0
- robotcode/repl/_session.py +1119 -0
- robotcode/repl/cli.py +114 -38
- robotcode/repl/console_interpreter.py +22 -10
- robotcode/repl/hooks.py +2 -1
- robotcode/repl/session_cli.py +530 -0
- {robotcode_repl-2.9.0.dev68.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/METADATA +2 -2
- {robotcode_repl-2.9.0.dev68.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/RECORD +11 -9
- {robotcode_repl-2.9.0.dev68.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/WHEEL +0 -0
- {robotcode_repl-2.9.0.dev68.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/entry_points.txt +0 -0
robotcode/repl/cli.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import Dict, List, Optional, Tuple
|
|
5
|
+
from typing import ContextManager, Dict, List, Optional, Tuple
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
8
|
|
|
9
|
+
from robotcode.core.utils.cli import show_hidden_arguments
|
|
8
10
|
from robotcode.plugin import Application, pass_application
|
|
9
11
|
from robotcode.plugin._agent_detection import is_running_in_ai_agent
|
|
10
12
|
from robotcode.plugin.click_helper.aliases import AliasedCommand
|
|
@@ -15,6 +17,7 @@ from robotcode.runner.cli.robot import ROBOT_OPTIONS, ROBOT_VERSION_OPTIONS, loa
|
|
|
15
17
|
from .__version__ import __version__
|
|
16
18
|
from ._backends import BACKEND_CHOICES
|
|
17
19
|
from ._debug import DebugController, DebugTerminated
|
|
20
|
+
from ._session import SessionError, SessionInterpreter, SessionKind, SessionServer, project_folder
|
|
18
21
|
from .console_interpreter import _LINE_BREAK_RE, ConsoleInterpreter
|
|
19
22
|
from .prompt_toolkit_interpreter import PromptToolkitConsoleInterpreter
|
|
20
23
|
from .run import ReplResult, run_repl
|
|
@@ -391,6 +394,52 @@ SHELL_OPTIONS = [
|
|
|
391
394
|
),
|
|
392
395
|
]
|
|
393
396
|
|
|
397
|
+
# The session that `robotcode session start` runs `repl` or `robot-debug` as.
|
|
398
|
+
SESSION_OPTIONS = [
|
|
399
|
+
click.option(
|
|
400
|
+
"--serve-session",
|
|
401
|
+
metavar="ID",
|
|
402
|
+
hidden=show_hidden_arguments(),
|
|
403
|
+
help="Serve the session ID that `robotcode session start` started. This is an internal option.",
|
|
404
|
+
),
|
|
405
|
+
click.option(
|
|
406
|
+
"--serve-output",
|
|
407
|
+
type=int,
|
|
408
|
+
metavar="FD",
|
|
409
|
+
hidden=show_hidden_arguments(),
|
|
410
|
+
help="Read the output of the session from the pipe FD, a handle on Windows, into memory. "
|
|
411
|
+
"This is an internal option.",
|
|
412
|
+
),
|
|
413
|
+
click.option(
|
|
414
|
+
"--serve-idle-timeout",
|
|
415
|
+
type=float,
|
|
416
|
+
default=15,
|
|
417
|
+
metavar="MINUTES",
|
|
418
|
+
hidden=show_hidden_arguments(),
|
|
419
|
+
help="End the session after it has waited at its prompt for MINUTES without a request. "
|
|
420
|
+
"This is an internal option.",
|
|
421
|
+
),
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _session_server(
|
|
426
|
+
app: Application,
|
|
427
|
+
serve_session: Optional[str],
|
|
428
|
+
serve_output: Optional[int],
|
|
429
|
+
kind: SessionKind,
|
|
430
|
+
idle_timeout: float,
|
|
431
|
+
) -> Optional[SessionServer]:
|
|
432
|
+
if serve_session is None:
|
|
433
|
+
return None
|
|
434
|
+
if serve_output is None:
|
|
435
|
+
raise click.UsageError("--serve-session needs --serve-output.")
|
|
436
|
+
try:
|
|
437
|
+
# The folder `session start` ran in decides the project, also for a debug run of a suite elsewhere.
|
|
438
|
+
return SessionServer(serve_session, kind, project_folder(app), idle_timeout * 60, serve_output)
|
|
439
|
+
except SessionError as e:
|
|
440
|
+
raise click.ClickException(str(e)) from e
|
|
441
|
+
|
|
442
|
+
|
|
394
443
|
# Debugger options — shared by `repl` and `robot-debug`. `--debugger-attached`
|
|
395
444
|
# is the master switch (does anything pause at all?); the `--break*` flags are
|
|
396
445
|
# the individual pause triggers. (`robot-debug` adds `--stop-on-entry`, which
|
|
@@ -447,6 +496,7 @@ DEBUG_OPTIONS = [
|
|
|
447
496
|
@add_options(*REPL_BASE_OPTIONS)
|
|
448
497
|
@add_options(*SHELL_OPTIONS)
|
|
449
498
|
@add_options(*DEBUG_OPTIONS)
|
|
499
|
+
@add_options(*SESSION_OPTIONS)
|
|
450
500
|
@click.version_option(version=__version__, prog_name="RobotCode REPL")
|
|
451
501
|
@pass_application
|
|
452
502
|
def repl(
|
|
@@ -474,6 +524,9 @@ def repl(
|
|
|
474
524
|
break_on_all_exceptions: bool,
|
|
475
525
|
break_on_failed_test: bool,
|
|
476
526
|
break_on_failed_suite: bool,
|
|
527
|
+
serve_session: Optional[str],
|
|
528
|
+
serve_output: Optional[int],
|
|
529
|
+
serve_idle_timeout: float,
|
|
477
530
|
) -> None:
|
|
478
531
|
"""Run Robot Framework interactively (alias `shell`).
|
|
479
532
|
|
|
@@ -500,14 +553,19 @@ def repl(
|
|
|
500
553
|
|
|
501
554
|
backend = _resolve_backend(plain, backend)
|
|
502
555
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
files=list(files),
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
556
|
+
server = _session_server(app, serve_session, serve_output, SessionKind.REPL, serve_idle_timeout)
|
|
557
|
+
interpreter = (
|
|
558
|
+
SessionInterpreter(server, app=app, files=list(files), show_keywords=show_keywords, inspect=inspect)
|
|
559
|
+
if server is not None
|
|
560
|
+
else _pick_interpreter(
|
|
561
|
+
app=app,
|
|
562
|
+
files=list(files),
|
|
563
|
+
show_keywords=show_keywords,
|
|
564
|
+
inspect=inspect,
|
|
565
|
+
no_history=no_history,
|
|
566
|
+
backend=backend,
|
|
567
|
+
show_prompt=_resolve_show_prompt(show_prompt),
|
|
568
|
+
)
|
|
511
569
|
)
|
|
512
570
|
# Only `repl` fails its session test on unhandled failures — not `robot-debug`,
|
|
513
571
|
# which builds its interpreter the same way.
|
|
@@ -515,7 +573,7 @@ def repl(
|
|
|
515
573
|
# The debugger stays wired for the whole interactive session and is torn down
|
|
516
574
|
# with the interpreter when the session ends — so, unlike `robot`, there is no
|
|
517
575
|
# per-run unregister here. `attached` decides whether it actually pauses.
|
|
518
|
-
_attach_debugger(
|
|
576
|
+
controller = _attach_debugger(
|
|
519
577
|
interpreter,
|
|
520
578
|
break_at=break_at,
|
|
521
579
|
attached=attached,
|
|
@@ -524,26 +582,31 @@ def repl(
|
|
|
524
582
|
break_on_failed_test=break_on_failed_test,
|
|
525
583
|
break_on_failed_suite=break_on_failed_suite,
|
|
526
584
|
)
|
|
585
|
+
if server is not None:
|
|
586
|
+
server.keep_breakpoints(controller)
|
|
587
|
+
|
|
588
|
+
# A session records the exit code of the command when the REPL exits, and stays open.
|
|
589
|
+
session: ContextManager[object] = server if server is not None else contextlib.nullcontext()
|
|
590
|
+
with session:
|
|
591
|
+
result = run_repl(
|
|
592
|
+
interpreter=interpreter,
|
|
593
|
+
app=app,
|
|
594
|
+
variable=variable,
|
|
595
|
+
variablefile=variablefile,
|
|
596
|
+
pythonpath=pythonpath,
|
|
597
|
+
outputdir=outputdir,
|
|
598
|
+
output=output,
|
|
599
|
+
report=report,
|
|
600
|
+
log=log,
|
|
601
|
+
xunit=xunit,
|
|
602
|
+
source=source,
|
|
603
|
+
files=files,
|
|
604
|
+
statusrc=statusrc,
|
|
605
|
+
)
|
|
527
606
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
app=
|
|
531
|
-
variable=variable,
|
|
532
|
-
variablefile=variablefile,
|
|
533
|
-
pythonpath=pythonpath,
|
|
534
|
-
outputdir=outputdir,
|
|
535
|
-
output=output,
|
|
536
|
-
report=report,
|
|
537
|
-
log=log,
|
|
538
|
-
xunit=xunit,
|
|
539
|
-
source=source,
|
|
540
|
-
files=files,
|
|
541
|
-
statusrc=statusrc,
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
# Script files without `--inspect` end on their own, like piped input.
|
|
545
|
-
interactive = _is_interactive_stdin() and (not files or inspect)
|
|
546
|
-
app.exit(_session_exit_code(result, statusrc=statusrc, interactive=interactive))
|
|
607
|
+
# Script files without `--inspect` end on their own, like piped input.
|
|
608
|
+
interactive = _is_interactive_stdin() and (not files or inspect)
|
|
609
|
+
app.exit(_session_exit_code(result, statusrc=statusrc, interactive=interactive))
|
|
547
610
|
|
|
548
611
|
|
|
549
612
|
@wrappable
|
|
@@ -557,6 +620,7 @@ def repl(
|
|
|
557
620
|
@add_options(*REPL_BASE_OPTIONS)
|
|
558
621
|
@add_options(*(option for option in ROBOT_OPTIONS if option not in ROBOT_VERSION_OPTIONS))
|
|
559
622
|
@add_options(*DEBUG_OPTIONS)
|
|
623
|
+
@add_options(*SESSION_OPTIONS)
|
|
560
624
|
@click.option(
|
|
561
625
|
"--stop-on-entry",
|
|
562
626
|
is_flag=True,
|
|
@@ -584,6 +648,9 @@ def robot_debug(
|
|
|
584
648
|
break_on_all_exceptions: bool,
|
|
585
649
|
break_on_failed_test: bool,
|
|
586
650
|
break_on_failed_suite: bool,
|
|
651
|
+
serve_session: Optional[str],
|
|
652
|
+
serve_output: Optional[int],
|
|
653
|
+
serve_idle_timeout: float,
|
|
587
654
|
) -> None:
|
|
588
655
|
"""Run a real Robot Framework suite with the debugger attached (alias `run-debug`).
|
|
589
656
|
|
|
@@ -600,15 +667,20 @@ def robot_debug(
|
|
|
600
667
|
|
|
601
668
|
backend = _resolve_backend(plain, backend)
|
|
602
669
|
|
|
670
|
+
server = _session_server(app, serve_session, serve_output, SessionKind.DEBUG, serve_idle_timeout)
|
|
603
671
|
# A run stops at `(rdb) `, which always shows; `robot-debug` has no option for `>>> `.
|
|
604
|
-
interpreter =
|
|
605
|
-
app=app,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
672
|
+
interpreter = (
|
|
673
|
+
SessionInterpreter(server, app=app, files=[], show_keywords=False, inspect=False)
|
|
674
|
+
if server is not None
|
|
675
|
+
else _pick_interpreter(
|
|
676
|
+
app=app,
|
|
677
|
+
files=[],
|
|
678
|
+
show_keywords=False,
|
|
679
|
+
inspect=False,
|
|
680
|
+
no_history=no_history,
|
|
681
|
+
backend=backend,
|
|
682
|
+
show_prompt=_is_interactive_stdin(),
|
|
683
|
+
)
|
|
612
684
|
)
|
|
613
685
|
controller = _attach_debugger(
|
|
614
686
|
interpreter,
|
|
@@ -620,14 +692,18 @@ def robot_debug(
|
|
|
620
692
|
break_on_failed_test=break_on_failed_test,
|
|
621
693
|
break_on_failed_suite=break_on_failed_suite,
|
|
622
694
|
)
|
|
695
|
+
if server is not None:
|
|
696
|
+
server.keep_breakpoints(controller)
|
|
623
697
|
|
|
624
698
|
# `robot-debug` produces the same console output as `robotcode robot` — the run
|
|
625
699
|
# is fully visible. At a stop the debug prompt is interleaved with Robot's live
|
|
626
700
|
# console; `_render_stop` breaks off any in-progress status line, and the run's
|
|
627
701
|
# output resumes on continue/step/detach. The console mode is left to Robot's
|
|
628
702
|
# default (or the user's `--console`), not forced.
|
|
703
|
+
# A session records the exit code of the run when the run ends, and stays open.
|
|
704
|
+
session: ContextManager[object] = server if server is not None else contextlib.nullcontext()
|
|
629
705
|
try:
|
|
630
|
-
with interpreter.forward_events(echo_messages=False):
|
|
706
|
+
with session, interpreter.forward_events(echo_messages=False):
|
|
631
707
|
_invoke_runner(
|
|
632
708
|
ctx,
|
|
633
709
|
by_longname,
|
|
@@ -28,7 +28,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional,
|
|
|
28
28
|
|
|
29
29
|
import click
|
|
30
30
|
from robot import result, running
|
|
31
|
-
from robot.errors import ExecutionFailed, ExecutionPassed
|
|
31
|
+
from robot.errors import ExecutionFailed, ExecutionPassed, ExecutionStatus
|
|
32
32
|
from robot.running import Keyword
|
|
33
33
|
from robot.running.context import EXECUTION_CONTEXTS
|
|
34
34
|
from robot.utils import normalize
|
|
@@ -334,6 +334,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
334
334
|
"""
|
|
335
335
|
|
|
336
336
|
# The prompt shown while paused at a debug stop (vs the `>>> ` REPL prompt).
|
|
337
|
+
PROMPT = ">>> "
|
|
337
338
|
DEBUG_PROMPT = "(rdb) "
|
|
338
339
|
|
|
339
340
|
# Whether `show_doc` opens a scrollable fullscreen viewer (prompt_toolkit) vs
|
|
@@ -580,7 +581,8 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
580
581
|
pager ignores them.
|
|
581
582
|
"""
|
|
582
583
|
del scroll_to, outline # only the scrollable viewer (prompt_toolkit) uses them
|
|
583
|
-
|
|
584
|
+
# Documentation can end with empty lines of its markdown; the output ends with the text.
|
|
585
|
+
body = f"{title}\n{'=' * len(title)}\n\n{markdown}".rstrip()
|
|
584
586
|
if self.app is not None:
|
|
585
587
|
self.app.echo_via_pager(body, color=False)
|
|
586
588
|
|
|
@@ -691,6 +693,10 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
691
693
|
self._dispatch_dot_command(line)
|
|
692
694
|
else:
|
|
693
695
|
self._evaluate_at_stop(line)
|
|
696
|
+
except ExecutionStatus:
|
|
697
|
+
# Robot Framework has reported the failure of the keyword, or its `Pass Execution`, as a message
|
|
698
|
+
# already; the REPL prompt adds nothing to it either.
|
|
699
|
+
pass
|
|
694
700
|
except Exception as e: # a throwing command must not eject the debug prompt
|
|
695
701
|
# `.abort` (SystemExit), Ctrl-C, and DebugTerminated are
|
|
696
702
|
# BaseException, not Exception, so they still propagate.
|
|
@@ -766,7 +772,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
766
772
|
return f"{path}:{frame.line}" if frame.line is not None else path
|
|
767
773
|
|
|
768
774
|
def _render_stop(self, stop: StopEvent) -> None:
|
|
769
|
-
self.
|
|
775
|
+
self._end_console_line()
|
|
770
776
|
parts = [f"* {stop.reason.value}", stop.frame.name]
|
|
771
777
|
loc = self._location(stop.frame)
|
|
772
778
|
if loc:
|
|
@@ -777,6 +783,11 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
777
783
|
self._echo(line)
|
|
778
784
|
self._show_displays()
|
|
779
785
|
|
|
786
|
+
def _end_console_line(self) -> None:
|
|
787
|
+
"""Break off the line of the run's console that a stop comes in, such as the name of a test that Robot
|
|
788
|
+
Framework has written before its status."""
|
|
789
|
+
self._echo("")
|
|
790
|
+
|
|
780
791
|
def _show_displays(self) -> None:
|
|
781
792
|
"""Auto-evaluate the `.display` expressions in the selected frame at a stop."""
|
|
782
793
|
if not self._display_exprs or self._controller is None:
|
|
@@ -842,8 +853,8 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
842
853
|
width = max(len(f.name) for f in stack)
|
|
843
854
|
self._echo(self._frame_line(self._frame_no, self._selected_frame(), width))
|
|
844
855
|
|
|
845
|
-
def _evaluate_at_stop(self, line: str) ->
|
|
846
|
-
"""Run `line` as Robot keyword(s) in the paused context.
|
|
856
|
+
def _evaluate_at_stop(self, line: str) -> bool:
|
|
857
|
+
"""Run `line` as Robot keyword(s) in the paused context; False when it can't run.
|
|
847
858
|
|
|
848
859
|
Wrapped in `suppress_pausing()` so the evaluated keyword's own events
|
|
849
860
|
don't re-trigger a stop, and in `forward_events(echo_messages=True)` so
|
|
@@ -853,18 +864,18 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
853
864
|
# output.xml when we stop; a keyword written after it makes the file unreadable.
|
|
854
865
|
if self._stop is not None and self._stop.reason == StopReason.EXCEPTION and self._stop.frame.type == "SUITE":
|
|
855
866
|
self._echo("! Keywords can't run at the end of a suite: its output is already closed.")
|
|
856
|
-
return
|
|
867
|
+
return False
|
|
857
868
|
try:
|
|
858
869
|
test, errors = self.get_test_body_from_string(line)
|
|
859
870
|
except Exception as e: # surface any parse failure to the prompt
|
|
860
871
|
self._echo(f"! {e}")
|
|
861
|
-
return
|
|
872
|
+
return False
|
|
862
873
|
if errors:
|
|
863
874
|
for err in errors:
|
|
864
875
|
self._echo(f"! {err}")
|
|
865
|
-
return
|
|
876
|
+
return False
|
|
866
877
|
if not test.body:
|
|
867
|
-
return
|
|
878
|
+
return True
|
|
868
879
|
# Keywords evaluated at a stop don't count for the session status.
|
|
869
880
|
record_failures, self.record_failures = self.record_failures, False
|
|
870
881
|
try:
|
|
@@ -874,6 +885,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
874
885
|
finally:
|
|
875
886
|
self.record_failures = record_failures
|
|
876
887
|
self._echo_result(test.body[-1], self.last_result)
|
|
888
|
+
return True
|
|
877
889
|
|
|
878
890
|
# ------------------------------------------------------------------
|
|
879
891
|
# Robot integration
|
|
@@ -958,7 +970,7 @@ class ConsoleInterpreter(BaseInterpreter):
|
|
|
958
970
|
while True:
|
|
959
971
|
prompt = ""
|
|
960
972
|
if self.show_prompt:
|
|
961
|
-
prompt =
|
|
973
|
+
prompt = self.PROMPT if not lines else "... "
|
|
962
974
|
|
|
963
975
|
prefill = compute_indent(lines) if lines else ""
|
|
964
976
|
try:
|
robotcode/repl/hooks.py
CHANGED
|
@@ -6,8 +6,9 @@ from robotcode.plugin import hookimpl
|
|
|
6
6
|
|
|
7
7
|
from .cli import repl, robot_debug
|
|
8
8
|
from .doc_cli import doc
|
|
9
|
+
from .session_cli import session
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@hookimpl
|
|
12
13
|
def register_cli_commands() -> List[click.Command]:
|
|
13
|
-
return [repl, robot_debug, doc]
|
|
14
|
+
return [repl, robot_debug, doc, session]
|