diff --git a/README.md b/README.md index 7832a26..657cd11 100644 --- a/README.md +++ b/README.md @@ -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 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 diff --git a/mac/command.cpp b/mac/command.cpp index 6dd9bb8..0754211 100644 --- a/mac/command.cpp +++ b/mac/command.cpp @@ -9,6 +9,7 @@ #include #include "argv.h" +#include "eval_python.h" #include "file.h" #include "log.h" #include "machine.h" @@ -73,6 +74,9 @@ void set_verbose_level(int argc, char* argv[]) args.opt("v", "'verbosity'", "level", "0", "'verbosity'"); args.parse(argc, argv, false); 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 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 " diff --git a/mac/eval_python.cpp b/mac/eval_python.cpp index f56f8a4..a17f098 100644 --- a/mac/eval_python.cpp +++ b/mac/eval_python.cpp @@ -6,6 +6,65 @@ std::regex Eval_python::statement_delimiter("\\s*;\\s*"); 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) : m_machine(machine) , m_loc(loc) @@ -30,6 +89,7 @@ Eval_python::Eval_python(Machine& machine, const Locator& loc) m_globals = PyDict_New(); m_locals = PyDict_New(); 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 // as an argument (the Python counterpart of a :cpp phase function reading // machine.m_result). Set directly rather than through the state's diff --git a/mac/eval_python.h b/mac/eval_python.h index 7a985c5..b06c44c 100644 --- a/mac/eval_python.h +++ b/mac/eval_python.h @@ -5,6 +5,18 @@ #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 { public: diff --git a/sks/kutil/kutil.py b/sks/kutil/kutil.py index 97b8b09..34aaea6 100644 --- a/sks/kutil/kutil.py +++ b/sks/kutil/kutil.py @@ -1,5 +1,6 @@ import os import re +import sys import inspect import html_util from html_util import E @@ -14,9 +15,18 @@ def blue_text(text): def red_text(text): 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 - 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): diff --git a/tst/eval_test.sh b/tst/eval_test.sh index 7b8f6a5..204a968 100755 --- a/tst/eval_test.sh +++ b/tst/eval_test.sh @@ -273,6 +273,32 @@ else fi 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 ": " (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 "Results: ${PASS} passed, ${FAIL} failed" diff --git a/tst/kpy_test.sh b/tst/kpy_test.sh index 60fb5a2..35e1287 100755 --- a/tst/kpy_test.sh +++ b/tst/kpy_test.sh @@ -276,6 +276,17 @@ except kt.Error as e: EOF 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' import klammertext as kt try: