stata-code 0.7.0__tar.gz → 0.7.1__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.
- {stata_code-0.7.0 → stata_code-0.7.1}/.gitignore +2 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/CHANGELOG.md +33 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/PKG-INFO +4 -4
- {stata_code-0.7.0 → stata_code-0.7.1}/README.md +3 -3
- {stata_code-0.7.0 → stata_code-0.7.1}/pyproject.toml +1 -1
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/__init__.py +1 -1
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/runner.py +40 -10
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/kernel.py +32 -2
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/mcp/server.py +1 -1
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_kernel.py +113 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_runner.py +61 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/LICENSE +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/LICENSE-POLICY.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/PUBLISHING.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/SCHEMA.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/docs/design/hard_timeout.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/01-basic-regression.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/02-did-card-krueger.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/03-graphs.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/04-multi-session.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/05-large-matrix.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/examples/README.md +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/schema/run_result.schema.json +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/scripts/check_versions.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/scripts/export_schema.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/__init__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/_pool.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/_refs.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/_runtime.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/errors.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/log_artifacts.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/notebook.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/run_index.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/core/schema.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/__init__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/__main__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/assets/logo-32x32.png +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/assets/logo-64x64.png +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/kernel/assets/logo-svg.svg +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/mcp/__init__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/stata_code/mcp/__main__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/__init__.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/conftest.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/fixtures/.gitkeep +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_cancel.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_errors.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_log_artifacts.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_mcp.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_mcp_stdio.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_notebook.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_notebook_phase2.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_pool.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_public_api.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_release_versions.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_run_index.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_runtime_discovery.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_schema.py +0 -0
- {stata_code-0.7.0 → stata_code-0.7.1}/tests/test_schema_artifact.py +0 -0
|
@@ -6,6 +6,39 @@ to semver-major.minor for the result schema (see `SCHEMA.md` §6).
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.7.1 — 2026-06-19
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Jupyter kernel: graphs after the first cell now display.** Graph capture
|
|
14
|
+
detected new graphs by diffing in-memory graph names before/after a run.
|
|
15
|
+
Because Stata keeps only one graph per name and every unnamed graph command
|
|
16
|
+
overwrites the default `Graph` in place, the second and later cells of a
|
|
17
|
+
persistent session produced no name delta and their graphs were silently
|
|
18
|
+
dropped — only the first cell's graph ever rendered. Capture now also
|
|
19
|
+
re-exports any graph the cell's own source shows it (re)drew (every
|
|
20
|
+
`name(...)` target, plus the default `Graph` for any unnamed graph command),
|
|
21
|
+
so in-place redraws surface every time. The same fix covers repeated MCP
|
|
22
|
+
`stata_run` calls in one session. The graph-command detector was tightened
|
|
23
|
+
to distinguish drawing commands from `graph` utility subcommands (`export`,
|
|
24
|
+
`display`, `dir`, `drop`, …) so a utility-only cell no longer re-surfaces a
|
|
25
|
+
stale graph.
|
|
26
|
+
- **Jupyter kernel: no more duplicated code echo in cell output.** pystata
|
|
27
|
+
runs a multi-line cell as a temporary do-file, and Stata echoes every
|
|
28
|
+
submitted command (`. cmd` / `> continuation`) regardless of `echo=False`
|
|
29
|
+
(which only suppresses echo for a single inline command). For a cell with no
|
|
30
|
+
textual output (e.g. a graph) that echo was the *only* thing shown — a
|
|
31
|
+
useless repeat of the source already visible in the input cell. The kernel
|
|
32
|
+
now strips command-echo lines before streaming, keeping genuine command
|
|
33
|
+
output. The full log (with echo) is unchanged in `RunResult.log` for MCP /
|
|
34
|
+
agent consumers.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **VS Code extension now ships a Marketplace icon** (coef-plot mark, Anthropic
|
|
39
|
+
palette on white) so the listing and Extensions sidebar render branded
|
|
40
|
+
artwork instead of the default placeholder.
|
|
41
|
+
|
|
9
42
|
## 0.7.0 — 2026-05-30
|
|
10
43
|
|
|
11
44
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stata-code
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Agent-native Stata bridge — one core, multiple frontends (MCP, Jupyter, VSCode)
|
|
5
5
|
Project-URL: Homepage, https://github.com/brycewang-stanford/stata-code
|
|
6
6
|
Project-URL: Repository, https://github.com/brycewang-stanford/stata-code
|
|
@@ -84,9 +84,9 @@ Description-Content-Type: text/markdown
|
|
|
84
84
|
└─────────────┘ └────────────┘ └─────────────────┘
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
**Status: v0.
|
|
87
|
+
**Status: v0.7 (May 2026)** — the core, MCP server, Jupyter kernel, and VS Code extension work end-to-end against Stata 18 MP. The test suite covers schema, runner, MCP, kernel, notebook, run-index, subprocess-pool, and VS Code modules; CI also checks linting, type safety, schema generation, package metadata, and VSIX packaging. License: **MIT**.
|
|
88
88
|
|
|
89
|
-
Two workflows
|
|
89
|
+
Two workflows the current release explicitly supports for end users:
|
|
90
90
|
|
|
91
91
|
- **Run Stata code from a Jupyter notebook.** `pip install "stata-code[kernel]"` + `stata-code-kernel install --user` registers a **Stata** kernel that the Jupyter Notebook UI, JupyterLab, and the VS Code Jupyter extension all pick up by name. Cells render Stata logs, graphs, and warnings inline (the kernel logo bundled since v0.5 makes it appear in VS Code's kernel picker too). See [As a Jupyter Kernel](#as-a-jupyter-kernel).
|
|
92
92
|
- **Optional agent "fix and rerun" loop.** `stata_run` returns typed `error.kind/line/context` plus `suggestions` on every failure. By default Claude Code only reports diagnostics — but if you explicitly say "fix this and rerun until it passes", the agent uses the same fields to edit your `.do` file and re-call `stata_run` until the run is green. The repair loop is **opt-in**: failed runs are diagnostics first, not automatic rewrite permission. See [Error Recovery in Agent Workflows](#error-recovery-in-agent-workflows).
|
|
@@ -444,7 +444,7 @@ stata_code/
|
|
|
444
444
|
|
|
445
445
|
## Roadmap
|
|
446
446
|
|
|
447
|
-
### Done (through v0.
|
|
447
|
+
### Done (through v0.7 — May 2026)
|
|
448
448
|
|
|
449
449
|
- v1.0 result schema ([SCHEMA.md](SCHEMA.md))
|
|
450
450
|
- `pystata`-based runner with native-typed `r()`, `e()`, and matrices
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
└─────────────┘ └────────────┘ └─────────────────┘
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
**Status: v0.
|
|
48
|
+
**Status: v0.7 (May 2026)** — the core, MCP server, Jupyter kernel, and VS Code extension work end-to-end against Stata 18 MP. The test suite covers schema, runner, MCP, kernel, notebook, run-index, subprocess-pool, and VS Code modules; CI also checks linting, type safety, schema generation, package metadata, and VSIX packaging. License: **MIT**.
|
|
49
49
|
|
|
50
|
-
Two workflows
|
|
50
|
+
Two workflows the current release explicitly supports for end users:
|
|
51
51
|
|
|
52
52
|
- **Run Stata code from a Jupyter notebook.** `pip install "stata-code[kernel]"` + `stata-code-kernel install --user` registers a **Stata** kernel that the Jupyter Notebook UI, JupyterLab, and the VS Code Jupyter extension all pick up by name. Cells render Stata logs, graphs, and warnings inline (the kernel logo bundled since v0.5 makes it appear in VS Code's kernel picker too). See [As a Jupyter Kernel](#as-a-jupyter-kernel).
|
|
53
53
|
- **Optional agent "fix and rerun" loop.** `stata_run` returns typed `error.kind/line/context` plus `suggestions` on every failure. By default Claude Code only reports diagnostics — but if you explicitly say "fix this and rerun until it passes", the agent uses the same fields to edit your `.do` file and re-call `stata_run` until the run is green. The repair loop is **opt-in**: failed runs are diagnostics first, not automatic rewrite permission. See [Error Recovery in Agent Workflows](#error-recovery-in-agent-workflows).
|
|
@@ -405,7 +405,7 @@ stata_code/
|
|
|
405
405
|
|
|
406
406
|
## Roadmap
|
|
407
407
|
|
|
408
|
-
### Done (through v0.
|
|
408
|
+
### Done (through v0.7 — May 2026)
|
|
409
409
|
|
|
410
410
|
- v1.0 result schema ([SCHEMA.md](SCHEMA.md))
|
|
411
411
|
- `pystata`-based runner with native-typed `r()`, `e()`, and matrices
|
|
@@ -1195,11 +1195,20 @@ def _extract_warnings(log: str) -> list: # list[StataWarning]
|
|
|
1195
1195
|
|
|
1196
1196
|
|
|
1197
1197
|
_GRAPH_NAME_RE = re.compile(r"\bname\(\s*([A-Za-z_][A-Za-z0-9_]*)", re.IGNORECASE)
|
|
1198
|
+
# Stata's default in-memory graph name, (re)used by any graph command that
|
|
1199
|
+
# omits an explicit `name(...)` option. Capture/redraw detection keys off this.
|
|
1200
|
+
_DEFAULT_GRAPH_NAME = "Graph"
|
|
1201
|
+
# Commands that actually *draw* a graph (and thus create/overwrite an
|
|
1202
|
+
# in-memory graph). Deliberately excludes the `graph` utility subcommands
|
|
1203
|
+
# (export, display, dir, drop, describe, save, use, rename, set, copy, query,
|
|
1204
|
+
# replay) — those operate on existing graphs and must not be mistaken for a
|
|
1205
|
+
# redraw, or a bare `graph export` cell would spuriously re-surface a stale
|
|
1206
|
+
# graph.
|
|
1198
1207
|
_GRAPH_COMMAND_RE = re.compile(
|
|
1199
1208
|
r"^\s*(?:"
|
|
1200
|
-
r"graph\s
|
|
1201
|
-
r"twoway|scatter|line|connected|histogram|kdensity|lowess|
|
|
1202
|
-
r"coefplot|binscatter"
|
|
1209
|
+
r"graph\s+(?:bar|hbar|box|hbox|dot|pie|twoway|matrix|combine)\b|"
|
|
1210
|
+
r"twoway|scatter|line|connected|histogram|hist|kdensity|lpoly|lowess|"
|
|
1211
|
+
r"lfit|qfit|coefplot|binscatter|marginsplot"
|
|
1203
1212
|
r")\b",
|
|
1204
1213
|
re.IGNORECASE,
|
|
1205
1214
|
)
|
|
@@ -1262,19 +1271,40 @@ def _collect_graphs(
|
|
|
1262
1271
|
source_hints: dict[str, tuple[str, int]] | None = None,
|
|
1263
1272
|
unnamed_source_hints: list[tuple[str, int]] | None = None,
|
|
1264
1273
|
) -> list[GraphInfo]:
|
|
1265
|
-
"""Capture graphs that user code newly created.
|
|
1274
|
+
"""Capture graphs that user code newly created or redrew.
|
|
1266
1275
|
|
|
1267
1276
|
Strategy: snapshot graph names before user code (`pre_existing`), call
|
|
1268
|
-
after to find the post-existing list
|
|
1269
|
-
new
|
|
1270
|
-
|
|
1277
|
+
after to find the post-existing list. Capture a graph when its name is
|
|
1278
|
+
genuinely new *or* when this cell's source shows it (re)drew that name.
|
|
1279
|
+
|
|
1280
|
+
The redraw case matters because Stata keeps only one in-memory graph per
|
|
1281
|
+
name, so a command that overwrites an existing name (most commonly the
|
|
1282
|
+
default ``Graph``, produced by any unnamed graph command) leaves the
|
|
1283
|
+
``graph dir`` name set unchanged. A pure set-difference against
|
|
1284
|
+
`pre_existing` therefore misses it — which is why, in a persistent session
|
|
1285
|
+
(Jupyter cell 2+, repeated MCP runs), only the first graph ever surfaced.
|
|
1286
|
+
|
|
1287
|
+
For each captured graph: `graph display <name>` (makes it active),
|
|
1288
|
+
`graph export` to a tmpfile, read bytes, store under a ref. Tmpfile is
|
|
1289
|
+
deleted after.
|
|
1271
1290
|
"""
|
|
1272
1291
|
after_names = _list_graph_names(rt)
|
|
1273
|
-
new_names = [n for n in after_names if n not in pre_existing]
|
|
1274
|
-
if not new_names:
|
|
1275
|
-
return []
|
|
1276
1292
|
source_hints = source_hints or {}
|
|
1277
1293
|
unnamed_source_hints = unnamed_source_hints or []
|
|
1294
|
+
|
|
1295
|
+
# Names this cell explicitly drew, inferred from its source: every
|
|
1296
|
+
# `name(...)` option, plus the default graph when any unnamed graph
|
|
1297
|
+
# command ran. These are re-captured even if they already existed, so an
|
|
1298
|
+
# in-place redraw is not dropped.
|
|
1299
|
+
redrawn = set(source_hints)
|
|
1300
|
+
if unnamed_source_hints:
|
|
1301
|
+
redrawn.add(_DEFAULT_GRAPH_NAME)
|
|
1302
|
+
|
|
1303
|
+
new_names = [
|
|
1304
|
+
n for n in after_names if n not in pre_existing or n in redrawn
|
|
1305
|
+
]
|
|
1306
|
+
if not new_names:
|
|
1307
|
+
return []
|
|
1278
1308
|
unattributed_names = [n for n in new_names if n not in source_hints]
|
|
1279
1309
|
unnamed_by_graph: dict[str, tuple[str, int]] = {}
|
|
1280
1310
|
if len(unattributed_names) == len(unnamed_source_hints):
|
|
@@ -102,6 +102,35 @@ def _word_at_cursor(code: str, cursor_pos: int) -> tuple[str, int, int]:
|
|
|
102
102
|
return code[start:end], start, end
|
|
103
103
|
|
|
104
104
|
|
|
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
|
+
|
|
105
134
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
106
135
|
# Kernel
|
|
107
136
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -155,8 +184,9 @@ class StataKernel(_KernelBase):
|
|
|
155
184
|
self._last_result = result
|
|
156
185
|
|
|
157
186
|
if not silent:
|
|
158
|
-
if result.log.head
|
|
159
|
-
|
|
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")
|
|
160
190
|
if result.warnings:
|
|
161
191
|
for w in result.warnings:
|
|
162
192
|
self._stream("stderr", f"[{w.kind}] {w.message}\n")
|
|
@@ -95,7 +95,7 @@ from stata_code.core.runner import (
|
|
|
95
95
|
)
|
|
96
96
|
from stata_code.core.schema import RunResult
|
|
97
97
|
|
|
98
|
-
__version__ = "0.7.
|
|
98
|
+
__version__ = "0.7.1"
|
|
99
99
|
|
|
100
100
|
SERVER_INSTRUCTIONS = (
|
|
101
101
|
"Use stata-code for running and inspecting Stata code. Prefer structuredContent "
|
|
@@ -130,6 +130,73 @@ class TestStataKernelClass:
|
|
|
130
130
|
finally:
|
|
131
131
|
kernel_module._HAS_IPYKERNEL = original
|
|
132
132
|
|
|
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
|
+
|
|
133
200
|
def test_do_complete_returns_stata_keywords(self):
|
|
134
201
|
"""do_complete should return Stata keyword matches."""
|
|
135
202
|
from stata_code.kernel import StataKernel
|
|
@@ -332,6 +399,52 @@ class TestInstallKernel:
|
|
|
332
399
|
assert flags == [False]
|
|
333
400
|
|
|
334
401
|
|
|
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
|
+
|
|
335
448
|
# NOTE: TestStataGraphDataUri was removed in v0.2. The legacy `StataGraph`
|
|
336
449
|
# dataclass (with .to_base64() / .to_data_uri()) is gone; the v1.0 `GraphInfo`
|
|
337
450
|
# schema returns refs by default and inline base64 only when explicitly
|
|
@@ -265,6 +265,67 @@ class TestGraphCapture:
|
|
|
265
265
|
# refs are unique per index
|
|
266
266
|
assert len({g.ref for g in r.graphs}) == 2
|
|
267
267
|
|
|
268
|
+
def test_redrawn_default_graph_recaptured_each_cell(self, loaded_auto):
|
|
269
|
+
"""Regression: in a persistent session every unnamed graph command
|
|
270
|
+
overwrites the default "Graph" in place, so a pure name set-diff sees
|
|
271
|
+
no new name on the 2nd+ run and captured nothing — only the first
|
|
272
|
+
Jupyter cell's graph displayed. Each redraw must now surface its own
|
|
273
|
+
graph. NOTE: deliberately no _clean_graphs between runs."""
|
|
274
|
+
from stata_code.core.runner import execute
|
|
275
|
+
|
|
276
|
+
self._clean_graphs(execute)
|
|
277
|
+
r1 = execute("scatter price mpg")
|
|
278
|
+
assert len(r1.graphs) == 1
|
|
279
|
+
r2 = execute("scatter weight mpg")
|
|
280
|
+
assert len(r2.graphs) == 1 # was 0 before the fix
|
|
281
|
+
r3 = execute("histogram price")
|
|
282
|
+
assert len(r3.graphs) == 1
|
|
283
|
+
|
|
284
|
+
def test_redrawn_named_graph_recaptured(self, loaded_auto):
|
|
285
|
+
"""A named graph redrawn under the same name across runs is captured
|
|
286
|
+
every time, not just the first."""
|
|
287
|
+
from stata_code.core.runner import execute
|
|
288
|
+
|
|
289
|
+
self._clean_graphs(execute)
|
|
290
|
+
r1 = execute("scatter price mpg, name(g_redraw, replace)")
|
|
291
|
+
assert len(r1.graphs) == 1
|
|
292
|
+
r2 = execute("scatter weight mpg, name(g_redraw, replace)")
|
|
293
|
+
assert len(r2.graphs) == 1 # was 0 before the fix
|
|
294
|
+
assert r2.graphs[0].name == "g_redraw"
|
|
295
|
+
|
|
296
|
+
def test_no_graph_cell_after_graph_no_recapture(self, loaded_auto):
|
|
297
|
+
"""A cell that draws nothing must not re-surface an existing graph
|
|
298
|
+
left over from an earlier cell."""
|
|
299
|
+
from stata_code.core.runner import execute
|
|
300
|
+
|
|
301
|
+
self._clean_graphs(execute)
|
|
302
|
+
first = execute("scatter price mpg")
|
|
303
|
+
assert len(first.graphs) == 1
|
|
304
|
+
r = execute('display "just text, no graph"')
|
|
305
|
+
assert r.graphs == []
|
|
306
|
+
|
|
307
|
+
def test_graph_export_cell_does_not_recapture(self, loaded_auto, tmp_path):
|
|
308
|
+
"""A utility-only cell (graph export of a prior graph) must not be
|
|
309
|
+
treated as a redraw — `graph export` is not a drawing command."""
|
|
310
|
+
from stata_code.core.runner import execute
|
|
311
|
+
|
|
312
|
+
self._clean_graphs(execute)
|
|
313
|
+
execute("scatter price mpg") # default Graph now exists
|
|
314
|
+
out = (tmp_path / "export.png").as_posix()
|
|
315
|
+
r = execute(f'graph export "{out}", replace')
|
|
316
|
+
assert r.graphs == []
|
|
317
|
+
|
|
318
|
+
def test_new_graph_with_untouched_graph_persisting(self, loaded_auto):
|
|
319
|
+
"""When a cell draws one new named graph while an older, untouched
|
|
320
|
+
named graph still lives in memory, only the new one is captured."""
|
|
321
|
+
from stata_code.core.runner import execute
|
|
322
|
+
|
|
323
|
+
self._clean_graphs(execute)
|
|
324
|
+
execute("scatter price mpg, name(g_keep)")
|
|
325
|
+
r = execute("histogram weight, name(g_fresh)")
|
|
326
|
+
assert len(r.graphs) == 1
|
|
327
|
+
assert r.graphs[0].name == "g_fresh"
|
|
328
|
+
|
|
268
329
|
def test_get_graph_returns_bytes(self, loaded_auto):
|
|
269
330
|
from stata_code.core.runner import execute, get_graph
|
|
270
331
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|