clikernel 0.1.1__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.
@@ -1,5 +1,12 @@
1
1
  <!-- do not remove -->
2
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
+
3
10
  ## 0.1.1
4
11
 
5
12
  ### New Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clikernel
3
- Version: 0.1.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
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.2"
2
+
3
+
@@ -75,10 +75,24 @@ def _disable_echo():
75
75
  def _restore_echo(state):
76
76
  if state: termios.tcsetattr(state[0], termios.TCSADRAIN, state[1])
77
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
+
78
91
 
79
92
  def main():
80
- echo_state = _disable_echo()
81
93
  shell = _make_shell()
94
+ output_state = _disable_output_newline_translation()
95
+ echo_state = _disable_echo()
82
96
  delim = _new_delim()
83
97
  print("loading complete. first delimiter:", flush=True)
84
98
  _write_response(delim)
@@ -96,7 +110,9 @@ def main():
96
110
  except BaseException: outputs = _format_error("internal-error", traceback.format_exc())
97
111
  _write_response(delim, outputs)
98
112
  if _should_exit(shell): break
99
- finally: _restore_echo(echo_state)
113
+ finally:
114
+ _restore_echo(echo_state)
115
+ _restore_termios(output_state)
100
116
 
101
117
 
102
118
  if __name__ == "__main__": main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clikernel
3
- Version: 0.1.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
@@ -1,4 +1,4 @@
1
- import os,re,select,shutil,subprocess,tempfile,time
1
+ import os,pty,re,select,shutil,subprocess,tempfile,time
2
2
 
3
3
  import pytest
4
4
 
@@ -55,10 +55,31 @@ def start_kernel(tmp_path):
55
55
  return proc
56
56
 
57
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
+
58
76
  def stop_kernel(proc):
59
77
  if proc.stdin is not None:
60
78
  try: proc.stdin.close()
61
79
  except OSError: pass
80
+ if hasattr(proc, "_pty_master"):
81
+ try: os.close(proc._pty_master)
82
+ except OSError: pass
62
83
  if proc.poll() is None:
63
84
  try: proc.wait(timeout=1)
64
85
  except subprocess.TimeoutExpired:
@@ -77,6 +98,55 @@ def send(proc, text, timeout=TIMEOUT):
77
98
  return read_until_ready(proc, timeout)
78
99
 
79
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
+
80
150
  @pytest.fixture
81
151
  def kernel(tmp_path):
82
152
  proc = start_kernel(tmp_path)
@@ -100,6 +170,31 @@ def test_single_line_request_returns_result_and_same_delimiter(kernel):
100
170
  assert next_delim == delim
101
171
 
102
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
+
103
198
  def test_request_ack_is_written_before_result(kernel):
104
199
  proc, _, delim = kernel
105
200
  assert proc.stdin is not None
@@ -1,2 +0,0 @@
1
- __version__ = "0.1.1"
2
-
File without changes
File without changes
File without changes
File without changes
File without changes