The Python binding (kpy/): klammertext.Machine from a script
Assembled from klammertext-dev ed13dfaa5b7a. kpy/klammertext.cpp is a CPython extension module giving a Python script an instance of the Machine class -- ktext in-process, set up through the same functions the command uses. Errors map one-to-one to Python exceptions under the C++ names; an @eval's Python exception is chained as __cause__. Built by make -C mac, found through PYTHONPATH (env/runtime.env sets it), tested by tst/kpy_test.sh. Three engine changes came with it: Error::message(), set_command_state(), and the @eval error report carrying its traceback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uv7KWxafinDPraswEf4GsC
This commit is contained in:
1 parent
e4f1307aa9
commit
760fda5470
17 files changed
+1241
-78
No files matched your search
+85
-49
@@ -4,6 +4,7 @@
|
||||
#include "log.h"
|
||||
|
||||
std::regex Eval_python::statement_delimiter("\\s*;\\s*");
|
||||
PyObject* Eval_python::s_last_exception = nullptr;
|
||||
|
||||
Eval_python::Eval_python(Machine& machine, const Locator& loc)
|
||||
: m_machine(machine)
|
||||
@@ -119,52 +120,10 @@ void Eval_python::import_module(const std::string& module_name, bool verify)
|
||||
}
|
||||
PyObject* module = PyImport_ImportModule(module_name.c_str());
|
||||
if (module == nullptr) {
|
||||
// Extract the Python traceback before clearing the error.
|
||||
// This reveals the actual source of the failure (e.g., a syntax
|
||||
// error in a transitively imported module), not just the top-level
|
||||
// module name that failed to load.
|
||||
std::string detail;
|
||||
PyObject* ptype;
|
||||
PyObject* pvalue;
|
||||
PyObject* ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
if (pvalue) {
|
||||
PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
|
||||
PyObject* str = PyObject_Str(pvalue);
|
||||
if (str) {
|
||||
detail = PyUnicode_AsUTF8(str);
|
||||
Py_DECREF(str);
|
||||
}
|
||||
// Format the traceback if available
|
||||
if (ptraceback) {
|
||||
PyObject* tb_module = PyImport_ImportModule("traceback");
|
||||
if (tb_module) {
|
||||
PyObject* format_tb = PyObject_GetAttrString(tb_module, "format_exception");
|
||||
if (format_tb) {
|
||||
PyObject* args = PyTuple_Pack(3, ptype, pvalue, ptraceback);
|
||||
PyObject* tb_list = PyObject_CallObject(format_tb, args);
|
||||
if (tb_list) {
|
||||
PyObject* separator = PyUnicode_FromString("");
|
||||
PyObject* joined = PyUnicode_Join(separator, tb_list);
|
||||
if (joined) {
|
||||
detail = PyUnicode_AsUTF8(joined);
|
||||
Py_DECREF(joined);
|
||||
}
|
||||
Py_DECREF(separator);
|
||||
Py_DECREF(tb_list);
|
||||
}
|
||||
Py_XDECREF(args);
|
||||
Py_DECREF(format_tb);
|
||||
}
|
||||
Py_DECREF(tb_module);
|
||||
}
|
||||
}
|
||||
}
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
PyErr_Clear();
|
||||
|
||||
// The traceback, not just the top-level module name that failed to
|
||||
// load: it reveals the actual source of the failure (e.g. a syntax
|
||||
// error in a transitively imported module).
|
||||
std::string detail = fetch_python_exception();
|
||||
std::string message = "Cannot import module \"" + module_name + "\"";
|
||||
if (!detail.empty()) {
|
||||
message += ":\n\n" + detail;
|
||||
@@ -176,6 +135,68 @@ void Eval_python::import_module(const std::string& module_name, bool verify)
|
||||
PyDict_SetItemString(m_globals, module_name.c_str(), module);
|
||||
}
|
||||
|
||||
PyObject* Eval_python::take_last_exception()
|
||||
{
|
||||
PyObject* e = s_last_exception;
|
||||
s_last_exception = nullptr;
|
||||
return e;
|
||||
}
|
||||
|
||||
std::string Eval_python::fetch_python_exception()
|
||||
{
|
||||
std::string detail {};
|
||||
PyObject* ptype = nullptr;
|
||||
PyObject* pvalue = nullptr;
|
||||
PyObject* ptraceback = nullptr;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
if (!pvalue) {
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(ptraceback);
|
||||
PyErr_Clear();
|
||||
return detail;
|
||||
}
|
||||
PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
|
||||
if (ptraceback) {
|
||||
// Attach the traceback to the instance, so whoever receives the
|
||||
// object (the binding, as __cause__) can still print it.
|
||||
PyException_SetTraceback(pvalue, ptraceback);
|
||||
}
|
||||
PyObject* str = PyObject_Str(pvalue);
|
||||
if (str) {
|
||||
detail = PyUnicode_AsUTF8(str);
|
||||
Py_DECREF(str);
|
||||
}
|
||||
PyObject* tb_module = PyImport_ImportModule("traceback");
|
||||
if (tb_module) {
|
||||
PyObject* format = PyObject_GetAttrString(tb_module, "format_exception");
|
||||
if (format) {
|
||||
PyObject* args = PyTuple_Pack(
|
||||
3, ptype, pvalue, ptraceback ? ptraceback : Py_None);
|
||||
PyObject* tb_list = PyObject_CallObject(format, args);
|
||||
if (tb_list) {
|
||||
PyObject* separator = PyUnicode_FromString("");
|
||||
PyObject* joined = PyUnicode_Join(separator, tb_list);
|
||||
if (joined) {
|
||||
detail = PyUnicode_AsUTF8(joined);
|
||||
Py_DECREF(joined);
|
||||
}
|
||||
Py_DECREF(separator);
|
||||
Py_DECREF(tb_list);
|
||||
}
|
||||
Py_XDECREF(args);
|
||||
Py_DECREF(format);
|
||||
}
|
||||
Py_DECREF(tb_module);
|
||||
}
|
||||
// Keep the instance (it owns its traceback now); drop the rest.
|
||||
Py_XDECREF(s_last_exception);
|
||||
s_last_exception = pvalue;
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(ptraceback);
|
||||
PyErr_Clear();
|
||||
return detail;
|
||||
}
|
||||
|
||||
std::string Eval_python::get_result(PyObject* result_object)
|
||||
{
|
||||
std::string result {};
|
||||
@@ -184,9 +205,24 @@ std::string Eval_python::get_result(PyObject* result_object)
|
||||
result = std::string(value);
|
||||
Py_DECREF(result_object);
|
||||
} else {
|
||||
std::cout << red;
|
||||
PyErr_Print();
|
||||
throw Parsing_error("Python code error in @eval", m_loc);
|
||||
// Until 2026-09-08 this was "std::cout << red; PyErr_Print();" --
|
||||
// the colour escape on stdout, the traceback on stderr, and then an
|
||||
// Error carrying neither. The traceback now travels IN the error,
|
||||
// indented so justify() leaves its lines verbatim (a traceback line
|
||||
// is not prose), and the exception object is kept for the binding.
|
||||
std::string detail = fetch_python_exception();
|
||||
std::string message = "Python code error in @eval";
|
||||
if (!detail.empty()) {
|
||||
message += ":\n\n";
|
||||
std::string::size_type from = 0;
|
||||
while (from < detail.size()) {
|
||||
auto nl = detail.find('\n', from);
|
||||
if (nl == std::string::npos) nl = detail.size();
|
||||
message += " " + detail.substr(from, nl - from) + "\n";
|
||||
from = nl + 1;
|
||||
}
|
||||
}
|
||||
throw Parsing_error(message, m_loc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user