Skip to content

Commit e40030f

Browse files
committed
gui: use the stable API for Python
Signed-off-by: Matt Liberty <[email protected]>
1 parent 57de8e4 commit e40030f

File tree

3 files changed

+149
-175
lines changed

3 files changed

+149
-175
lines changed

src/gui/src/pythonCmdInputWidget.cpp

Lines changed: 130 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include <QCoreApplication>
4646
#include <cstddef>
4747
#include <functional>
48+
#include <iostream>
4849
#include <map>
4950
#include <memory>
5051
#include <string>
@@ -55,141 +56,144 @@
5556

5657
namespace gui {
5758

58-
// https://stackoverflow.com/questions/4307187/how-to-catch-python-stdout-in-c-code/8335297#8335297
59-
namespace emb {
60-
6159
using stdout_write_type = std::function<void(std::string)>;
6260

63-
struct Stdout
61+
struct StreamCatcher
6462
{
65-
PyObject_HEAD stdout_write_type write;
63+
PyObject_HEAD;
64+
stdout_write_type write;
65+
std::string buffer;
66+
bool is_stderr; // distinguish stdout vs stderr
6667
};
6768

68-
static PyObject* Stdout_write(PyObject* self, PyObject* args)
69+
// method implementation
70+
static PyObject* StreamCatcher_write(PyObject* self, PyObject* args)
6971
{
70-
std::size_t written(0);
71-
Stdout* selfimpl = reinterpret_cast<Stdout*>(self);
72-
if (selfimpl->write) {
73-
char* data;
74-
if (!PyArg_ParseTuple(args, "s", &data)) {
75-
return nullptr;
76-
}
72+
const char* text = nullptr;
73+
if (!PyArg_ParseTuple(args, "s", &text)) {
74+
Py_RETURN_NONE;
75+
}
7776

78-
std::string str(data);
79-
selfimpl->write(str);
80-
written = str.size();
77+
if (text[0] == '\0') {
78+
Py_RETURN_NONE;
8179
}
82-
return PyLong_FromSize_t(written);
83-
}
8480

85-
static PyObject* Stdout_flush(PyObject* self, PyObject* args)
86-
{
87-
// no-op
88-
return Py_BuildValue("");
89-
}
81+
auto* catcher = reinterpret_cast<StreamCatcher*>(self);
82+
catcher->buffer += text;
9083

91-
static PyMethodDef Stdout_methods[] = {
92-
{"write", Stdout_write, METH_VARARGS, "sys.stdout.write"},
93-
{"flush", Stdout_flush, METH_VARARGS, "sys.stdout.flush"},
94-
{nullptr, nullptr, 0, nullptr} // sentinel
95-
};
96-
97-
static PyTypeObject StdoutType = {
98-
PyVarObject_HEAD_INIT(0, 0) "emb.StdoutType", /* tp_name */
99-
sizeof(Stdout), /* tp_basicsize */
100-
0, /* tp_itemsize */
101-
nullptr, /* tp_dealloc */
102-
0, /* tp_print */
103-
nullptr, /* tp_getattr */
104-
nullptr, /* tp_setattr */
105-
nullptr, /* tp_reserved */
106-
nullptr, /* tp_repr */
107-
nullptr, /* tp_as_number */
108-
nullptr, /* tp_as_sequence */
109-
nullptr, /* tp_as_mapping */
110-
nullptr, /* tp_hash */
111-
nullptr, /* tp_call */
112-
nullptr, /* tp_str */
113-
nullptr, /* tp_getattro */
114-
nullptr, /* tp_setattro */
115-
nullptr, /* tp_as_buffer */
116-
Py_TPFLAGS_DEFAULT, /* tp_flags */
117-
"emb.Stdout objects", /* tp_doc */
118-
nullptr, /* tp_traverse */
119-
nullptr, /* tp_clear */
120-
nullptr, /* tp_richcompare */
121-
0, /* tp_weaklistoffset */
122-
nullptr, /* tp_iter */
123-
nullptr, /* tp_iternext */
124-
Stdout_methods, /* tp_methods */
125-
nullptr, /* tp_members */
126-
nullptr, /* tp_getset */
127-
nullptr, /* tp_base */
128-
nullptr, /* tp_dict */
129-
nullptr, /* tp_descr_get */
130-
nullptr, /* tp_descr_set */
131-
0, /* tp_dictoffset */
132-
nullptr, /* tp_init */
133-
nullptr, /* tp_alloc */
134-
nullptr, /* tp_new */
135-
};
84+
if (catcher->buffer.back() == '\n') {
85+
const size_t end = catcher->buffer.find_last_not_of("\r\n");
86+
catcher->buffer.erase(end + 1);
87+
} else {
88+
Py_RETURN_NONE;
89+
}
13690

137-
static PyModuleDef embmodule = {
138-
PyModuleDef_HEAD_INIT,
139-
"emb",
140-
nullptr,
141-
-1,
142-
nullptr,
143-
};
91+
catcher->write(catcher->buffer);
92+
catcher->buffer.clear();
14493

145-
// Internal state
146-
struct StreamPair
147-
{
148-
PyObject* saved = nullptr;
149-
PyObject* current = nullptr;
150-
};
151-
static std::map<std::string, StreamPair> streams;
94+
Py_RETURN_NONE;
95+
}
15296

153-
PyMODINIT_FUNC PyInit_emb(void)
97+
static PyMethodDef StreamCatcher_methods[]
98+
= {{"write", (PyCFunction) StreamCatcher_write, METH_VARARGS, "Write text"},
99+
{nullptr, nullptr, 0, nullptr}};
100+
101+
static PyType_Slot StreamCatcher_slots[]
102+
= {{Py_tp_methods, StreamCatcher_methods},
103+
{Py_tp_doc, (void*) "C++ StreamCatcher for stdout/stderr"},
104+
{0, nullptr}};
105+
106+
static PyType_Spec StreamCatcher_spec = {"embed.StreamCatcher",
107+
sizeof(StreamCatcher),
108+
0,
109+
Py_TPFLAGS_DEFAULT,
110+
StreamCatcher_slots};
111+
112+
// Keep global references so they can be restored
113+
static PyObject* original_stdout = nullptr;
114+
static PyObject* original_stderr = nullptr;
115+
static StreamCatcher* stdout_catcher = nullptr;
116+
static StreamCatcher* stderr_catcher = nullptr;
117+
118+
static StreamCatcher* create_catcher(const bool is_stderr,
119+
stdout_write_type write)
154120
{
155-
StdoutType.tp_new = PyType_GenericNew;
156-
if (PyType_Ready(&StdoutType) < 0) {
121+
PyObject* type = PyType_FromSpec(&StreamCatcher_spec);
122+
if (!type) {
157123
return nullptr;
158124
}
159125

160-
PyObject* m = PyModule_Create(&embmodule);
161-
if (m) {
162-
Py_INCREF(&StdoutType);
163-
PyModule_AddObject(m, "Stdout", reinterpret_cast<PyObject*>(&StdoutType));
126+
PyObject* obj = PyObject_CallObject(type, nullptr);
127+
Py_DECREF(type);
128+
if (!obj) {
129+
return nullptr;
164130
}
165-
return m;
131+
132+
auto* catcher = reinterpret_cast<StreamCatcher*>(obj);
133+
catcher->is_stderr = is_stderr;
134+
catcher->write = std::move(write);
135+
return catcher;
166136
}
167137

168-
static void set_stream(const std::string& stream, stdout_write_type write)
138+
// Redirect sys.stdout and sys.stderr
139+
bool redirect_python_output(stdout_write_type stdout_write,
140+
stdout_write_type stderr_write)
169141
{
170-
auto& stream_pair = streams[stream];
171-
if (stream_pair.current == nullptr) {
172-
stream_pair.saved = PySys_GetObject(stream.c_str());
173-
stream_pair.current = StdoutType.tp_new(&StdoutType, nullptr, nullptr);
142+
PyObject* sysmod = PyImport_ImportModule("sys");
143+
if (!sysmod) {
144+
return false;
145+
}
146+
147+
// Save original streams
148+
original_stdout = PyObject_GetAttrString(sysmod, "stdout");
149+
original_stderr = PyObject_GetAttrString(sysmod, "stderr");
150+
151+
// Create new catchers
152+
stdout_catcher = create_catcher(false, stdout_write);
153+
stderr_catcher = create_catcher(true, stderr_write);
154+
155+
if (!stdout_catcher || !stderr_catcher) {
156+
Py_DECREF(sysmod);
157+
PyErr_Print();
158+
return false;
174159
}
175160

176-
Stdout* impl = reinterpret_cast<Stdout*>(stream_pair.current);
177-
impl->write = std::move(write);
178-
PySys_SetObject(stream.c_str(), stream_pair.current);
161+
// Replace Python's sys.stdout/stderr
162+
PyObject_SetAttrString(
163+
sysmod, "stdout", reinterpret_cast<PyObject*>(stdout_catcher));
164+
PyObject_SetAttrString(
165+
sysmod, "stderr", reinterpret_cast<PyObject*>(stderr_catcher));
166+
167+
Py_DECREF(sysmod);
168+
return true;
179169
}
180170

181-
static void reset_streams()
171+
// Restore original sys.stdout/stderr
172+
void restore_python_output()
182173
{
183-
for (auto& [stream, stream_pair] : streams) {
184-
if (stream_pair.saved != nullptr) {
185-
PySys_SetObject(stream.c_str(), stream_pair.saved);
186-
}
187-
Py_XDECREF(stream_pair.current);
188-
stream_pair.current = nullptr;
174+
PyObject* sysmod = PyImport_ImportModule("sys");
175+
if (!sysmod) {
176+
return;
189177
}
190-
}
191178

192-
} // namespace emb
179+
if (original_stdout) {
180+
PyObject_SetAttrString(sysmod, "stdout", original_stdout);
181+
Py_DECREF(original_stdout);
182+
original_stdout = nullptr;
183+
}
184+
if (original_stderr) {
185+
PyObject_SetAttrString(sysmod, "stderr", original_stderr);
186+
Py_DECREF(original_stderr);
187+
original_stderr = nullptr;
188+
}
189+
190+
Py_XDECREF(stdout_catcher);
191+
Py_XDECREF(stderr_catcher);
192+
stdout_catcher = nullptr;
193+
stderr_catcher = nullptr;
194+
195+
Py_DECREF(sysmod);
196+
}
193197

194198
static PythonCmdInputWidget* widget = nullptr;
195199

@@ -220,7 +224,6 @@ PythonCmdInputWidget::PythonCmdInputWidget(QWidget* parent)
220224

221225
PythonCmdInputWidget::~PythonCmdInputWidget()
222226
{
223-
emb::reset_streams();
224227
widget = nullptr;
225228
}
226229

@@ -310,47 +313,30 @@ void PythonCmdInputWidget::exitHandler()
310313

311314
void PythonCmdInputWidget::init()
312315
{
313-
PyImport_AppendInittab("emb", emb::PyInit_emb);
314316
ord::pyAppInit(false);
315317

316-
PyImport_ImportModule("emb");
317-
318-
Py_AtExit(&exitHandler);
318+
auto* openroad = ord::OpenRoad::openRoad();
319+
if (openroad == nullptr) {
320+
return;
321+
}
322+
auto* logger = openroad->getLogger();
323+
if (logger == nullptr) {
324+
return;
325+
}
319326

320-
emb::stdout_write_type stdout_stream = [](std::string s) {
321-
auto* openroad = ord::OpenRoad::openRoad();
322-
if (openroad == nullptr) {
323-
return;
324-
}
325-
auto* logger = openroad->getLogger();
326-
if (logger == nullptr) {
327-
return;
328-
}
329-
s.erase(s.find_last_not_of("\r\n") + 1);
330-
s.erase(s.find_last_not_of('\n') + 1);
331-
if (s.empty()) {
332-
return;
333-
}
334-
logger->report("{}", s);
335-
};
336-
emb::set_stream("stdout", stdout_stream);
337-
emb::stdout_write_type stderr_stream = [this](std::string s) {
338-
auto* openroad = ord::OpenRoad::openRoad();
339-
if (openroad == nullptr) {
340-
return;
341-
}
342-
auto* logger = openroad->getLogger();
343-
if (logger == nullptr) {
344-
return;
345-
}
346-
s.erase(s.find_last_not_of("\r\n") + 1);
347-
s.erase(s.find_last_not_of('\n') + 1);
348-
if (s.empty()) {
349-
return;
350-
}
327+
stdout_write_type stdout_write
328+
= [logger](std::string s) { logger->report("{}", s); };
329+
stdout_write_type stderr_write = [this](std::string s) {
351330
emit addResultToOutput(QString::fromStdString(s), false);
352331
};
353-
emb::set_stream("stderr", stderr_stream);
332+
333+
if (!redirect_python_output(stdout_write, stderr_write)) {
334+
std::cerr << "Failed to redirect Python output" << std::endl;
335+
Py_Finalize();
336+
return;
337+
}
338+
339+
Py_AtExit(&exitHandler);
354340
}
355341

356342
} // namespace gui

src/gui/src/pythonCmdInputWidget.h

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,15 @@ class PythonCmdInputWidget : public CmdInputWidget
4646

4747
void init();
4848

49-
signals:
50-
void exiting();
51-
52-
void commandAboutToExecute();
53-
void commandFinishedExecuting(bool code);
54-
55-
void addResultToOutput(const QString& result, bool code);
56-
void addCommandToOutput(const QString& cmd);
57-
5849
public slots:
59-
virtual void executeCommand(const QString& cmd,
60-
bool echo = true,
61-
bool silent = false) override;
50+
void executeCommand(const QString& cmd,
51+
bool echo = true,
52+
bool silent = false) override;
6253

6354
protected:
6455
void keyPressEvent(QKeyEvent* e) override;
6556

66-
virtual bool isCommandComplete(const std::string& cmd) const override;
57+
bool isCommandComplete(const std::string& cmd) const override;
6758

6859
private:
6960
static void exitHandler();

0 commit comments

Comments
 (0)