A klammer set's Python speaks through the log
Assembled from development commit d5f9c20b94b3. Since 5c29da5: a print() in
@eval, or in any Python a klammer set runs, is a log line -- on the
error stream under the command's name at -v 1, silent otherwise, never
in the document -- and K_log(level, text) is the builtin behind it; a
Python script using the binding keeps its own stdout. Previously such
output was lost, or on a terminal landed in the document.
This commit is contained in:
1 parent
5c29da52dc
commit
fc2e848e91
7 files changed
+126
-3
No files matched your search
@@ -53,7 +53,7 @@ are regenerated on each release — patches cannot be merged directly.
|
|||||||
Report problems (or send patches) to the author; accepted changes are
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `632297b7657a`.
|
This snapshot was assembled from development commit `d5f9c20b94b3`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
|
||||||
#include "argv.h"
|
#include "argv.h"
|
||||||
|
#include "eval_python.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
@@ -73,6 +74,9 @@ void set_verbose_level(int argc, char* argv[])
|
|||||||
args.opt("v", "'verbosity'", "level", "0", "'verbosity'");
|
args.opt("v", "'verbosity'", "level", "0", "'verbosity'");
|
||||||
args.parse(argc, argv, false);
|
args.parse(argc, argv, false);
|
||||||
verbose_level = args.as_verbosity("v");
|
verbose_level = args.as_verbosity("v");
|
||||||
|
// A klammer set's Python speaks through the log (eval_python.h): the
|
||||||
|
// commands only -- the binding leaves the host's sys.stdout alone.
|
||||||
|
set_python_output_to_log(true);
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (console_output_cp != 0 && console_output_cp != CP_UTF8) {
|
if (console_output_cp != 0 && console_output_cp != CP_UTF8) {
|
||||||
K::log(1, "Console output code page set to UTF-8 for this run (was "
|
K::log(1, "Console output code page set to UTF-8 for this run (was "
|
||||||
|
|||||||
@@ -6,6 +6,65 @@
|
|||||||
std::regex Eval_python::statement_delimiter("\\s*;\\s*");
|
std::regex Eval_python::statement_delimiter("\\s*;\\s*");
|
||||||
PyObject* Eval_python::s_last_exception = nullptr;
|
PyObject* Eval_python::s_last_exception = nullptr;
|
||||||
|
|
||||||
|
static bool s_python_output_to_log = false;
|
||||||
|
|
||||||
|
void set_python_output_to_log(bool on)
|
||||||
|
{
|
||||||
|
s_python_output_to_log = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
// K_log(level, text): the engine's K::log, for Python. A builtin, so a
|
||||||
|
// klammer set's modules reach it without an import; absent outside a
|
||||||
|
// Machine (kutil.msg falls back to stderr then).
|
||||||
|
static PyObject* py_K_log(PyObject*, PyObject* args)
|
||||||
|
{
|
||||||
|
int level = 1;
|
||||||
|
const char* text = nullptr;
|
||||||
|
if (!PyArg_ParseTuple(args, "is", &level, &text)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
(void)K::log(level, std::string(text));
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyMethodDef K_log_method = {
|
||||||
|
"K_log", py_K_log, METH_VARARGS,
|
||||||
|
"K_log(level, text): write text to the Klammertext log, shown at -v level or higher."};
|
||||||
|
|
||||||
|
static void install_python_log(PyObject* globals)
|
||||||
|
{
|
||||||
|
PyObject* builtins = PyEval_GetBuiltins(); // borrowed
|
||||||
|
if (!PyDict_GetItemString(builtins, "K_log")) {
|
||||||
|
PyObject* f = PyCFunction_New(&K_log_method, nullptr);
|
||||||
|
if (f) {
|
||||||
|
PyDict_SetItemString(builtins, "K_log", f);
|
||||||
|
Py_DECREF(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!s_python_output_to_log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// sys.stdout -> the log, line by line, once per process (the class
|
||||||
|
// attribute marks ours; a second Eval_python leaves it in place).
|
||||||
|
PyRun_String(
|
||||||
|
"import sys\n"
|
||||||
|
"if not getattr(sys.stdout, '_klammertext_log', False):\n"
|
||||||
|
" class _K_log_stream:\n"
|
||||||
|
" _klammertext_log = True\n"
|
||||||
|
" def __init__(self): self._buf = ''\n"
|
||||||
|
" def write(self, s):\n"
|
||||||
|
" self._buf += s\n"
|
||||||
|
" while '\\n' in self._buf:\n"
|
||||||
|
" line, self._buf = self._buf.split('\\n', 1)\n"
|
||||||
|
" K_log(1, line)\n"
|
||||||
|
" return len(s)\n"
|
||||||
|
" def flush(self):\n"
|
||||||
|
" if self._buf: K_log(1, self._buf); self._buf = ''\n"
|
||||||
|
" def isatty(self): return False\n"
|
||||||
|
" sys.stdout = _K_log_stream()\n",
|
||||||
|
Py_file_input, globals, globals);
|
||||||
|
}
|
||||||
|
|
||||||
Eval_python::Eval_python(Machine& machine, const Locator& loc)
|
Eval_python::Eval_python(Machine& machine, const Locator& loc)
|
||||||
: m_machine(machine)
|
: m_machine(machine)
|
||||||
, m_loc(loc)
|
, m_loc(loc)
|
||||||
@@ -30,6 +89,7 @@ Eval_python::Eval_python(Machine& machine, const Locator& loc)
|
|||||||
m_globals = PyDict_New();
|
m_globals = PyDict_New();
|
||||||
m_locals = PyDict_New();
|
m_locals = PyDict_New();
|
||||||
PyDict_SetItemString(m_globals, "__builtins__", PyEval_GetBuiltins());
|
PyDict_SetItemString(m_globals, "__builtins__", PyEval_GetBuiltins());
|
||||||
|
install_python_log(m_globals);
|
||||||
// The machine's result text, so :after_apply phase functions can take it
|
// The machine's result text, so :after_apply phase functions can take it
|
||||||
// as an argument (the Python counterpart of a :cpp phase function reading
|
// as an argument (the Python counterpart of a :cpp phase function reading
|
||||||
// machine.m_result). Set directly rather than through the state's
|
// machine.m_result). Set directly rather than through the state's
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
|
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
|
|
||||||
|
// Where a klammer set's Python may speak. Embedded Python's own stdout is
|
||||||
|
// nowhere a user looks: its buffer is never flushed (no Py_Finalize), so on
|
||||||
|
// a pipe or a file a print() vanished, and on a terminal it landed in the
|
||||||
|
// document's stream. Under a command, Eval_python routes it to the engine's
|
||||||
|
// log instead: K_log(level, text) is a builtin every module sees (kutil.msg
|
||||||
|
// uses it), and sys.stdout is replaced by a stream that logs each line at
|
||||||
|
// level 1 -- stderr under -v, silent otherwise, never the document (the
|
||||||
|
// output policy in CLAUDE.md). The switch is ON for the commands
|
||||||
|
// (set_verbose_level) and OFF for the Python binding, where sys.stdout is
|
||||||
|
// the host script's own and must not be touched.
|
||||||
|
void set_python_output_to_log(bool on);
|
||||||
|
|
||||||
class Eval_python
|
class Eval_python
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
+12
-2
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
import html_util
|
import html_util
|
||||||
from html_util import E
|
from html_util import E
|
||||||
@@ -14,9 +15,18 @@ def blue_text(text):
|
|||||||
def red_text(text):
|
def red_text(text):
|
||||||
return f"\033[31m{text}\033[0m"
|
return f"\033[31m{text}\033[0m"
|
||||||
|
|
||||||
def msg(text=""):
|
def msg(text="", level=1):
|
||||||
|
"""A message from the klammer set: the engine's log (K_log, a builtin
|
||||||
|
under ktext), shown at -v LEVEL or higher on stderr, never in the
|
||||||
|
document. Outside a Machine (a module run by hand) it goes to stderr.
|
||||||
|
Until 2026-09-25 this was a print(): embedded Python's stdout, which no
|
||||||
|
user ever saw."""
|
||||||
frame = inspect.currentframe().f_back
|
frame = inspect.currentframe().f_back
|
||||||
print(blue_text(f"[{os.path.basename(frame.f_code.co_filename)}:{frame.f_lineno}]"), text)
|
where = f"[{os.path.basename(frame.f_code.co_filename)}:{frame.f_lineno}]"
|
||||||
|
try:
|
||||||
|
K_log(level, f"{where} {text}")
|
||||||
|
except NameError:
|
||||||
|
print(where, text, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def escape(s):
|
def escape(s):
|
||||||
|
|||||||
@@ -273,6 +273,32 @@ else
|
|||||||
fi
|
fi
|
||||||
rm -rf "$CTX"
|
rm -rf "$CTX"
|
||||||
|
|
||||||
|
# A klammer set's Python speaks through the log (2026-09-25, TODO 69): a
|
||||||
|
# print() -- embedded Python's stdout, which was never flushed and so never
|
||||||
|
# seen, or on a terminal landed in the document -- goes to stderr at -v 1
|
||||||
|
# under the command's name, and never into the result; K_log(level, text)
|
||||||
|
# is the builtin behind it.
|
||||||
|
"$K/bin/ktext" --klammersets none -s '@eval print("from print"); K_log(2, "from K_log"); 42 @' -d >"$OUTF" 2>"$ERRF"
|
||||||
|
if [ "$(cat "$OUTF")" = "42" ] && [ ! -s "$ERRF" ]; then
|
||||||
|
pass "23. at -v 0 a print() in @eval is silent and the result is the result alone"
|
||||||
|
else
|
||||||
|
fail "23. stdout [$(cat "$OUTF")], stderr [$(plain < "$ERRF" | head -2 | tr '\n' '|')]"
|
||||||
|
fi
|
||||||
|
"$K/bin/ktext" --klammersets none -s '@eval print("from print"); K_log(2, "from K_log"); 42 @' -d -v 1 >"$OUTF" 2>"$ERRF"
|
||||||
|
# A log line is "<prefix>: <text> " (K::log ends with a space); the -v 1
|
||||||
|
# argument dump also echoes the -s text, so the checks want the log form.
|
||||||
|
if [ "$(cat "$OUTF")" = "42" ] && grep -qE 'ktext(\.exe)?: from print $' "$ERRF" && ! grep -qE ': from K_log $' "$ERRF"; then
|
||||||
|
pass "24. at -v 1 the print() is a log line on stderr; a level-2 K_log is not"
|
||||||
|
else
|
||||||
|
fail "24. stdout [$(cat "$OUTF")], stderr [$(plain < "$ERRF" | grep -v '^ ' | tr '\n' '|')]"
|
||||||
|
fi
|
||||||
|
"$K/bin/ktext" --klammersets none -s '@eval K_log(2, "from K_log"); 42 @' -d -v 2 >"$OUTF" 2>"$ERRF"
|
||||||
|
if [ "$(cat "$OUTF")" = "42" ] && grep -qE ': from K_log $' "$ERRF"; then
|
||||||
|
pass "25. at -v 2 the level-2 K_log line appears"
|
||||||
|
else
|
||||||
|
fail "25. stdout [$(cat "$OUTF")]" "$(plain < "$ERRF" | tail -3 | tr '\n' '|')"
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "==========="
|
echo "==========="
|
||||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||||
|
|||||||
@@ -276,6 +276,17 @@ except kt.Error as e:
|
|||||||
EOF
|
EOF
|
||||||
expect "12a. an error inside a klammer body is located at the application" 'error 3 1 True'
|
expect "12a. an error inside a klammer body is located at the application" 'error 3 1 True'
|
||||||
|
|
||||||
|
# The binding does NOT redirect sys.stdout: a print() in a document's @eval
|
||||||
|
# is the host script's own stdout (under ktext it is a log line instead).
|
||||||
|
run <<'EOF'
|
||||||
|
import klammertext as kt
|
||||||
|
M = kt.Machine(klammersets="none")
|
||||||
|
M.read("@eval print('host stdout'); 1 @")
|
||||||
|
print("result", M.apply())
|
||||||
|
EOF
|
||||||
|
expect "12b. a print() in @eval reaches the host script's stdout" 'host stdout
|
||||||
|
result 1'
|
||||||
|
|
||||||
run <<'EOF'
|
run <<'EOF'
|
||||||
import klammertext as kt
|
import klammertext as kt
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in new issue
Block a user