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/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
- interpreter = _pick_interpreter(
504
- app=app,
505
- files=list(files),
506
- show_keywords=show_keywords,
507
- inspect=inspect,
508
- no_history=no_history,
509
- backend=backend,
510
- show_prompt=_resolve_show_prompt(show_prompt),
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
- result = run_repl(
529
- interpreter=interpreter,
530
- app=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 = _pick_interpreter(
605
- app=app,
606
- files=[],
607
- show_keywords=False,
608
- inspect=False,
609
- no_history=no_history,
610
- backend=backend,
611
- show_prompt=_is_interactive_stdin(),
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
- body = f"{title}\n{'=' * len(title)}\n\n{markdown}"
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._echo("") # break off any in-progress runner console status line
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) -> None:
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 = ">>> " if not lines else "... "
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]