Skip to content

Commit f212deb

Browse files
kernel: strip Stata command echo from cell output
pystata runs a multi-line cell as a temporary do-file, and Stata echoes every submitted command (`. cmd` and `> ...` continuations) regardless of the echo=False flag (which only suppresses echo for a single inline command). In a notebook the input cell already shows the source, so the echo is pure duplication; for a cell with no textual output (e.g. a graph) the echo is the *only* thing streamed, reading as a useless repeat of the code. Add `_strip_command_echo`, applied to the streamed cell log: drop echoed command/continuation lines (always column-0 `. ` or `> `, which real Stata output never is), collapse the blank-line runs that leaves, and trim trailing blanks. A bare `.` (e.g. `display .`) is real output and survives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1fee980 commit f212deb

2 files changed

Lines changed: 145 additions & 2 deletions

File tree

stata_code/kernel/kernel.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,35 @@ def _word_at_cursor(code: str, cursor_pos: int) -> tuple[str, int, int]:
102102
return code[start:end], start, end
103103

104104

105+
def _strip_command_echo(log_text: str) -> str:
106+
"""Drop Stata's do-file command echo from a captured cell log.
107+
108+
pystata runs a multi-line cell as a temporary do-file, and Stata echoes
109+
every submitted command — ``. cmd`` for the first line of each command and
110+
``> ...`` for wrapped/continued lines — regardless of the ``echo=False``
111+
flag (which only suppresses echo for a single inline command). In a
112+
notebook the input cell already shows the source, so the echo is pure
113+
duplication; for a cell with no textual output (e.g. a graph) the echo is
114+
the *only* thing shown, which reads as a useless repeat of the code.
115+
116+
Strip the echoed command/continuation lines, keep genuine command output,
117+
and collapse the blank-line runs the removal leaves behind. Echoed lines
118+
always start at column 0 with ``. `` (dot-space) or ``> `` (continuation);
119+
real Stata output never begins that way, so this is safe.
120+
"""
121+
kept: list[str] = []
122+
for line in log_text.split("\n"):
123+
if line.startswith(". ") or line.startswith("> "):
124+
continue
125+
# Collapse leading and consecutive blank lines left by removed echoes.
126+
if not line.strip() and (not kept or not kept[-1].strip()):
127+
continue
128+
kept.append(line)
129+
while kept and not kept[-1].strip():
130+
kept.pop()
131+
return "\n".join(kept)
132+
133+
105134
# ─────────────────────────────────────────────────────────────────────────────
106135
# Kernel
107136
# ─────────────────────────────────────────────────────────────────────────────
@@ -155,8 +184,9 @@ def do_execute(
155184
self._last_result = result
156185

157186
if not silent:
158-
if result.log.head:
159-
self._stream("stdout", result.log.head + "\n")
187+
log_text = _strip_command_echo(result.log.head) if result.log.head else ""
188+
if log_text:
189+
self._stream("stdout", log_text + "\n")
160190
if result.warnings:
161191
for w in result.warnings:
162192
self._stream("stderr", f"[{w.kind}] {w.message}\n")

tests/test_kernel.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,73 @@ def test_do_execute_handles_error_result(self):
130130
finally:
131131
kernel_module._HAS_IPYKERNEL = original
132132

133+
def test_do_execute_suppresses_pure_command_echo(self):
134+
"""A cell with no textual output (e.g. a graph) must not stream the
135+
echoed source back — that read as a useless repeat of the code."""
136+
from stata_code.kernel import kernel as kernel_module
137+
138+
original = kernel_module._HAS_IPYKERNEL
139+
kernel_module._HAS_IPYKERNEL = True
140+
echo_only = LogInfo(
141+
head='\n. * 3) fit\n. twoway (scatter price mpg) (lfit price mpg)\n\n. \n',
142+
tail="",
143+
lines_total=5,
144+
bytes_total=60,
145+
)
146+
mock_result = _make_run_result(ok=True, log=echo_only)
147+
try:
148+
from stata_code.kernel import StataKernel
149+
150+
kb = StataKernel()
151+
streamed: list[tuple[str, str]] = []
152+
with patch.object(
153+
kb, "_stream", side_effect=lambda n, t: streamed.append((n, t))
154+
):
155+
with patch(
156+
"stata_code.kernel.kernel.execute", return_value=mock_result
157+
):
158+
reply = kb.do_execute(
159+
"* 3) fit\ntwoway (scatter price mpg) (lfit price mpg)",
160+
silent=False,
161+
)
162+
assert reply["status"] == "ok"
163+
assert not any(name == "stdout" for name, _ in streamed)
164+
finally:
165+
kernel_module._HAS_IPYKERNEL = original
166+
167+
def test_do_execute_streams_output_without_echo(self):
168+
"""Genuine command output is streamed, but the leading `. cmd` echo is
169+
stripped from it."""
170+
from stata_code.kernel import kernel as kernel_module
171+
172+
original = kernel_module._HAS_IPYKERNEL
173+
kernel_module._HAS_IPYKERNEL = True
174+
mixed = LogInfo(
175+
head="\n. summarize price\n\n price | 74\n\n. \n",
176+
tail="",
177+
lines_total=6,
178+
bytes_total=40,
179+
)
180+
mock_result = _make_run_result(ok=True, log=mixed)
181+
try:
182+
from stata_code.kernel import StataKernel
183+
184+
kb = StataKernel()
185+
streamed: list[tuple[str, str]] = []
186+
with patch.object(
187+
kb, "_stream", side_effect=lambda n, t: streamed.append((n, t))
188+
):
189+
with patch(
190+
"stata_code.kernel.kernel.execute", return_value=mock_result
191+
):
192+
kb.do_execute("summarize price", silent=False)
193+
stdout = [t for n, t in streamed if n == "stdout"]
194+
assert len(stdout) == 1
195+
assert ". summarize price" not in stdout[0]
196+
assert "price | 74" in stdout[0]
197+
finally:
198+
kernel_module._HAS_IPYKERNEL = original
199+
133200
def test_do_complete_returns_stata_keywords(self):
134201
"""do_complete should return Stata keyword matches."""
135202
from stata_code.kernel import StataKernel
@@ -332,6 +399,52 @@ def install_kernel_spec(
332399
assert flags == [False]
333400

334401

402+
class TestCommandEchoStripping:
403+
"""Unit tests for `_strip_command_echo` (pure, no Stata required)."""
404+
405+
def test_pure_echo_graph_cell_becomes_empty(self):
406+
from stata_code.kernel.kernel import _strip_command_echo
407+
408+
log = "\n. * 3) fit\n. twoway (scatter price mpg) (lfit price mpg)\n\n. \n"
409+
assert _strip_command_echo(log) == ""
410+
411+
def test_wrapped_continuation_lines_stripped(self):
412+
from stata_code.kernel.kernel import _strip_command_echo
413+
414+
# Stata wraps a long command onto a `> ` continuation line.
415+
log = (
416+
'\n. twoway scatter price mpg, title("A long title that wraps\n'
417+
'> onto another line")\n\n. \n'
418+
)
419+
assert _strip_command_echo(log) == ""
420+
421+
def test_real_output_preserved_echo_removed(self):
422+
from stata_code.kernel.kernel import _strip_command_echo
423+
424+
log = "\n. summarize price\n\n price | 74\n\n. \n"
425+
out = _strip_command_echo(log)
426+
assert ". summarize" not in out
427+
assert out == " price | 74"
428+
429+
def test_consecutive_blank_lines_collapsed(self):
430+
from stata_code.kernel.kernel import _strip_command_echo
431+
432+
assert _strip_command_echo("line1\n\n\n\nline2") == "line1\n\nline2"
433+
434+
def test_log_without_echo_unchanged(self):
435+
from stata_code.kernel.kernel import _strip_command_echo
436+
437+
log = " Variable | Obs\n price | 74"
438+
assert _strip_command_echo(log) == log
439+
440+
def test_missing_value_dot_output_not_stripped(self):
441+
from stata_code.kernel.kernel import _strip_command_echo
442+
443+
# A bare "." (e.g. `display .`) is real output, not an echoed prompt
444+
# (which is always ". " — dot-space). It must survive.
445+
assert _strip_command_echo(".") == "."
446+
447+
335448
# NOTE: TestStataGraphDataUri was removed in v0.2. The legacy `StataGraph`
336449
# dataclass (with .to_base64() / .to_data_uri()) is gone; the v1.0 `GraphInfo`
337450
# schema returns refs by default and inline base64 only when explicitly

0 commit comments

Comments
 (0)