agentic-python-coder 3.4.3__tar.gz → 3.4.4__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.
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/PKG-INFO +2 -2
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/kernel.py +5 -1
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/mcp_server.py +40 -5
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/pyproject.toml +6 -2
- agentic_python_coder-3.4.3/coder/tests/test_kernel.py +0 -212
- agentic_python_coder-3.4.3/coder/tests/test_library_api.py +0 -384
- agentic_python_coder-3.4.3/coder/tests/test_llm_config.py +0 -90
- agentic_python_coder-3.4.3/coder/tests/test_mcp_hardening.py +0 -275
- agentic_python_coder-3.4.3/coder/tests/test_mcp_server.py +0 -429
- agentic_python_coder-3.4.3/coder/tests/test_react_loop.py +0 -306
- agentic_python_coder-3.4.3/coder/tests/test_todo_flag_integration.py +0 -131
- agentic_python_coder-3.4.3/coder/tests/test_todo_tool_availability.py +0 -58
- agentic_python_coder-3.4.3/coder/tests/test_tool_descriptions.py +0 -74
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/.gitignore +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/LICENSE +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/.gitignore +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/prompts/system.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/prompts/system_todo.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/__init__.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/agent.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/cli.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/clingo.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/bird_reasoning.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/diagnosis.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/simple_coloring.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/stable_marriage.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/sudoku_mini.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/magic_square.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/n_queens.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/regex/sample_tasks/email_extraction.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/regex/sample_tasks/phone_validation.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/examples/regex/sample_tasks/url_parsing.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/llm.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/deepseek31.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gemini25.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gemini31.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gemini3flash.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gemini4argon.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gpt52.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gpt56sol.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gpt56terra.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gpt6astra.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/gpt6sol.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/grok47.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/opus45.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/opus48.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/opus55.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/qwen3.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/sonnet45.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/sonnet46.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/sonnet5.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/models/sonnet55.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/project_md.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/runner.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/tools.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/examples/clingo/clingo.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentic-python-coder
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.4
|
|
4
4
|
Summary: A lightweight Python coding agent that writes, executes, and iterates on code through natural language instructions
|
|
5
5
|
Author: Stefan Szeider
|
|
6
6
|
License: Apache-2.0
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
19
|
Requires-Dist: ipykernel>=7.3.0
|
|
20
20
|
Requires-Dist: jupyter-client>=8.9.1
|
|
21
|
-
Requires-Dist: mcp<
|
|
21
|
+
Requires-Dist: mcp<3,>=1.28.1
|
|
22
22
|
Requires-Dist: openai>=2.45.0
|
|
23
23
|
Requires-Dist: python-dotenv>=1.2.2
|
|
24
24
|
Requires-Dist: pyyaml>=6.0.3
|
{agentic_python_coder-3.4.3 → agentic_python_coder-3.4.4}/coder/src/agentic_python_coder/kernel.py
RENAMED
|
@@ -157,7 +157,11 @@ class PythonKernel:
|
|
|
157
157
|
self.with_packages = with_packages or []
|
|
158
158
|
|
|
159
159
|
# Set kernel's working directory
|
|
160
|
-
|
|
160
|
+
# Send the kernel's fd 1 to our stderr: inherited stdout is the
|
|
161
|
+
# JSON-RPC pipe under ipython_mcp, and native code (e.g. solver
|
|
162
|
+
# progress output) writing to fd 1 would corrupt the protocol.
|
|
163
|
+
# print() is unaffected (ipykernel routes it over iopub).
|
|
164
|
+
kernel_kwargs = {"stdout": 2}
|
|
161
165
|
if cwd:
|
|
162
166
|
kernel_kwargs["cwd"] = cwd
|
|
163
167
|
|
|
@@ -5,6 +5,7 @@ Supports multiple concurrent kernels via kernel_id parameter.
|
|
|
5
5
|
|
|
6
6
|
import ast
|
|
7
7
|
import asyncio
|
|
8
|
+
import importlib.metadata
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
@@ -13,7 +14,7 @@ import threading
|
|
|
13
14
|
|
|
14
15
|
from mcp.server import Server
|
|
15
16
|
from mcp.server.stdio import stdio_server
|
|
16
|
-
from mcp.types import TextContent, Tool
|
|
17
|
+
from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool
|
|
17
18
|
|
|
18
19
|
from agentic_python_coder.kernel import (
|
|
19
20
|
DEFAULT_KERNEL_ID,
|
|
@@ -27,11 +28,12 @@ from agentic_python_coder.kernel import (
|
|
|
27
28
|
shutdown_all_kernels,
|
|
28
29
|
)
|
|
29
30
|
|
|
31
|
+
_MCP_V2 = int(importlib.metadata.version("mcp").split(".")[0]) >= 2
|
|
32
|
+
|
|
30
33
|
# Configure logging
|
|
31
34
|
logger = logging.getLogger(__name__)
|
|
32
35
|
|
|
33
|
-
#
|
|
34
|
-
server = Server("ipython_mcp")
|
|
36
|
+
# The MCP server object is created at the end of the module (see _make_server).
|
|
35
37
|
|
|
36
38
|
# Constants
|
|
37
39
|
MAX_OUTPUT = 100 * 1024 # 100KB truncation limit
|
|
@@ -147,7 +149,6 @@ async def execute_with_timeout(
|
|
|
147
149
|
return {"success": False, "kernel_id": kernel_id, "error": error_msg}
|
|
148
150
|
|
|
149
151
|
|
|
150
|
-
@server.list_tools()
|
|
151
152
|
async def list_tools():
|
|
152
153
|
"""List available MCP tools."""
|
|
153
154
|
return [
|
|
@@ -296,7 +297,6 @@ Note: Call this, then call python_exec to continue working.""",
|
|
|
296
297
|
]
|
|
297
298
|
|
|
298
299
|
|
|
299
|
-
@server.call_tool()
|
|
300
300
|
async def call_tool(name: str, arguments: dict):
|
|
301
301
|
"""Handle tool calls."""
|
|
302
302
|
loop = asyncio.get_running_loop()
|
|
@@ -569,6 +569,41 @@ import importlib.metadata
|
|
|
569
569
|
]
|
|
570
570
|
|
|
571
571
|
|
|
572
|
+
async def _on_list_tools(ctx, params) -> ListToolsResult:
|
|
573
|
+
"""mcp>=2 lowlevel handler: wrap the tool list in its result type."""
|
|
574
|
+
return ListToolsResult(tools=await list_tools())
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
async def _on_call_tool(ctx, params) -> CallToolResult:
|
|
578
|
+
"""mcp>=2 lowlevel handler.
|
|
579
|
+
|
|
580
|
+
v2 no longer wraps a returned content list, and an exception would become
|
|
581
|
+
a JSON-RPC error instead of an LLM-visible isError result (v1 behaviour),
|
|
582
|
+
so both are done here.
|
|
583
|
+
"""
|
|
584
|
+
try:
|
|
585
|
+
content = await call_tool(params.name, params.arguments or {})
|
|
586
|
+
except Exception as e: # keep v1 semantics: error text reaches the model
|
|
587
|
+
return CallToolResult(
|
|
588
|
+
content=[TextContent(type="text", text=str(e))], is_error=True
|
|
589
|
+
)
|
|
590
|
+
return CallToolResult(content=content)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _make_server() -> Server:
|
|
594
|
+
if _MCP_V2:
|
|
595
|
+
return Server(
|
|
596
|
+
"ipython_mcp", on_list_tools=_on_list_tools, on_call_tool=_on_call_tool
|
|
597
|
+
)
|
|
598
|
+
srv = Server("ipython_mcp")
|
|
599
|
+
srv.list_tools()(list_tools)
|
|
600
|
+
srv.call_tool()(call_tool)
|
|
601
|
+
return srv
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
server = _make_server()
|
|
605
|
+
|
|
606
|
+
|
|
572
607
|
async def run_server():
|
|
573
608
|
"""Run the MCP server."""
|
|
574
609
|
async with stdio_server() as (read, write):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentic-python-coder"
|
|
3
|
-
version = "3.4.
|
|
3
|
+
version = "3.4.4"
|
|
4
4
|
description = "A lightweight Python coding agent that writes, executes, and iterates on code through natural language instructions"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
"jupyter-client>=8.9.1",
|
|
28
28
|
"ipykernel>=7.3.0",
|
|
29
29
|
"PyYAML>=6.0.3",
|
|
30
|
-
"mcp>=1.28.1,<
|
|
30
|
+
"mcp>=1.28.1,<3",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.scripts]
|
|
@@ -82,6 +82,10 @@ target-version = "py313"
|
|
|
82
82
|
line-length = 88
|
|
83
83
|
exclude = ["CPBENCH", "LOCAL", "TRASH"]
|
|
84
84
|
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
# Explicit: ruff 0.17 widened its default rule set
|
|
87
|
+
select = ["E4", "E7", "E9", "F"]
|
|
88
|
+
|
|
85
89
|
[tool.mypy]
|
|
86
90
|
python_version = "3.13"
|
|
87
91
|
warn_return_any = true
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
"""Tests for the IPython kernel module."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from agentic_python_coder.kernel import (
|
|
5
|
-
PythonKernel,
|
|
6
|
-
get_kernel,
|
|
7
|
-
shutdown_kernel,
|
|
8
|
-
format_output,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def kernel():
|
|
14
|
-
"""Create a fresh kernel for each test."""
|
|
15
|
-
# Shutdown any existing global kernel
|
|
16
|
-
shutdown_kernel()
|
|
17
|
-
# Create a new kernel
|
|
18
|
-
k = PythonKernel()
|
|
19
|
-
yield k
|
|
20
|
-
# Cleanup
|
|
21
|
-
k.shutdown()
|
|
22
|
-
shutdown_kernel()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TestKernelBasics:
|
|
26
|
-
"""Test basic kernel execution."""
|
|
27
|
-
|
|
28
|
-
def test_simple_expression(self, kernel):
|
|
29
|
-
"""Basic expression returns result."""
|
|
30
|
-
output = kernel.execute("2 + 2")
|
|
31
|
-
assert output["error"] is None
|
|
32
|
-
assert output["result"] == "4"
|
|
33
|
-
|
|
34
|
-
def test_print_output(self, kernel):
|
|
35
|
-
"""Print statements captured in stdout."""
|
|
36
|
-
output = kernel.execute("print('hello world')")
|
|
37
|
-
assert output["error"] is None
|
|
38
|
-
assert "hello world" in output["stdout"]
|
|
39
|
-
|
|
40
|
-
def test_multiline_code(self, kernel):
|
|
41
|
-
"""Multiline code executes correctly."""
|
|
42
|
-
code = """
|
|
43
|
-
x = 10
|
|
44
|
-
y = 20
|
|
45
|
-
x + y
|
|
46
|
-
"""
|
|
47
|
-
output = kernel.execute(code)
|
|
48
|
-
assert output["error"] is None
|
|
49
|
-
assert output["result"] == "30"
|
|
50
|
-
|
|
51
|
-
def test_syntax_error(self, kernel):
|
|
52
|
-
"""Syntax errors are captured."""
|
|
53
|
-
output = kernel.execute("def (")
|
|
54
|
-
assert output["error"] is not None
|
|
55
|
-
assert "SyntaxError" in output["error"]
|
|
56
|
-
|
|
57
|
-
def test_runtime_error(self, kernel):
|
|
58
|
-
"""Runtime errors are captured."""
|
|
59
|
-
output = kernel.execute("1 / 0")
|
|
60
|
-
assert output["error"] is not None
|
|
61
|
-
assert "ZeroDivisionError" in output["error"]
|
|
62
|
-
|
|
63
|
-
def test_stderr_capture(self, kernel):
|
|
64
|
-
"""Stderr is captured."""
|
|
65
|
-
output = kernel.execute("import sys; print('error', file=sys.stderr)")
|
|
66
|
-
assert "error" in output["stderr"]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class TestKernelStatePersistence:
|
|
70
|
-
"""Test that state persists across executions."""
|
|
71
|
-
|
|
72
|
-
def test_variable_persistence(self, kernel):
|
|
73
|
-
"""Variables persist between calls."""
|
|
74
|
-
kernel.execute("x = 42")
|
|
75
|
-
output = kernel.execute("x * 2")
|
|
76
|
-
assert output["result"] == "84"
|
|
77
|
-
|
|
78
|
-
def test_function_persistence(self, kernel):
|
|
79
|
-
"""Functions persist between calls."""
|
|
80
|
-
kernel.execute("def double(n): return n * 2")
|
|
81
|
-
output = kernel.execute("double(21)")
|
|
82
|
-
assert output["result"] == "42"
|
|
83
|
-
|
|
84
|
-
def test_import_persistence(self, kernel):
|
|
85
|
-
"""Imports persist between calls."""
|
|
86
|
-
kernel.execute("import math")
|
|
87
|
-
output = kernel.execute("math.pi")
|
|
88
|
-
assert "3.14" in output["result"]
|
|
89
|
-
|
|
90
|
-
def test_class_persistence(self, kernel):
|
|
91
|
-
"""Classes persist between calls."""
|
|
92
|
-
kernel.execute("class Counter:\n def __init__(self):\n self.n = 0")
|
|
93
|
-
kernel.execute("c = Counter()")
|
|
94
|
-
kernel.execute("c.n = 5")
|
|
95
|
-
output = kernel.execute("c.n")
|
|
96
|
-
assert output["result"] == "5"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class TestMessageLoopFix:
|
|
100
|
-
"""Test that message loop waits for idle status."""
|
|
101
|
-
|
|
102
|
-
def test_slow_print_captured(self, kernel):
|
|
103
|
-
"""Slow output is fully captured (waits for idle, not Empty)."""
|
|
104
|
-
# This used to break early on Empty before the fix
|
|
105
|
-
code = """
|
|
106
|
-
import time
|
|
107
|
-
for i in range(5):
|
|
108
|
-
print(f"line {i}")
|
|
109
|
-
time.sleep(0.1)
|
|
110
|
-
print("done")
|
|
111
|
-
"""
|
|
112
|
-
output = kernel.execute(code)
|
|
113
|
-
assert output["error"] is None
|
|
114
|
-
assert "line 0" in output["stdout"]
|
|
115
|
-
assert "line 4" in output["stdout"]
|
|
116
|
-
assert "done" in output["stdout"]
|
|
117
|
-
|
|
118
|
-
def test_delayed_result(self, kernel):
|
|
119
|
-
"""Result from delayed computation is captured."""
|
|
120
|
-
code = """
|
|
121
|
-
import time
|
|
122
|
-
time.sleep(0.5)
|
|
123
|
-
42 * 2
|
|
124
|
-
"""
|
|
125
|
-
output = kernel.execute(code)
|
|
126
|
-
assert output["error"] is None
|
|
127
|
-
assert output["result"] == "84"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class TestFormatOutput:
|
|
131
|
-
"""Test output formatting."""
|
|
132
|
-
|
|
133
|
-
def test_success_format(self):
|
|
134
|
-
"""Successful output is formatted correctly."""
|
|
135
|
-
output = {"stdout": "hello", "stderr": "", "result": "42", "error": None}
|
|
136
|
-
result = format_output(output)
|
|
137
|
-
assert '"success": true' in result
|
|
138
|
-
assert '"stdout": "hello"' in result
|
|
139
|
-
assert '"result": "42"' in result
|
|
140
|
-
|
|
141
|
-
def test_error_format(self):
|
|
142
|
-
"""Error output is formatted correctly."""
|
|
143
|
-
output = {
|
|
144
|
-
"stdout": "",
|
|
145
|
-
"stderr": "",
|
|
146
|
-
"result": None,
|
|
147
|
-
"error": "ValueError: bad",
|
|
148
|
-
}
|
|
149
|
-
result = format_output(output)
|
|
150
|
-
assert '"success": false' in result
|
|
151
|
-
assert "ValueError" in result
|
|
152
|
-
|
|
153
|
-
def test_none_values_removed(self):
|
|
154
|
-
"""None values are removed from output."""
|
|
155
|
-
output = {"stdout": "", "stderr": None, "result": None, "error": None}
|
|
156
|
-
result = format_output(output)
|
|
157
|
-
assert "stderr" not in result
|
|
158
|
-
assert "result" not in result
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class TestGlobalKernel:
|
|
162
|
-
"""Test global kernel management."""
|
|
163
|
-
|
|
164
|
-
def test_get_kernel_creates_kernel(self):
|
|
165
|
-
"""get_kernel creates a new kernel if none exists."""
|
|
166
|
-
shutdown_kernel() # Ensure clean state
|
|
167
|
-
k = get_kernel()
|
|
168
|
-
assert k is not None
|
|
169
|
-
output = k.execute("1 + 1")
|
|
170
|
-
assert output["result"] == "2"
|
|
171
|
-
shutdown_kernel()
|
|
172
|
-
|
|
173
|
-
def test_get_kernel_reuses_kernel(self):
|
|
174
|
-
"""get_kernel reuses existing kernel with same config."""
|
|
175
|
-
shutdown_kernel()
|
|
176
|
-
k1 = get_kernel()
|
|
177
|
-
k1.execute("x = 123")
|
|
178
|
-
k2 = get_kernel()
|
|
179
|
-
output = k2.execute("x")
|
|
180
|
-
assert output["result"] == "123"
|
|
181
|
-
shutdown_kernel()
|
|
182
|
-
|
|
183
|
-
def test_shutdown_clears_state(self):
|
|
184
|
-
"""shutdown_kernel clears the global kernel."""
|
|
185
|
-
shutdown_kernel()
|
|
186
|
-
k = get_kernel()
|
|
187
|
-
k.execute("y = 456")
|
|
188
|
-
shutdown_kernel()
|
|
189
|
-
k2 = get_kernel()
|
|
190
|
-
output = k2.execute("y")
|
|
191
|
-
assert output["error"] is not None
|
|
192
|
-
assert "NameError" in output["error"]
|
|
193
|
-
shutdown_kernel()
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class TestDeadlineTimeout:
|
|
197
|
-
"""Deadline overrun must report a timeout error, not silent success."""
|
|
198
|
-
|
|
199
|
-
def test_timeout_reports_error_and_interrupts(self, kernel):
|
|
200
|
-
output = kernel.execute("import time; time.sleep(30)", deadline_timeout=2)
|
|
201
|
-
assert output["error"] is not None
|
|
202
|
-
assert "timed out" in output["error"]
|
|
203
|
-
|
|
204
|
-
# The interrupt (plus grace period) must leave the kernel usable
|
|
205
|
-
output2 = kernel.execute("21 * 2")
|
|
206
|
-
assert output2["error"] is None
|
|
207
|
-
assert output2["result"] == "42"
|
|
208
|
-
|
|
209
|
-
def test_fast_code_unaffected_by_deadline(self, kernel):
|
|
210
|
-
output = kernel.execute("1 + 1", deadline_timeout=30)
|
|
211
|
-
assert output["error"] is None
|
|
212
|
-
assert output["result"] == "2"
|