clikernel 0.1.0__tar.gz → 0.1.2__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.
- clikernel-0.1.2/CHANGELOG.md +20 -0
- {clikernel-0.1.0/clikernel.egg-info → clikernel-0.1.2}/PKG-INFO +11 -7
- {clikernel-0.1.0 → clikernel-0.1.2}/README.md +10 -6
- clikernel-0.1.2/clikernel/__init__.py +3 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel/cli.py +30 -5
- {clikernel-0.1.0 → clikernel-0.1.2/clikernel.egg-info}/PKG-INFO +11 -7
- clikernel-0.1.2/tests/test_cli.py +260 -0
- clikernel-0.1.0/CHANGELOG.md +0 -6
- clikernel-0.1.0/clikernel/__init__.py +0 -1
- clikernel-0.1.0/tests/test_cli.py +0 -142
- {clikernel-0.1.0 → clikernel-0.1.2}/LICENSE +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/MANIFEST.in +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel.egg-info/SOURCES.txt +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel.egg-info/dependency_links.txt +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel.egg-info/entry_points.txt +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel.egg-info/requires.txt +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/clikernel.egg-info/top_level.txt +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/pyproject.toml +0 -0
- {clikernel-0.1.0 → clikernel-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- do not remove -->
|
|
2
|
+
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- Add ONLCR terminal flag control ([#2](https://github.com/AnswerDotAI/clikernel/issues/2))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## 0.1.1
|
|
11
|
+
|
|
12
|
+
### New Features
|
|
13
|
+
|
|
14
|
+
- Use a fixed per-session delimiter and emit `.` ack before each response ([#1](https://github.com/AnswerDotAI/clikernel/issues/1))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 0.1.0
|
|
18
|
+
|
|
19
|
+
- Initial release
|
|
20
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A tiny stdin/stdout IPython worker
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -34,7 +34,7 @@ This is especially useful with [pyskills](https://github.com/AnswerDotAI/pyskill
|
|
|
34
34
|
|
|
35
35
|
## Protocol
|
|
36
36
|
|
|
37
|
-
On startup, `clikernel` prints loading status followed by a
|
|
37
|
+
On startup, `clikernel` prints loading status followed by a random session delimiter:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
40
|
please wait, loading...
|
|
@@ -42,13 +42,17 @@ loading complete. first delimiter:
|
|
|
42
42
|
--aB3x9
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
That delimiter stays the same until the worker exits.
|
|
46
|
+
|
|
45
47
|
Send one line to execute it immediately:
|
|
46
48
|
|
|
47
49
|
```text
|
|
48
50
|
1+1
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
Each complete request is acknowledged with `.` before execution starts. Send `exit()` or `quit()` to receive an acknowledgement, a final delimiter, and stop the worker.
|
|
54
|
+
|
|
55
|
+
For multiline code, send `--` on its own line, then the code, then the session delimiter exactly:
|
|
52
56
|
|
|
53
57
|
```text
|
|
54
58
|
--
|
|
@@ -59,11 +63,12 @@ f(2)
|
|
|
59
63
|
--aB3x9
|
|
60
64
|
```
|
|
61
65
|
|
|
62
|
-
After execution, `clikernel` prints the rendered output
|
|
66
|
+
After execution, `clikernel` prints the acknowledgement, the rendered output, and the session delimiter:
|
|
63
67
|
|
|
64
68
|
```text
|
|
69
|
+
.
|
|
65
70
|
3
|
|
66
|
-
--
|
|
71
|
+
--aB3x9
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output is printed directly. Multiple outputs use raw XML-ish tags, for example `<stdout>`, `<display_data mime="text/markdown">`, and `<execute_result>`.
|
|
@@ -74,7 +79,7 @@ Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output
|
|
|
74
79
|
|
|
75
80
|
`clikernel` is built for a client that reads stdout as tokens. Local echo is disabled when stdin is a TTY. The client already knows the code it sent, so echoing it back only makes the LLM read slow, expensive tokens that add no information.
|
|
76
81
|
|
|
77
|
-
Each response ends with
|
|
82
|
+
Each complete request prints `.` on its own line before execution starts. That gives the client a cheap early byte to read, which is useful when the request will run for a while. Each response ends with the same delimiter on its own line. The client can read until that line appears instead of parsing prompts or waiting and guessing. The delimiter is random per process, so it is unlikely to appear in generated code, copied logs, examples, or earlier transcript text. Keeping it fixed for the session means a client does not get stuck just because it missed a rotated delimiter.
|
|
78
83
|
|
|
79
84
|
Startup messages appear before the first delimiter. After that first delimiter, the stream follows the request-response protocol. Outputs are rendered as concise text, using unescaped XML if required when there's multiple outputs.
|
|
80
85
|
|
|
@@ -100,4 +105,3 @@ ship-gh
|
|
|
100
105
|
ship-pypi
|
|
101
106
|
ship-bump # dev release always later than prod release
|
|
102
107
|
```
|
|
103
|
-
|
|
@@ -14,7 +14,7 @@ This is especially useful with [pyskills](https://github.com/AnswerDotAI/pyskill
|
|
|
14
14
|
|
|
15
15
|
## Protocol
|
|
16
16
|
|
|
17
|
-
On startup, `clikernel` prints loading status followed by a
|
|
17
|
+
On startup, `clikernel` prints loading status followed by a random session delimiter:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
please wait, loading...
|
|
@@ -22,13 +22,17 @@ loading complete. first delimiter:
|
|
|
22
22
|
--aB3x9
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
That delimiter stays the same until the worker exits.
|
|
26
|
+
|
|
25
27
|
Send one line to execute it immediately:
|
|
26
28
|
|
|
27
29
|
```text
|
|
28
30
|
1+1
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Each complete request is acknowledged with `.` before execution starts. Send `exit()` or `quit()` to receive an acknowledgement, a final delimiter, and stop the worker.
|
|
34
|
+
|
|
35
|
+
For multiline code, send `--` on its own line, then the code, then the session delimiter exactly:
|
|
32
36
|
|
|
33
37
|
```text
|
|
34
38
|
--
|
|
@@ -39,11 +43,12 @@ f(2)
|
|
|
39
43
|
--aB3x9
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
After execution, `clikernel` prints the rendered output
|
|
46
|
+
After execution, `clikernel` prints the acknowledgement, the rendered output, and the session delimiter:
|
|
43
47
|
|
|
44
48
|
```text
|
|
49
|
+
.
|
|
45
50
|
3
|
|
46
|
-
--
|
|
51
|
+
--aB3x9
|
|
47
52
|
```
|
|
48
53
|
|
|
49
54
|
Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output is printed directly. Multiple outputs use raw XML-ish tags, for example `<stdout>`, `<display_data mime="text/markdown">`, and `<execute_result>`.
|
|
@@ -54,7 +59,7 @@ Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output
|
|
|
54
59
|
|
|
55
60
|
`clikernel` is built for a client that reads stdout as tokens. Local echo is disabled when stdin is a TTY. The client already knows the code it sent, so echoing it back only makes the LLM read slow, expensive tokens that add no information.
|
|
56
61
|
|
|
57
|
-
Each response ends with
|
|
62
|
+
Each complete request prints `.` on its own line before execution starts. That gives the client a cheap early byte to read, which is useful when the request will run for a while. Each response ends with the same delimiter on its own line. The client can read until that line appears instead of parsing prompts or waiting and guessing. The delimiter is random per process, so it is unlikely to appear in generated code, copied logs, examples, or earlier transcript text. Keeping it fixed for the session means a client does not get stuck just because it missed a rotated delimiter.
|
|
58
63
|
|
|
59
64
|
Startup messages appear before the first delimiter. After that first delimiter, the stream follows the request-response protocol. Outputs are rendered as concise text, using unescaped XML if required when there's multiple outputs.
|
|
60
65
|
|
|
@@ -80,4 +85,3 @@ ship-gh
|
|
|
80
85
|
ship-pypi
|
|
81
86
|
ship-bump # dev release always later than prod release
|
|
82
87
|
```
|
|
83
|
-
|
|
@@ -48,9 +48,18 @@ def _execute(shell, code):
|
|
|
48
48
|
return ("error" if shell.exc else "ok"), render_text(outputs)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _request_exit(shell): shell._clikernel_exit = True
|
|
52
|
+
|
|
53
|
+
|
|
51
54
|
def _make_shell():
|
|
52
55
|
from execnb.shell import CaptureShell
|
|
53
|
-
|
|
56
|
+
shell = CaptureShell(mpl_format=None, history=False)
|
|
57
|
+
shell._clikernel_exit = False
|
|
58
|
+
shell.ask_exit = lambda: _request_exit(shell)
|
|
59
|
+
return shell
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _should_exit(shell): return getattr(shell, "_clikernel_exit", False)
|
|
54
63
|
|
|
55
64
|
|
|
56
65
|
def _disable_echo():
|
|
@@ -66,10 +75,24 @@ def _disable_echo():
|
|
|
66
75
|
def _restore_echo(state):
|
|
67
76
|
if state: termios.tcsetattr(state[0], termios.TCSADRAIN, state[1])
|
|
68
77
|
|
|
78
|
+
def _disable_output_newline_translation():
|
|
79
|
+
if not sys.__stdout__.isatty(): return None
|
|
80
|
+
fd = sys.__stdout__.fileno()
|
|
81
|
+
attrs = termios.tcgetattr(fd)
|
|
82
|
+
new_attrs = attrs[:]
|
|
83
|
+
new_attrs[1] &= ~termios.ONLCR
|
|
84
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, new_attrs)
|
|
85
|
+
return fd, attrs
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _restore_termios(state):
|
|
89
|
+
if state: termios.tcsetattr(state[0], termios.TCSADRAIN, state[1])
|
|
90
|
+
|
|
69
91
|
|
|
70
92
|
def main():
|
|
71
|
-
echo_state = _disable_echo()
|
|
72
93
|
shell = _make_shell()
|
|
94
|
+
output_state = _disable_output_newline_translation()
|
|
95
|
+
echo_state = _disable_echo()
|
|
73
96
|
delim = _new_delim()
|
|
74
97
|
print("loading complete. first delimiter:", flush=True)
|
|
75
98
|
_write_response(delim)
|
|
@@ -79,15 +102,17 @@ def main():
|
|
|
79
102
|
if line == _MULTILINE:
|
|
80
103
|
code, err = _read_block(sys.stdin, delim)
|
|
81
104
|
if err:
|
|
82
|
-
delim = _new_delim()
|
|
83
105
|
_write_response(delim, _format_error("protocol-error", err))
|
|
84
106
|
continue
|
|
85
107
|
else: code = line
|
|
108
|
+
print(".", flush=True)
|
|
86
109
|
try: _, outputs = _execute(shell, code)
|
|
87
110
|
except BaseException: outputs = _format_error("internal-error", traceback.format_exc())
|
|
88
|
-
delim = _new_delim()
|
|
89
111
|
_write_response(delim, outputs)
|
|
90
|
-
|
|
112
|
+
if _should_exit(shell): break
|
|
113
|
+
finally:
|
|
114
|
+
_restore_echo(echo_state)
|
|
115
|
+
_restore_termios(output_state)
|
|
91
116
|
|
|
92
117
|
|
|
93
118
|
if __name__ == "__main__": main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A tiny stdin/stdout IPython worker
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -34,7 +34,7 @@ This is especially useful with [pyskills](https://github.com/AnswerDotAI/pyskill
|
|
|
34
34
|
|
|
35
35
|
## Protocol
|
|
36
36
|
|
|
37
|
-
On startup, `clikernel` prints loading status followed by a
|
|
37
|
+
On startup, `clikernel` prints loading status followed by a random session delimiter:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
40
|
please wait, loading...
|
|
@@ -42,13 +42,17 @@ loading complete. first delimiter:
|
|
|
42
42
|
--aB3x9
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
That delimiter stays the same until the worker exits.
|
|
46
|
+
|
|
45
47
|
Send one line to execute it immediately:
|
|
46
48
|
|
|
47
49
|
```text
|
|
48
50
|
1+1
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
Each complete request is acknowledged with `.` before execution starts. Send `exit()` or `quit()` to receive an acknowledgement, a final delimiter, and stop the worker.
|
|
54
|
+
|
|
55
|
+
For multiline code, send `--` on its own line, then the code, then the session delimiter exactly:
|
|
52
56
|
|
|
53
57
|
```text
|
|
54
58
|
--
|
|
@@ -59,11 +63,12 @@ f(2)
|
|
|
59
63
|
--aB3x9
|
|
60
64
|
```
|
|
61
65
|
|
|
62
|
-
After execution, `clikernel` prints the rendered output
|
|
66
|
+
After execution, `clikernel` prints the acknowledgement, the rendered output, and the session delimiter:
|
|
63
67
|
|
|
64
68
|
```text
|
|
69
|
+
.
|
|
65
70
|
3
|
|
66
|
-
--
|
|
71
|
+
--aB3x9
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output is printed directly. Multiple outputs use raw XML-ish tags, for example `<stdout>`, `<display_data mime="text/markdown">`, and `<execute_result>`.
|
|
@@ -74,7 +79,7 @@ Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output
|
|
|
74
79
|
|
|
75
80
|
`clikernel` is built for a client that reads stdout as tokens. Local echo is disabled when stdin is a TTY. The client already knows the code it sent, so echoing it back only makes the LLM read slow, expensive tokens that add no information.
|
|
76
81
|
|
|
77
|
-
Each response ends with
|
|
82
|
+
Each complete request prints `.` on its own line before execution starts. That gives the client a cheap early byte to read, which is useful when the request will run for a while. Each response ends with the same delimiter on its own line. The client can read until that line appears instead of parsing prompts or waiting and guessing. The delimiter is random per process, so it is unlikely to appear in generated code, copied logs, examples, or earlier transcript text. Keeping it fixed for the session means a client does not get stuck just because it missed a rotated delimiter.
|
|
78
83
|
|
|
79
84
|
Startup messages appear before the first delimiter. After that first delimiter, the stream follows the request-response protocol. Outputs are rendered as concise text, using unescaped XML if required when there's multiple outputs.
|
|
80
85
|
|
|
@@ -100,4 +105,3 @@ ship-gh
|
|
|
100
105
|
ship-pypi
|
|
101
106
|
ship-bump # dev release always later than prod release
|
|
102
107
|
```
|
|
103
|
-
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import os,pty,re,select,shutil,subprocess,tempfile,time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
DELIM_RE = re.compile(r"--[A-Za-z0-9]{5}")
|
|
6
|
+
TIMEOUT = 5
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _failure_detail(proc):
|
|
10
|
+
if proc.stderr is None: return ""
|
|
11
|
+
ready, _, _ = select.select([proc.stderr], [], [], 0)
|
|
12
|
+
if not ready: return ""
|
|
13
|
+
err = os.read(proc.stderr.fileno(), 65536).decode("utf-8", "replace")
|
|
14
|
+
return f"\nstderr:\n{err}" if err else ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _readline(proc, timeout):
|
|
18
|
+
assert proc.stdout is not None
|
|
19
|
+
buf = proc._stdout_buffer
|
|
20
|
+
while b"\n" not in buf:
|
|
21
|
+
ready, _, _ = select.select([proc.stdout], [], [], timeout)
|
|
22
|
+
if not ready: raise AssertionError(f"timed out waiting for clikernel output{_failure_detail(proc)}")
|
|
23
|
+
chunk = os.read(proc.stdout.fileno(), 4096)
|
|
24
|
+
assert chunk, _failure_detail(proc)
|
|
25
|
+
buf.extend(chunk)
|
|
26
|
+
line, _, rest = buf.partition(b"\n")
|
|
27
|
+
proc._stdout_buffer = bytearray(rest)
|
|
28
|
+
return line.decode("utf-8", "replace") + "\n"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_until_ready(proc, timeout=TIMEOUT):
|
|
32
|
+
lines = []
|
|
33
|
+
deadline = time.monotonic() + timeout
|
|
34
|
+
while True:
|
|
35
|
+
remaining = deadline - time.monotonic()
|
|
36
|
+
if remaining <= 0: raise AssertionError(f"timed out waiting for ready delimiter{_failure_detail(proc)}")
|
|
37
|
+
line = _readline(proc, remaining)
|
|
38
|
+
s = line.rstrip("\n")
|
|
39
|
+
if DELIM_RE.fullmatch(s): return "".join(lines), s
|
|
40
|
+
lines.append(line)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def start_kernel(tmp_path):
|
|
44
|
+
cmd = shutil.which("clikernel")
|
|
45
|
+
assert cmd, "clikernel console script is not on PATH"
|
|
46
|
+
env = os.environ.copy()
|
|
47
|
+
state = os.path.join(tempfile.gettempdir(), f"clikernel-{os.getuid()}")
|
|
48
|
+
env["CLIKERNEL_STATE_DIR"] = str(tmp_path / "state")
|
|
49
|
+
env.setdefault("IPYTHONDIR", os.path.join(state, "ipython"))
|
|
50
|
+
env.setdefault("MPLCONFIGDIR", os.path.join(state, "matplotlib"))
|
|
51
|
+
env.setdefault("MPLBACKEND", "Agg")
|
|
52
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
53
|
+
proc = subprocess.Popen([cmd], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
|
54
|
+
proc._stdout_buffer = bytearray()
|
|
55
|
+
return proc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def start_kernel_pty(tmp_path):
|
|
59
|
+
cmd = shutil.which("clikernel")
|
|
60
|
+
assert cmd, "clikernel console script is not on PATH"
|
|
61
|
+
env = os.environ.copy()
|
|
62
|
+
state = os.path.join(tempfile.gettempdir(), f"clikernel-{os.getuid()}")
|
|
63
|
+
env["CLIKERNEL_STATE_DIR"] = str(tmp_path / "state")
|
|
64
|
+
env.setdefault("IPYTHONDIR", os.path.join(state, "ipython"))
|
|
65
|
+
env.setdefault("MPLCONFIGDIR", os.path.join(state, "matplotlib"))
|
|
66
|
+
env.setdefault("MPLBACKEND", "Agg")
|
|
67
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
68
|
+
master, slave = pty.openpty()
|
|
69
|
+
try: proc = subprocess.Popen([cmd], stdin=slave, stdout=slave, stderr=subprocess.PIPE, env=env, close_fds=True)
|
|
70
|
+
finally: os.close(slave)
|
|
71
|
+
proc._pty_master = master
|
|
72
|
+
proc._pty_buffer = bytearray()
|
|
73
|
+
return proc
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def stop_kernel(proc):
|
|
77
|
+
if proc.stdin is not None:
|
|
78
|
+
try: proc.stdin.close()
|
|
79
|
+
except OSError: pass
|
|
80
|
+
if hasattr(proc, "_pty_master"):
|
|
81
|
+
try: os.close(proc._pty_master)
|
|
82
|
+
except OSError: pass
|
|
83
|
+
if proc.poll() is None:
|
|
84
|
+
try: proc.wait(timeout=1)
|
|
85
|
+
except subprocess.TimeoutExpired:
|
|
86
|
+
proc.terminate()
|
|
87
|
+
try: proc.wait(timeout=1)
|
|
88
|
+
except subprocess.TimeoutExpired:
|
|
89
|
+
proc.kill()
|
|
90
|
+
proc.wait(timeout=1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def send(proc, text, timeout=TIMEOUT):
|
|
94
|
+
assert proc.stdin is not None
|
|
95
|
+
proc.stdin.write(text.encode("utf-8"))
|
|
96
|
+
proc.stdin.flush()
|
|
97
|
+
assert _readline(proc, timeout) == ".\n"
|
|
98
|
+
return read_until_ready(proc, timeout)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_ptyline(proc, timeout):
|
|
102
|
+
buf = proc._pty_buffer
|
|
103
|
+
while b"\n" not in buf:
|
|
104
|
+
ready, _, _ = select.select([proc._pty_master], [], [], timeout)
|
|
105
|
+
if not ready: raise AssertionError(f"timed out waiting for clikernel pty output{_failure_detail(proc)}")
|
|
106
|
+
chunk = os.read(proc._pty_master, 4096)
|
|
107
|
+
assert chunk, _failure_detail(proc)
|
|
108
|
+
buf.extend(chunk)
|
|
109
|
+
line, _, rest = buf.partition(b"\n")
|
|
110
|
+
proc._pty_buffer = bytearray(rest)
|
|
111
|
+
return line.decode("utf-8", "replace").rstrip("\r") + "\n"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_pty_until_ready(proc, timeout=TIMEOUT):
|
|
115
|
+
lines = []
|
|
116
|
+
deadline = time.monotonic() + timeout
|
|
117
|
+
while True:
|
|
118
|
+
remaining = deadline - time.monotonic()
|
|
119
|
+
if remaining <= 0: raise AssertionError(f"timed out waiting for ready delimiter{_failure_detail(proc)}")
|
|
120
|
+
line = _read_ptyline(proc, remaining)
|
|
121
|
+
s = line.rstrip("\n")
|
|
122
|
+
if DELIM_RE.fullmatch(s): return "".join(lines), s
|
|
123
|
+
lines.append(line)
|
|
124
|
+
|
|
125
|
+
def _read_pty_rawline(proc, timeout):
|
|
126
|
+
buf = proc._pty_buffer
|
|
127
|
+
while b"\n" not in buf:
|
|
128
|
+
ready, _, _ = select.select([proc._pty_master], [], [], timeout)
|
|
129
|
+
if not ready: raise AssertionError(f"timed out waiting for clikernel pty output{_failure_detail(proc)}")
|
|
130
|
+
chunk = os.read(proc._pty_master, 4096)
|
|
131
|
+
assert chunk, _failure_detail(proc)
|
|
132
|
+
buf.extend(chunk)
|
|
133
|
+
line, _, rest = buf.partition(b"\n")
|
|
134
|
+
proc._pty_buffer = bytearray(rest)
|
|
135
|
+
return bytes(line + b"\n")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_pty_raw_until_ready(proc, timeout=TIMEOUT):
|
|
139
|
+
body = bytearray()
|
|
140
|
+
deadline = time.monotonic() + timeout
|
|
141
|
+
while True:
|
|
142
|
+
remaining = deadline - time.monotonic()
|
|
143
|
+
if remaining <= 0: raise AssertionError(f"timed out waiting for ready delimiter{_failure_detail(proc)}")
|
|
144
|
+
line = _read_pty_rawline(proc, remaining)
|
|
145
|
+
s = line.rstrip(b"\r\n").decode("utf-8", "replace")
|
|
146
|
+
if DELIM_RE.fullmatch(s): return bytes(body), s, line
|
|
147
|
+
body.extend(line)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.fixture
|
|
151
|
+
def kernel(tmp_path):
|
|
152
|
+
proc = start_kernel(tmp_path)
|
|
153
|
+
try:
|
|
154
|
+
body, delim = read_until_ready(proc)
|
|
155
|
+
yield proc, body, delim
|
|
156
|
+
finally: stop_kernel(proc)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_startup_prints_valid_ready_delimiter(kernel):
|
|
160
|
+
_, body, delim = kernel
|
|
161
|
+
assert body == "please wait, loading...\nloading complete. first delimiter:\n"
|
|
162
|
+
assert DELIM_RE.fullmatch(delim)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_single_line_request_returns_result_and_same_delimiter(kernel):
|
|
166
|
+
proc, _, delim = kernel
|
|
167
|
+
body, next_delim = send(proc, "1+1\n")
|
|
168
|
+
assert body == "2\n"
|
|
169
|
+
assert DELIM_RE.fullmatch(next_delim)
|
|
170
|
+
assert next_delim == delim
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_tty_input_is_not_echoed_after_startup(tmp_path):
|
|
174
|
+
proc = start_kernel_pty(tmp_path)
|
|
175
|
+
try:
|
|
176
|
+
body, delim = read_pty_until_ready(proc)
|
|
177
|
+
assert body == "please wait, loading...\nloading complete. first delimiter:\n"
|
|
178
|
+
os.write(proc._pty_master, b"1+1\n")
|
|
179
|
+
assert _read_ptyline(proc, TIMEOUT) == ".\n"
|
|
180
|
+
body, next_delim = read_pty_until_ready(proc)
|
|
181
|
+
assert body == "2\n"
|
|
182
|
+
assert next_delim == delim
|
|
183
|
+
finally: stop_kernel(proc)
|
|
184
|
+
|
|
185
|
+
def test_tty_output_keeps_newlines_as_lf(tmp_path):
|
|
186
|
+
proc = start_kernel_pty(tmp_path)
|
|
187
|
+
try:
|
|
188
|
+
_, delim = read_pty_until_ready(proc)
|
|
189
|
+
os.write(proc._pty_master, b"print('hello')\n")
|
|
190
|
+
body, next_delim, raw_delim = read_pty_raw_until_ready(proc)
|
|
191
|
+
assert body == b".\nhello\n"
|
|
192
|
+
assert raw_delim == next_delim.encode() + b"\n"
|
|
193
|
+
assert b"\r" not in body + raw_delim
|
|
194
|
+
assert next_delim == delim
|
|
195
|
+
finally: stop_kernel(proc)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_request_ack_is_written_before_result(kernel):
|
|
199
|
+
proc, _, delim = kernel
|
|
200
|
+
assert proc.stdin is not None
|
|
201
|
+
proc.stdin.write(b"1+1\n")
|
|
202
|
+
proc.stdin.flush()
|
|
203
|
+
assert _readline(proc, TIMEOUT) == ".\n"
|
|
204
|
+
body, next_delim = read_until_ready(proc)
|
|
205
|
+
assert body == "2\n"
|
|
206
|
+
assert next_delim == delim
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_state_persists_across_requests(kernel):
|
|
210
|
+
proc, _, _ = kernel
|
|
211
|
+
body, _ = send(proc, "x=41\n")
|
|
212
|
+
assert body == ""
|
|
213
|
+
body, _ = send(proc, "x+1\n")
|
|
214
|
+
assert body == "42\n"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_multiline_request_uses_current_delimiter(kernel):
|
|
218
|
+
proc, _, delim = kernel
|
|
219
|
+
body, _ = send(proc, f"--\ndef f(x):\n return x + 1\n\nf(41)\n{delim}\n")
|
|
220
|
+
assert body == "42\n"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_multiple_outputs_use_xmlish_blocks(kernel):
|
|
224
|
+
proc, _, delim = kernel
|
|
225
|
+
code = (
|
|
226
|
+
"--\n"
|
|
227
|
+
"from IPython.display import Markdown, display\n"
|
|
228
|
+
"print('hello')\n"
|
|
229
|
+
"display(Markdown('**shown**'))\n"
|
|
230
|
+
"42\n"
|
|
231
|
+
f"{delim}\n")
|
|
232
|
+
body, _ = send(proc, code)
|
|
233
|
+
assert '<stdout>\nhello\n</stdout>\n' in body
|
|
234
|
+
assert '<display_data mime="text/markdown">\n**shown**\n</display_data>\n' in body
|
|
235
|
+
assert '<execute_result>\n42\n</execute_result>\n' in body
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_runtime_errors_return_error_text_and_same_delimiter(kernel):
|
|
239
|
+
proc, _, delim = kernel
|
|
240
|
+
body, next_delim = send(proc, "1/0\n")
|
|
241
|
+
assert "ZeroDivisionError" in body
|
|
242
|
+
assert DELIM_RE.fullmatch(next_delim)
|
|
243
|
+
assert next_delim == delim
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.parametrize("cmd", ["exit()", "quit()"])
|
|
247
|
+
def test_exit_request_returns_final_delimiter_and_stops_process(kernel, cmd):
|
|
248
|
+
proc, _, start_delim = kernel
|
|
249
|
+
body, delim = send(proc, f"{cmd}\n")
|
|
250
|
+
assert body == ""
|
|
251
|
+
assert DELIM_RE.fullmatch(delim)
|
|
252
|
+
assert delim == start_delim
|
|
253
|
+
proc.wait(timeout=TIMEOUT)
|
|
254
|
+
assert proc.returncode == 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_history_is_disabled(kernel):
|
|
258
|
+
proc, _, _ = kernel
|
|
259
|
+
body, _ = send(proc, "get_ipython().history_manager.enabled\n")
|
|
260
|
+
assert body == "False\n"
|
clikernel-0.1.0/CHANGELOG.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import os,re,select,shutil,subprocess,tempfile,time
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
DELIM_RE = re.compile(r"--[A-Za-z0-9]{5}")
|
|
6
|
-
TIMEOUT = 5
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _failure_detail(proc):
|
|
10
|
-
if proc.stderr is None: return ""
|
|
11
|
-
ready, _, _ = select.select([proc.stderr], [], [], 0)
|
|
12
|
-
if not ready: return ""
|
|
13
|
-
err = os.read(proc.stderr.fileno(), 65536).decode("utf-8", "replace")
|
|
14
|
-
return f"\nstderr:\n{err}" if err else ""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _readline(proc, timeout):
|
|
18
|
-
assert proc.stdout is not None
|
|
19
|
-
buf = proc._stdout_buffer
|
|
20
|
-
while b"\n" not in buf:
|
|
21
|
-
ready, _, _ = select.select([proc.stdout], [], [], timeout)
|
|
22
|
-
if not ready: raise AssertionError(f"timed out waiting for clikernel output{_failure_detail(proc)}")
|
|
23
|
-
chunk = os.read(proc.stdout.fileno(), 4096)
|
|
24
|
-
assert chunk, _failure_detail(proc)
|
|
25
|
-
buf.extend(chunk)
|
|
26
|
-
line, _, rest = buf.partition(b"\n")
|
|
27
|
-
proc._stdout_buffer = bytearray(rest)
|
|
28
|
-
return line.decode("utf-8", "replace") + "\n"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def read_until_ready(proc, timeout=TIMEOUT):
|
|
32
|
-
lines = []
|
|
33
|
-
deadline = time.monotonic() + timeout
|
|
34
|
-
while True:
|
|
35
|
-
remaining = deadline - time.monotonic()
|
|
36
|
-
if remaining <= 0: raise AssertionError(f"timed out waiting for ready delimiter{_failure_detail(proc)}")
|
|
37
|
-
line = _readline(proc, remaining)
|
|
38
|
-
s = line.rstrip("\n")
|
|
39
|
-
if DELIM_RE.fullmatch(s): return "".join(lines), s
|
|
40
|
-
lines.append(line)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def start_kernel(tmp_path):
|
|
44
|
-
cmd = shutil.which("clikernel")
|
|
45
|
-
assert cmd, "clikernel console script is not on PATH"
|
|
46
|
-
env = os.environ.copy()
|
|
47
|
-
state = os.path.join(tempfile.gettempdir(), f"clikernel-{os.getuid()}")
|
|
48
|
-
env["CLIKERNEL_STATE_DIR"] = str(tmp_path / "state")
|
|
49
|
-
env.setdefault("IPYTHONDIR", os.path.join(state, "ipython"))
|
|
50
|
-
env.setdefault("MPLCONFIGDIR", os.path.join(state, "matplotlib"))
|
|
51
|
-
env.setdefault("MPLBACKEND", "Agg")
|
|
52
|
-
env["PYTHONUNBUFFERED"] = "1"
|
|
53
|
-
proc = subprocess.Popen([cmd], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
|
54
|
-
proc._stdout_buffer = bytearray()
|
|
55
|
-
return proc
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def stop_kernel(proc):
|
|
59
|
-
if proc.stdin is not None:
|
|
60
|
-
try: proc.stdin.close()
|
|
61
|
-
except OSError: pass
|
|
62
|
-
if proc.poll() is None:
|
|
63
|
-
try: proc.wait(timeout=1)
|
|
64
|
-
except subprocess.TimeoutExpired:
|
|
65
|
-
proc.terminate()
|
|
66
|
-
try: proc.wait(timeout=1)
|
|
67
|
-
except subprocess.TimeoutExpired:
|
|
68
|
-
proc.kill()
|
|
69
|
-
proc.wait(timeout=1)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def send(proc, text, timeout=TIMEOUT):
|
|
73
|
-
assert proc.stdin is not None
|
|
74
|
-
proc.stdin.write(text.encode("utf-8"))
|
|
75
|
-
proc.stdin.flush()
|
|
76
|
-
return read_until_ready(proc, timeout)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@pytest.fixture
|
|
80
|
-
def kernel(tmp_path):
|
|
81
|
-
proc = start_kernel(tmp_path)
|
|
82
|
-
try:
|
|
83
|
-
body, delim = read_until_ready(proc)
|
|
84
|
-
yield proc, body, delim
|
|
85
|
-
finally: stop_kernel(proc)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_startup_prints_valid_ready_delimiter(kernel):
|
|
89
|
-
_, body, delim = kernel
|
|
90
|
-
assert body == "please wait, loading...\nloading complete. first delimiter:\n"
|
|
91
|
-
assert DELIM_RE.fullmatch(delim)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_single_line_request_returns_result_and_fresh_delimiter(kernel):
|
|
95
|
-
proc, _, delim = kernel
|
|
96
|
-
body, next_delim = send(proc, "1+1\n")
|
|
97
|
-
assert body == "2\n"
|
|
98
|
-
assert DELIM_RE.fullmatch(next_delim)
|
|
99
|
-
assert next_delim != delim
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_state_persists_across_requests(kernel):
|
|
103
|
-
proc, _, _ = kernel
|
|
104
|
-
body, _ = send(proc, "x=41\n")
|
|
105
|
-
assert body == ""
|
|
106
|
-
body, _ = send(proc, "x+1\n")
|
|
107
|
-
assert body == "42\n"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_multiline_request_uses_current_delimiter(kernel):
|
|
111
|
-
proc, _, delim = kernel
|
|
112
|
-
body, _ = send(proc, f"--\ndef f(x):\n return x + 1\n\nf(41)\n{delim}\n")
|
|
113
|
-
assert body == "42\n"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_multiple_outputs_use_xmlish_blocks(kernel):
|
|
117
|
-
proc, _, delim = kernel
|
|
118
|
-
code = (
|
|
119
|
-
"--\n"
|
|
120
|
-
"from IPython.display import Markdown, display\n"
|
|
121
|
-
"print('hello')\n"
|
|
122
|
-
"display(Markdown('**shown**'))\n"
|
|
123
|
-
"42\n"
|
|
124
|
-
f"{delim}\n")
|
|
125
|
-
body, _ = send(proc, code)
|
|
126
|
-
assert '<stdout>\nhello\n</stdout>\n' in body
|
|
127
|
-
assert '<display_data mime="text/markdown">\n**shown**\n</display_data>\n' in body
|
|
128
|
-
assert '<execute_result>\n42\n</execute_result>\n' in body
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_runtime_errors_return_error_text_and_fresh_delimiter(kernel):
|
|
132
|
-
proc, _, delim = kernel
|
|
133
|
-
body, next_delim = send(proc, "1/0\n")
|
|
134
|
-
assert "ZeroDivisionError" in body
|
|
135
|
-
assert DELIM_RE.fullmatch(next_delim)
|
|
136
|
-
assert next_delim != delim
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def test_history_is_disabled(kernel):
|
|
140
|
-
proc, _, _ = kernel
|
|
141
|
-
body, _ = send(proc, "get_ipython().history_manager.enabled\n")
|
|
142
|
-
assert body == "False\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|