robotcode-repl 2.9.0.dev51__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/base_interpreter.py +7 -1
- robotcode/repl/cli.py +142 -31
- robotcode/repl/console_interpreter.py +43 -12
- robotcode/repl/hooks.py +2 -1
- robotcode/repl/prompt_toolkit_interpreter.py +2 -0
- robotcode/repl/session_cli.py +530 -0
- {robotcode_repl-2.9.0.dev51.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/METADATA +2 -2
- {robotcode_repl-2.9.0.dev51.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/RECORD +13 -11
- {robotcode_repl-2.9.0.dev51.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/WHEEL +0 -0
- {robotcode_repl-2.9.0.dev51.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from click.core import ParameterSource
|
|
9
|
+
from robot.errors import DATA_ERROR
|
|
10
|
+
|
|
11
|
+
from robotcode.core.utils.path import same_file
|
|
12
|
+
from robotcode.plugin import Application, OutputFormat, pass_application
|
|
13
|
+
from robotcode.plugin.click_helper.exit_codes import robot_exit_codes
|
|
14
|
+
from robotcode.plugin.click_helper.types import add_options
|
|
15
|
+
|
|
16
|
+
from ._session import (
|
|
17
|
+
STARTUP_LIMIT,
|
|
18
|
+
AnswerStatus,
|
|
19
|
+
Request,
|
|
20
|
+
SessionEntry,
|
|
21
|
+
SessionKind,
|
|
22
|
+
connect,
|
|
23
|
+
create_output_pipe,
|
|
24
|
+
create_sessions_dir,
|
|
25
|
+
new_session_id,
|
|
26
|
+
open_sessions,
|
|
27
|
+
project_folder,
|
|
28
|
+
read_entry,
|
|
29
|
+
read_to_end,
|
|
30
|
+
remove_session_files,
|
|
31
|
+
sessions_dir,
|
|
32
|
+
start_session_process,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if sys.platform == "win32":
|
|
36
|
+
import msvcrt
|
|
37
|
+
|
|
38
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
39
|
+
|
|
40
|
+
# How long `session start` waits for a session to take commands, when `--timeout` isn't longer.
|
|
41
|
+
_STARTUP_LIMIT = STARTUP_LIMIT
|
|
42
|
+
|
|
43
|
+
# 124 for a statement that still runs, as `timeout(1)` exits.
|
|
44
|
+
_EXIT_CODES = {AnswerStatus.PASSED: 0, AnswerStatus.FAILED: 1, AnswerStatus.RUNNING: 124}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _timeout_option(what: str) -> Callable[[F], F]:
|
|
48
|
+
return click.option(
|
|
49
|
+
"--timeout",
|
|
50
|
+
type=click.FloatRange(min=0),
|
|
51
|
+
default=60,
|
|
52
|
+
show_default=True,
|
|
53
|
+
metavar="SECONDS",
|
|
54
|
+
help=f"Wait at most SECONDS for {what}. After that, print the output so far and exit with code 124, while "
|
|
55
|
+
"the session goes on; `session wait` prints the rest.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.group(invoke_without_command=False)
|
|
60
|
+
def session() -> None:
|
|
61
|
+
"""\
|
|
62
|
+
Run a REPL or a debug run in the background and give it one input after the other.
|
|
63
|
+
|
|
64
|
+
`session start` starts the session and prints its ID, which the other commands
|
|
65
|
+
take. The session keeps running when the command has returned, until it is
|
|
66
|
+
stopped or has waited for the idle timeout without a request. When the REPL
|
|
67
|
+
exits or the debug run ends, the session stays open: `session status` tells
|
|
68
|
+
it, `session result` prints the exit code, and `session restart` starts it
|
|
69
|
+
again.
|
|
70
|
+
|
|
71
|
+
\b
|
|
72
|
+
Examples:
|
|
73
|
+
```
|
|
74
|
+
robotcode session start repl
|
|
75
|
+
robotcode session send repl-3f9a2c1e "Evaluate 1 + 1"
|
|
76
|
+
robotcode session start debug --break tests/orders.robot:19 tests
|
|
77
|
+
robotcode session send debug-7b2d9e04 ".vars --user"
|
|
78
|
+
robotcode session stop repl-3f9a2c1e
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Errors of robotcode itself exit with 252, as other commands that run Robot Framework do, and not with the 1 of an
|
|
84
|
+
# input that failed.
|
|
85
|
+
robot_exit_codes(session)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@session.group(invoke_without_command=False)
|
|
89
|
+
def start() -> None:
|
|
90
|
+
"""\
|
|
91
|
+
Start a session in the background and print its ID and its output up to its first prompt or its end.
|
|
92
|
+
|
|
93
|
+
`start` returns once the session takes commands. A session that isn't
|
|
94
|
+
ready within 60 seconds, or within `--timeout` when that is longer, is
|
|
95
|
+
ended.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_START_OPTIONS = [
|
|
100
|
+
click.option(
|
|
101
|
+
"--idle-timeout",
|
|
102
|
+
type=click.FloatRange(min=0, min_open=True),
|
|
103
|
+
default=15,
|
|
104
|
+
show_default=True,
|
|
105
|
+
metavar="MINUTES",
|
|
106
|
+
help="End the session when it has waited at its prompt for MINUTES without a request. A statement that "
|
|
107
|
+
"runs doesn't count as waiting.",
|
|
108
|
+
),
|
|
109
|
+
_timeout_option("the first prompt"),
|
|
110
|
+
click.argument("args", nargs=-1, type=click.UNPROCESSED),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@start.command(
|
|
115
|
+
name="repl",
|
|
116
|
+
context_settings={"ignore_unknown_options": True},
|
|
117
|
+
epilog="Takes the options and arguments of `robotcode repl`.",
|
|
118
|
+
)
|
|
119
|
+
@add_options(*_START_OPTIONS)
|
|
120
|
+
@pass_application
|
|
121
|
+
@click.pass_context
|
|
122
|
+
def start_repl(
|
|
123
|
+
ctx: click.Context, app: Application, idle_timeout: float, timeout: float, args: Tuple[str, ...]
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Start `robotcode repl` in the background; the session is ready at its first prompt."""
|
|
126
|
+
_start(ctx, app, SessionKind.REPL, idle_timeout, timeout, args)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@start.command(
|
|
130
|
+
name="debug",
|
|
131
|
+
context_settings={"ignore_unknown_options": True},
|
|
132
|
+
epilog="Takes the options and arguments of `robotcode robot-debug`.",
|
|
133
|
+
)
|
|
134
|
+
@add_options(*_START_OPTIONS)
|
|
135
|
+
@pass_application
|
|
136
|
+
@click.pass_context
|
|
137
|
+
def start_debug(
|
|
138
|
+
ctx: click.Context, app: Application, idle_timeout: float, timeout: float, args: Tuple[str, ...]
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Start `robotcode robot-debug` in the background; the session is ready at its first stop or at the end of the
|
|
141
|
+
run."""
|
|
142
|
+
_start(ctx, app, SessionKind.DEBUG, idle_timeout, timeout, args)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _robotcode_options(ctx: click.Context) -> List[str]:
|
|
146
|
+
"""The options given to `robotcode` itself on the command line, which the session process gets too; options from
|
|
147
|
+
the environment reach it with the environment."""
|
|
148
|
+
root = ctx.find_root()
|
|
149
|
+
args: List[str] = []
|
|
150
|
+
for param in root.command.params:
|
|
151
|
+
if not isinstance(param, click.Option) or param.name is None or param.name not in root.params:
|
|
152
|
+
continue
|
|
153
|
+
if root.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE:
|
|
154
|
+
continue
|
|
155
|
+
value = root.params[param.name]
|
|
156
|
+
if param.is_flag:
|
|
157
|
+
if value:
|
|
158
|
+
args.append(param.opts[0])
|
|
159
|
+
elif param.secondary_opts:
|
|
160
|
+
args.append(param.secondary_opts[0])
|
|
161
|
+
elif param.multiple:
|
|
162
|
+
for item in value:
|
|
163
|
+
args += [param.opts[0], str(item)]
|
|
164
|
+
elif value is not None:
|
|
165
|
+
args += [param.opts[0], str(value)]
|
|
166
|
+
return args
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _start(
|
|
170
|
+
ctx: click.Context, app: Application, kind: SessionKind, idle_timeout: float, timeout: float, args: Tuple[str, ...]
|
|
171
|
+
) -> None:
|
|
172
|
+
session_id = new_session_id(kind)
|
|
173
|
+
try:
|
|
174
|
+
create_sessions_dir()
|
|
175
|
+
except OSError as e:
|
|
176
|
+
# A sandbox, for example, can keep a command from writing there.
|
|
177
|
+
raise click.ClickException(
|
|
178
|
+
f"Can't create the files of the session in {sessions_dir()}: {e.strerror or e}."
|
|
179
|
+
) from e
|
|
180
|
+
# The process writes its standard output and standard error to the pipe and reads it at the other end.
|
|
181
|
+
read_end, write_end = create_output_pipe()
|
|
182
|
+
if sys.platform == "win32":
|
|
183
|
+
output = msvcrt.get_osfhandle(read_end)
|
|
184
|
+
else:
|
|
185
|
+
output = read_end
|
|
186
|
+
command = [
|
|
187
|
+
sys.executable,
|
|
188
|
+
"-m",
|
|
189
|
+
"robotcode.cli",
|
|
190
|
+
*_robotcode_options(ctx),
|
|
191
|
+
"--no-color",
|
|
192
|
+
"--no-pager",
|
|
193
|
+
kind.command,
|
|
194
|
+
"--serve-session",
|
|
195
|
+
session_id,
|
|
196
|
+
"--serve-output",
|
|
197
|
+
str(output),
|
|
198
|
+
"--serve-idle-timeout",
|
|
199
|
+
str(idle_timeout),
|
|
200
|
+
*args,
|
|
201
|
+
]
|
|
202
|
+
# Unbuffered, so that an answer has all the output written before it, and in UTF-8, which the session decodes.
|
|
203
|
+
env = {**os.environ, "PYTHONUNBUFFERED": "1", "PYTHONIOENCODING": "utf-8"}
|
|
204
|
+
try:
|
|
205
|
+
process = start_session_process(command, output, write_end, env)
|
|
206
|
+
finally:
|
|
207
|
+
os.close(write_end)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# `--timeout` is for the first prompt. Until the session takes commands at all, `start` waits longer, so that
|
|
211
|
+
# the other commands always reach a session whose ID it has printed.
|
|
212
|
+
deadline = time.monotonic() + timeout
|
|
213
|
+
limit = max(timeout, _STARTUP_LIMIT)
|
|
214
|
+
ready_by = time.monotonic() + limit
|
|
215
|
+
entry = read_entry(session_id)
|
|
216
|
+
while entry is None:
|
|
217
|
+
if process.poll() is not None:
|
|
218
|
+
_ended_before_ready(
|
|
219
|
+
app,
|
|
220
|
+
session_id,
|
|
221
|
+
read_to_end(read_end),
|
|
222
|
+
f"The session ended before it was ready, with exit code {process.returncode}.",
|
|
223
|
+
process.returncode,
|
|
224
|
+
)
|
|
225
|
+
if time.monotonic() >= ready_by:
|
|
226
|
+
process.kill()
|
|
227
|
+
process.wait()
|
|
228
|
+
_ended_before_ready(
|
|
229
|
+
app,
|
|
230
|
+
session_id,
|
|
231
|
+
read_to_end(read_end),
|
|
232
|
+
f"The session wasn't ready after {limit:g} seconds and was ended.",
|
|
233
|
+
DATA_ERROR,
|
|
234
|
+
)
|
|
235
|
+
time.sleep(0.05)
|
|
236
|
+
entry = read_entry(session_id)
|
|
237
|
+
finally:
|
|
238
|
+
# Once the session is ready, it reads the pipe itself.
|
|
239
|
+
os.close(read_end)
|
|
240
|
+
|
|
241
|
+
answer = _request(entry, {"request": Request.WAIT}, max(0.0, deadline - time.monotonic()))
|
|
242
|
+
if _structured(app):
|
|
243
|
+
app.print_data({"id": session_id, **_answer_data(answer)})
|
|
244
|
+
else:
|
|
245
|
+
app.echo(f"Started session {session_id}.")
|
|
246
|
+
_echo_answer(app, entry, answer)
|
|
247
|
+
app.exit(_exit_code(answer))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _ended_before_ready(app: Application, session_id: str, output: str, message: str, exit_code: int) -> None:
|
|
251
|
+
remove_session_files(session_id)
|
|
252
|
+
if output:
|
|
253
|
+
app.echo(output, nl=False)
|
|
254
|
+
app.error(message)
|
|
255
|
+
app.exit(exit_code)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@session.command()
|
|
259
|
+
@_timeout_option("the answer")
|
|
260
|
+
@click.argument("session_id", metavar="ID")
|
|
261
|
+
@click.argument("text", metavar="INPUT")
|
|
262
|
+
@pass_application
|
|
263
|
+
def send(app: Application, timeout: float, session_id: str, text: str) -> None:
|
|
264
|
+
"""\
|
|
265
|
+
Give INPUT to the prompt of the session ID and print its output up to the next prompt.
|
|
266
|
+
|
|
267
|
+
INPUT is a whole input: a statement, a text of several lines such as a FOR
|
|
268
|
+
loop with its END, or a dot-command such as `.vars`, which works as at the
|
|
269
|
+
prompt, each in a send of its own. A block that INPUT leaves unfinished
|
|
270
|
+
fails. The exit code is 0 when the statement passed, 1 when it failed, and
|
|
271
|
+
124 when it still runs after `--timeout`. When the REPL or the run ends,
|
|
272
|
+
`send` says so with its exit code; a `send` after that fails, and
|
|
273
|
+
`session restart` starts it again.
|
|
274
|
+
"""
|
|
275
|
+
entry = _entry(session_id)
|
|
276
|
+
answer = _request(entry, {"request": Request.SEND, "input": text}, timeout)
|
|
277
|
+
_echo_answer(app, entry, answer)
|
|
278
|
+
app.exit(_exit_code(answer))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@session.command()
|
|
282
|
+
@_timeout_option("the next prompt")
|
|
283
|
+
@click.argument("session_id", metavar="ID")
|
|
284
|
+
@pass_application
|
|
285
|
+
def wait(app: Application, timeout: float, session_id: str) -> None:
|
|
286
|
+
"""\
|
|
287
|
+
Wait until the session ID waits at its prompt or its REPL or run has ended, and print its output since the last
|
|
288
|
+
command.
|
|
289
|
+
|
|
290
|
+
Takes over from a `send` that returned after its `--timeout`, with the exit
|
|
291
|
+
code that `send` would have had.
|
|
292
|
+
"""
|
|
293
|
+
entry = _entry(session_id)
|
|
294
|
+
answer = _request(entry, {"request": Request.WAIT}, timeout)
|
|
295
|
+
_echo_answer(app, entry, answer)
|
|
296
|
+
app.exit(_exit_code(answer))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@session.command(name="status")
|
|
300
|
+
@click.argument("session_id", metavar="ID")
|
|
301
|
+
@pass_application
|
|
302
|
+
def session_status(app: Application, session_id: str) -> None:
|
|
303
|
+
"""\
|
|
304
|
+
Print whether the session ID runs, waits at its prompt or its REPL or run has ended: `runs`, `waits` or `ended`.
|
|
305
|
+
|
|
306
|
+
It takes none of the output of the session, which `session wait` prints.
|
|
307
|
+
"""
|
|
308
|
+
answer = _request(_entry(session_id), {"request": Request.STATE})
|
|
309
|
+
if _structured(app):
|
|
310
|
+
app.print_data({"state": answer["state"]})
|
|
311
|
+
else:
|
|
312
|
+
app.echo(str(answer["state"]))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@session.command(name="result")
|
|
316
|
+
@click.argument("session_id", metavar="ID")
|
|
317
|
+
@pass_application
|
|
318
|
+
def session_result(app: Application, session_id: str) -> None:
|
|
319
|
+
"""\
|
|
320
|
+
Print the exit code of the REPL or the run of the session ID once it has ended.
|
|
321
|
+
|
|
322
|
+
`result` itself exits with 0, as Robot Framework gives exit codes a meaning
|
|
323
|
+
of its own; before the end, it says so and exits with 252.
|
|
324
|
+
"""
|
|
325
|
+
answer = _request(_entry(session_id), {"request": Request.RESULT})
|
|
326
|
+
if _structured(app):
|
|
327
|
+
app.print_data({"exit_code": answer["exit_code"]})
|
|
328
|
+
else:
|
|
329
|
+
app.echo(str(answer["exit_code"]))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@session.command()
|
|
333
|
+
@_timeout_option("the first prompt")
|
|
334
|
+
@click.argument("session_id", metavar="ID")
|
|
335
|
+
@pass_application
|
|
336
|
+
def restart(app: Application, timeout: float, session_id: str) -> None:
|
|
337
|
+
"""\
|
|
338
|
+
Start the REPL or the run of the session ID again in a new process, and print its output up to its first prompt
|
|
339
|
+
or its end.
|
|
340
|
+
|
|
341
|
+
A REPL or a run that hasn't ended is ended as with `session stop`, and its
|
|
342
|
+
output since the last command comes first. The session keeps its ID, and
|
|
343
|
+
the new process starts with the options of `session start` and the
|
|
344
|
+
breakpoints as they are.
|
|
345
|
+
"""
|
|
346
|
+
entry = _entry(session_id)
|
|
347
|
+
# The old process ends the REPL or the run as `stop` does, then waits for the new process to take commands.
|
|
348
|
+
answer = _request(entry, {"request": Request.RESTART}, 2 * _STARTUP_LIMIT)
|
|
349
|
+
before = answer.get("output", "")
|
|
350
|
+
if not answer.get("restarted"):
|
|
351
|
+
if before:
|
|
352
|
+
app.echo(before, nl=False)
|
|
353
|
+
if "restart_failed" in answer:
|
|
354
|
+
raise click.ClickException(str(answer["restart_failed"]))
|
|
355
|
+
raise click.ClickException(f"Session '{session_id}' hasn't restarted yet; `session status` tells when it has.")
|
|
356
|
+
# The file names the new process now.
|
|
357
|
+
entry = _entry(session_id)
|
|
358
|
+
answer = _request(entry, {"request": Request.WAIT}, timeout)
|
|
359
|
+
if _structured(app):
|
|
360
|
+
app.print_data({"id": session_id, **_answer_data({**answer, "output": before + answer.get("output", "")})})
|
|
361
|
+
else:
|
|
362
|
+
if before:
|
|
363
|
+
app.echo(before, nl=not before.endswith("\n"))
|
|
364
|
+
app.echo(f"Restarted session {session_id}.")
|
|
365
|
+
_echo_answer(app, entry, answer)
|
|
366
|
+
app.exit(_exit_code(answer))
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@session.command()
|
|
370
|
+
@click.argument("session_id", metavar="ID")
|
|
371
|
+
def interrupt(session_id: str) -> None:
|
|
372
|
+
"""\
|
|
373
|
+
Interrupt the session ID as Ctrl-C does at its terminal.
|
|
374
|
+
|
|
375
|
+
In a REPL, the statement that runs fails; with the debugger attached, the run
|
|
376
|
+
pauses at the next keyword. `session wait` prints what follows.
|
|
377
|
+
"""
|
|
378
|
+
_request(_entry(session_id), {"request": Request.INTERRUPT})
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@session.command()
|
|
382
|
+
@click.option("--project", "of_project", is_flag=True, help="End the sessions of the project of the current folder.")
|
|
383
|
+
@click.option("--all", "all_sessions", is_flag=True, help="End all your sessions.")
|
|
384
|
+
@click.argument("session_id", metavar="[ID]", required=False)
|
|
385
|
+
@pass_application
|
|
386
|
+
def stop(app: Application, of_project: bool, all_sessions: bool, session_id: Optional[str]) -> None:
|
|
387
|
+
"""\
|
|
388
|
+
End the session ID and print its output since the last command, or end several sessions.
|
|
389
|
+
|
|
390
|
+
`--project` ends the sessions of the project the current folder belongs to,
|
|
391
|
+
the folder RobotCode finds for its `robot.toml`; a session belongs to the
|
|
392
|
+
project of the folder `session start` ran in. `--all` ends all your
|
|
393
|
+
sessions. A debug run that has not ended is aborted as with `.abort`.
|
|
394
|
+
"""
|
|
395
|
+
if (session_id is not None) + of_project + all_sessions != 1:
|
|
396
|
+
raise click.UsageError("Give the ID of a session, --project or --all.")
|
|
397
|
+
if session_id is not None:
|
|
398
|
+
answer = _stop(_entry(session_id))
|
|
399
|
+
if answer.get("output"):
|
|
400
|
+
app.echo(answer["output"], nl=False)
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
entries = open_sessions()
|
|
404
|
+
if of_project:
|
|
405
|
+
project = project_folder(app)
|
|
406
|
+
entries = [entry for entry in entries if same_file(entry.project, project)]
|
|
407
|
+
if not entries:
|
|
408
|
+
app.echo("No session is open.")
|
|
409
|
+
for entry in entries:
|
|
410
|
+
# It can have ended since it was listed.
|
|
411
|
+
if read_entry(entry.id) is not None:
|
|
412
|
+
_stop(entry)
|
|
413
|
+
app.echo(f"Stopped session {entry.id}.")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _stop(entry: SessionEntry) -> Dict[str, Any]:
|
|
417
|
+
answer = _request(entry, {"request": Request.STOP}, 60)
|
|
418
|
+
if not answer.get("stopped"):
|
|
419
|
+
raise click.ClickException(f"Session '{entry.id}' hasn't ended yet; a second `session stop` ends it at once.")
|
|
420
|
+
return answer
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@session.command(name="list")
|
|
424
|
+
@pass_application
|
|
425
|
+
def list_sessions(app: Application) -> None:
|
|
426
|
+
"""List the open sessions with their project folder, whether they run, wait or their REPL or run has ended, and
|
|
427
|
+
the time left until they end for being idle."""
|
|
428
|
+
entries = open_sessions()
|
|
429
|
+
if _structured(app):
|
|
430
|
+
app.print_data(
|
|
431
|
+
[
|
|
432
|
+
{
|
|
433
|
+
"id": entry.id,
|
|
434
|
+
"project": entry.project,
|
|
435
|
+
"state": entry.state,
|
|
436
|
+
"time_left": None if (left := entry.time_left()) is None else round(left),
|
|
437
|
+
}
|
|
438
|
+
for entry in entries
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
if not entries:
|
|
443
|
+
app.echo("No session is open.")
|
|
444
|
+
return
|
|
445
|
+
rows = [("ID", "PROJECT", "STATE", "IDLE TIME LEFT")] + [
|
|
446
|
+
(entry.id, entry.project, entry.state, _minutes(entry.time_left())) for entry in entries
|
|
447
|
+
]
|
|
448
|
+
widths = [max(len(row[column]) for row in rows) for column in range(3)]
|
|
449
|
+
for row in rows:
|
|
450
|
+
app.echo(" ".join([*(cell.ljust(width) for cell, width in zip(row, widths)), row[3]]))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _minutes(seconds: Optional[float]) -> str:
|
|
454
|
+
if seconds is None:
|
|
455
|
+
return "-"
|
|
456
|
+
minutes, seconds = divmod(round(seconds), 60)
|
|
457
|
+
return f"{minutes}:{seconds:02d}"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _entry(session_id: str) -> SessionEntry:
|
|
461
|
+
entry = read_entry(session_id)
|
|
462
|
+
if entry is None:
|
|
463
|
+
raise click.ClickException(f"No session '{session_id}' is open.")
|
|
464
|
+
return entry
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _request(entry: SessionEntry, message: Dict[str, Any], timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
468
|
+
"""The answer of the session `entry` to `message`; after `timeout` seconds, the output so far."""
|
|
469
|
+
answer = _ask(entry, message, timeout)
|
|
470
|
+
if answer.get("moved"):
|
|
471
|
+
# The session has just restarted in a new process, which its file names now.
|
|
472
|
+
moved_to = read_entry(entry.id)
|
|
473
|
+
if moved_to is None or moved_to.address == entry.address:
|
|
474
|
+
raise click.ClickException(f"No session '{entry.id}' is open.")
|
|
475
|
+
answer = _ask(moved_to, message, timeout)
|
|
476
|
+
if "error" in answer:
|
|
477
|
+
raise click.ClickException(str(answer["error"]))
|
|
478
|
+
return answer
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _ask(entry: SessionEntry, message: Dict[str, Any], timeout: Optional[float]) -> Dict[str, Any]:
|
|
482
|
+
"""Send `message` to the process that `entry` names and return its answer; after `timeout` seconds, the output so
|
|
483
|
+
far."""
|
|
484
|
+
try:
|
|
485
|
+
with connect(entry) as connection:
|
|
486
|
+
connection.send(message)
|
|
487
|
+
if timeout is not None and not connection.poll(timeout):
|
|
488
|
+
try:
|
|
489
|
+
connection.send({"request": Request.STATUS})
|
|
490
|
+
except OSError:
|
|
491
|
+
# The answer came just now, and the session has closed the connection.
|
|
492
|
+
pass
|
|
493
|
+
answer: Dict[str, Any] = connection.recv()
|
|
494
|
+
except (OSError, EOFError, multiprocessing.AuthenticationError) as e:
|
|
495
|
+
raise click.ClickException(f"Session '{entry.id}' doesn't answer: {e or type(e).__name__}") from e
|
|
496
|
+
return answer
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _structured(app: Application) -> bool:
|
|
500
|
+
return app.config.output_format not in (None, OutputFormat.TEXT)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _answer_data(answer: Dict[str, Any]) -> Dict[str, Any]:
|
|
504
|
+
data = {"output": answer.get("output", ""), "status": answer.get("status")}
|
|
505
|
+
if "exit_code" in answer:
|
|
506
|
+
data["exit_code"] = answer["exit_code"]
|
|
507
|
+
return data
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _echo_answer(app: Application, entry: SessionEntry, answer: Dict[str, Any]) -> None:
|
|
511
|
+
if _structured(app):
|
|
512
|
+
app.print_data(_answer_data(answer))
|
|
513
|
+
return
|
|
514
|
+
output = answer.get("output", "")
|
|
515
|
+
if output:
|
|
516
|
+
app.echo(output, nl=False)
|
|
517
|
+
if not output.endswith("\n"):
|
|
518
|
+
app.echo("")
|
|
519
|
+
if "exit_code" in answer:
|
|
520
|
+
app.echo(f"{entry.kind.what} has ended with exit code {answer['exit_code']}.", err=True)
|
|
521
|
+
elif answer.get("status") == AnswerStatus.RUNNING:
|
|
522
|
+
app.echo(f"It still runs; `robotcode session wait {entry.id}` prints the rest.", err=True)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _exit_code(answer: Dict[str, Any]) -> int:
|
|
526
|
+
"""How the input went; the exit code of the REPL or the run is in the output."""
|
|
527
|
+
status = answer.get("status")
|
|
528
|
+
if not isinstance(status, AnswerStatus):
|
|
529
|
+
return int(DATA_ERROR)
|
|
530
|
+
return _EXIT_CODES[status]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: robotcode-repl
|
|
3
|
-
Version: 2.9.0-dev.
|
|
3
|
+
Version: 2.9.0-dev.74
|
|
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.
|
|
30
|
+
Requires-Dist: robotcode-runner==2.9.0-dev.74
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
33
|
# robotcode-repl
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
robotcode/repl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
robotcode/repl/__version__.py,sha256=
|
|
2
|
+
robotcode/repl/__version__.py,sha256=x1AYp8_TYii5MaUcQCE8tgBdUh4FTuZIcutnrqGidP8,29
|
|
3
3
|
robotcode/repl/_backends.py,sha256=s7PV-HBhnAvwjn2le0cbOJ5hwLb4MjahUo6KWtRC3lc,110
|
|
4
4
|
robotcode/repl/_indent.py,sha256=jwFKaHNvTWYpUwIWHu6JHoaCz7ljOxneR5sBrKcAjpI,3303
|
|
5
5
|
robotcode/repl/_keyword_lookup.py,sha256=c8InMUDGKsw4wYmiqJzBr9GGxPC-59sRZHI0C-GWjFA,7748
|
|
6
6
|
robotcode/repl/_progress.py,sha256=BaSbDGOy5UOXvD855Kd-lDrYbqPrYIWRVs507vJznK4,2586
|
|
7
|
+
robotcode/repl/_session.py,sha256=3L1vzFpdQULMaqKzwmdlXJdpazqg8OkORR7cRX8breY,47702
|
|
7
8
|
robotcode/repl/_session_export.py,sha256=sTSkLEMoSxNQOsII3xfDY8h-nV-mrem-4h6Iug7VQv4,2841
|
|
8
|
-
robotcode/repl/base_interpreter.py,sha256=
|
|
9
|
-
robotcode/repl/cli.py,sha256=
|
|
10
|
-
robotcode/repl/console_interpreter.py,sha256=
|
|
9
|
+
robotcode/repl/base_interpreter.py,sha256=ep1xqH5QCtkn2U2P77IJQCk_Q5TTBsYOcRjGC5hV5cY,30176
|
|
10
|
+
robotcode/repl/cli.py,sha256=RcjFT1yvcDy8HMzVO6r0cuqWCKqFofrs7XdkJPUpp1E,26878
|
|
11
|
+
robotcode/repl/console_interpreter.py,sha256=VNCYLHRrW4LPtj37q27_dq75OHPlayCWPurw4EO9k0o,89411
|
|
11
12
|
robotcode/repl/doc_cli.py,sha256=lJ1KrS-oxqW32XyRSFiyhSrtKVvzA7TuMclMuZRYIrE,23221
|
|
12
|
-
robotcode/repl/hooks.py,sha256=
|
|
13
|
-
robotcode/repl/prompt_toolkit_interpreter.py,sha256=
|
|
13
|
+
robotcode/repl/hooks.py,sha256=JD0A6vqQ0pcJx45l5w0wq-bWqnNkBV_6tPugtMZN4yo,280
|
|
14
|
+
robotcode/repl/prompt_toolkit_interpreter.py,sha256=fq0uRsWucGhjKwGekUShz4r4Vt0nvbeAa-phLvI6WNw,13416
|
|
14
15
|
robotcode/repl/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
|
|
15
16
|
robotcode/repl/run.py,sha256=IWeiGZm56vkLn-hykP-_mWLrdpDciNyMk33_dT-6EJA,5327
|
|
17
|
+
robotcode/repl/session_cli.py,sha256=m5qFjusybPP4c25qc5FSmRNjk1Y3nshXzBWJnAC-Z2E,19825
|
|
16
18
|
robotcode/repl/Repl/__init__.py,sha256=-j2WxY3MzdLfsMvpa2wMs6WVF_eeJygdshtMN50r67U,43
|
|
17
19
|
robotcode/repl/Repl/repl.py,sha256=k3vCeg9z_4cb2dgZ4YKHl4TD599gumVkQMImAzGl20Y,1628
|
|
18
20
|
robotcode/repl/_debug/__init__.py,sha256=uQNyC3pdlpJZI3FRKzcp9BYEz1QtfBR--gsgKNomxrQ,908
|
|
19
|
-
robotcode/repl/_debug/controller.py,sha256=
|
|
21
|
+
robotcode/repl/_debug/controller.py,sha256=FQY92Jd4gRWcIZ4grZ7WY9cQ9g09SUnQPxmp3bo385Q,34114
|
|
20
22
|
robotcode/repl/_debug/types.py,sha256=3Vbecx44pruY0fXptEowKr_tYhljXSb6Sbr81YS22Mk,4597
|
|
21
23
|
robotcode/repl/_pt/__init__.py,sha256=k1bJgYbKwp5ScYO8fSx0BVy8XW9Aw_8o0jTOWQTQdX8,411
|
|
22
24
|
robotcode/repl/_pt/completion.py,sha256=BJD6sASKu1EfW0QPNKIDRc9O-8ttErXvxvnFb10lAxk,23318
|
|
@@ -24,7 +26,7 @@ robotcode/repl/_pt/components.py,sha256=kuYVXJc4bFJq4b3-JtAtBYymvS7ssSwlKDldWYFG
|
|
|
24
26
|
robotcode/repl/_pt/doc_viewer.py,sha256=iazNOQEvRygPSUyqKH2qOYShzdYqd7vufZcxPziJx8Q,71648
|
|
25
27
|
robotcode/repl/_pt/history.py,sha256=Rvbdu4LtbYk9dPgYJY7D1JpfyT1SETUdNmtevxo21H4,1591
|
|
26
28
|
robotcode/repl/_pt/lexer.py,sha256=YXtDMWQXPIocwceMXFlvULWyGXCDmqeAtTer_Ecuvwk,13925
|
|
27
|
-
robotcode_repl-2.9.0.
|
|
28
|
-
robotcode_repl-2.9.0.
|
|
29
|
-
robotcode_repl-2.9.0.
|
|
30
|
-
robotcode_repl-2.9.0.
|
|
29
|
+
robotcode_repl-2.9.0.dev74.dist-info/METADATA,sha256=Fghn58v8ilKZ1gaFB1Xbq9t2EVfapSCk_6fpIiANR0w,2833
|
|
30
|
+
robotcode_repl-2.9.0.dev74.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
31
|
+
robotcode_repl-2.9.0.dev74.dist-info/entry_points.txt,sha256=lI4idVOEmSWxmmioejg-_QvHXTLySK6ZxJ13R_v8ykU,40
|
|
32
|
+
robotcode_repl-2.9.0.dev74.dist-info/RECORD,,
|
|
File without changes
|
{robotcode_repl-2.9.0.dev51.dist-info → robotcode_repl-2.9.0.dev74.dist-info}/entry_points.txt
RENAMED
|
File without changes
|