diff --git a/etg/_core.py b/etg/_core.py index 1eedf726..da56b819 100644 --- a/etg/_core.py +++ b/etg/_core.py @@ -47,6 +47,8 @@ INCLUDES = [ 'defs', 'gdicmn', 'geometry', + 'stream', + 'image', 'gdiobj', 'bitmap', diff --git a/etg/defs.py b/etg/defs.py index 8a904bbe..3e0524f7 100644 --- a/etg/defs.py +++ b/etg/defs.py @@ -54,6 +54,9 @@ def run(): module.insertItemAfter(td, etgtools.TypedefDef(type='unsigned int', name='size_t')) module.insertItemAfter(td, etgtools.TypedefDef(type='long', name='time_t')) module.insertItemAfter(td, etgtools.TypedefDef(type='int', name='wxPrintQuality')) + module.insertItemAfter(td, etgtools.TypedefDef(type='long long', name='wxFileOffset')) + module.insertItemAfter(td, etgtools.TypedefDef(type='SIP_SSIZE_T', name='ssize_t')) + # Forward declarations for classes that are referenced but not defined @@ -69,8 +72,6 @@ def run(): class wxImageHandler; class wxToolBar; class wxExecuteEnv; - class wxInputStream; - class wxOutputStream; """)) diff --git a/etg/stream.py b/etg/stream.py new file mode 100644 index 00000000..87159971 --- /dev/null +++ b/etg/stream.py @@ -0,0 +1,297 @@ +#--------------------------------------------------------------------------- +# Name: etg/stream.py +# Author: Robin Dunn +# +# Created: 18-Nov-2011 +# Copyright: (c) 2011 by Total Control Software +# License: wxWindows License +#--------------------------------------------------------------------------- + +import etgtools +import etgtools.tweaker_tools as tools + +PACKAGE = "wx" +MODULE = "_core" +NAME = "stream" # Base name of the file to generate to for this script +DOCSTRING = "" + +# The classes and/or the basename of the Doxygen XML files to be processed by +# this script. +ITEMS = [ 'wxStreamBase', + 'wxInputStream', + 'wxOutputStream', + ] + + +OTHERDEPS = [ 'src/stream_input.cpp', + 'src/stream_output.cpp', + ] + +#--------------------------------------------------------------------------- + +def run(): + # Parse the XML file(s) building a collection of Extractor objects + module = etgtools.ModuleDef(PACKAGE, MODULE, NAME, DOCSTRING) + etgtools.parseDoxyXML(module, ITEMS) + + #----------------------------------------------------------------- + # Tweak the parsed meta objects in the module object as needed for + # customizing the generated code and docstrings. + + # These enums are declared in files that we will not be using because + # wxPython does not need the classes that are in those files. So just + # inject the enums here instead. + module.insertItem(0, etgtools.WigCode("""\ + enum wxStreamError + { + wxSTREAM_NO_ERROR, + wxSTREAM_EOF, + wxSTREAM_WRITE_ERROR, + wxSTREAM_READ_ERROR + }; + + enum wxSeekMode + { + wxFromStart, + wxFromCurrent, + wxFromEnd + }; + """)) + + + #----------------------------------------------------------------- + c = module.find('wxStreamBase') + assert isinstance(c, etgtools.ClassDef) + c.abstract = True + tools.removeVirtuals(c) + c.find('operator!').ignore() + + + #----------------------------------------------------------------- + c = module.find('wxInputStream') + c.abstract = True + tools.removeVirtuals(c) + + # Include a C++ class that can wrap a Python file-like object so it can + # be used as a wxInputStream + c.includeCppCode('src/stream_input.cpp') + + # Use that class for the convert code + c.convertFromPyObject = """\ + // is it just a typecheck? + if (!sipIsErr) { + if (wxPyInputStream::Check(sipPy)) + return 1; + return 0; + } + // otherwise do the conversion + *sipCppPtr = new wxPyInputStream(sipPy); + return sipGetState(sipTransferObj); + """ + + # Add Python file-like methods so a wx.InputStream can be used as if it + # was any other Python file object. + c.addCppMethod('void', 'seek', '(wxFileOffset offset, int whence=0)', """\ + self->SeekI(offset, (wxSeekMode)whence); + """) + c.addCppMethod('wxFileOffset', 'tell', '()', """\ + return self->TellI(); + """); + c.addCppMethod('void', 'close', '()', """\ + // ignored for now + """) + c.addCppMethod('void', 'flush', '()', """\ + // ignored for now + """) + c.addCppMethod('bool', 'eof', '()', """\ + return self->Eof(); + """) + + c.addCppCode("""\ + // helper used by the read and readline methods to make a PyObject + static PyObject* _makeReadBufObj(wxInputStream* self, wxMemoryBuffer& buf) { + PyObject* obj = NULL; + + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + wxStreamError err = self->GetLastError(); // error check + if (err != wxSTREAM_NO_ERROR && err != wxSTREAM_EOF) { + PyErr_SetString(PyExc_IOError,"IOError in wxInputStream"); + } + else { + // Return the data as a string object. TODO: Py3 + obj = PyString_FromStringAndSize(buf, buf.GetDataLen()); + } + wxPyEndBlockThreads(blocked); + return obj; + } + """) + + + c.addCppMethod('PyObject*', 'read', '()', """\ + wxMemoryBuffer buf; + const size_t BUFSIZE = 1024; + + // read while bytes are available on the stream + while ( self->CanRead() ) { + self->Read(buf.GetAppendBuf(BUFSIZE), BUFSIZE); + buf.UngetAppendBuf(self->LastRead()); + } + return _makeReadBufObj(self, buf); + """) + + c.addCppMethod('PyObject*', 'read', '(size_t size)', """\ + wxMemoryBuffer buf; + + // Read only size number of characters + self->Read(buf.GetWriteBuf(size), size); + buf.UngetWriteBuf(self->LastRead()); + return _makeReadBufObj(self, buf); + """) + + c.addCppMethod('PyObject*', 'readline', '()', """\ + wxMemoryBuffer buf; + char ch = 0; + + // read until \\n + while ((ch != '\\n') && (self->CanRead())) { + ch = self->GetC(); + buf.AppendByte(ch); + } + return _makeReadBufObj(self, buf); + """) + + c.addCppMethod('PyObject*', 'readline', '(size_t size)', """\ + wxMemoryBuffer buf; + int i; + char ch; + + // read until \\n or byte limit reached + for (i=ch=0; (ch != '\\n') && (self->CanRead()) && (i < size); i++) { + ch = self->GetC(); + buf.AppendByte(ch); + } + return _makeReadBufObj(self, buf); + """) + + + c.addCppCode("""\ + PyObject* _wxInputStream_readline(wxInputStream* self); + + // This does the real work of the readlines methods + static PyObject* _readlinesHelper(wxInputStream* self, + bool useSizeHint=false, size_t sizehint=0) { + PyObject* pylist; + + // init list + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + pylist = PyList_New(0); + wxPyEndBlockThreads(blocked); + + if (!pylist) { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyErr_NoMemory(); + wxPyEndBlockThreads(blocked); + return NULL; + } + + // read sizehint bytes or until EOF + size_t i; + for (i=0; (self->CanRead()) && (useSizeHint || (i < sizehint));) { + PyObject* s = _wxInputStream_readline(self); + if (s == NULL) { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + Py_DECREF(pylist); + wxPyEndBlockThreads(blocked); + return NULL; + } + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyList_Append(pylist, s); + i += PyString_Size(s); + wxPyEndBlockThreads(blocked); + } + + // error check + wxStreamError err = self->GetLastError(); + if (err != wxSTREAM_NO_ERROR && err != wxSTREAM_EOF) { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + Py_DECREF(pylist); + PyErr_SetString(PyExc_IOError,"IOError in wxInputStream"); + wxPyEndBlockThreads(blocked); + return NULL; + } + return pylist; + } + """) + + c.addCppMethod('PyObject*', 'readlines', '()', """\ + return _readlinesHelper(self); + """) + c.addCppMethod('PyObject*', 'readlines', '(size_t sizehint)', """\ + return _readlinesHelper(self, true, sizehint); + """) + + + #----------------------------------------------------------------- + c = module.find('wxOutputStream') + c.abstract = True + tools.removeVirtuals(c) + + + # Include a C++ class that can wrap a Python file-like object so it can + # be used as a wxOutputStream + c.includeCppCode('src/stream_output.cpp') + + # Use that class for the convert code + c.convertFromPyObject = """\ + // is it just a typecheck? + if (!sipIsErr) { + if (wxPyOutputStream::Check(sipPy)) + return 1; + return 0; + } + // otherwise do the conversion + *sipCppPtr = new wxPyOutputStream(sipPy); + return sipGetState(sipTransferObj); + """ + + + # Add Python file-like methods so a wx.OutputStream can be used as if it + # was any other Python file object. + c.addCppMethod('void', 'seek', '(wxFileOffset offset, int whence=0)', """\ + self->SeekO(offset, (wxSeekMode)whence); + """) + c.addCppMethod('wxFileOffset', 'tell', '()', """\ + return self->TellO(); + """); + c.addCppMethod('void', 'close', '()', """\ + self->Close(); + """) + c.addCppMethod('void', 'flush', '()', """\ + self->Sync(); + """) + c.addCppMethod('bool', 'eof', '()', """\ + return false; //self->Eof(); + """) + + c.addCppMethod('void', 'write', '(PyObject* data)', """\ + // We use only strings for the streams, not unicode + PyObject* str = PyObject_Str(data); + if (! str) { + PyErr_SetString(PyExc_TypeError, "Unable to convert to string"); + return; + } + self->Write(PyString_AS_STRING(str), PyString_GET_SIZE(str)); + Py_DECREF(str); + """) + + # TODO: Add a writelines(sequence) method + + #----------------------------------------------------------------- + tools.doCommonTweaks(module) + tools.runGenerators(module) + + +#--------------------------------------------------------------------------- +if __name__ == '__main__': + run() + diff --git a/src/stream_input.cpp b/src/stream_input.cpp new file mode 100644 index 00000000..ed26c391 --- /dev/null +++ b/src/stream_input.cpp @@ -0,0 +1,169 @@ +//-------------------------------------------------------------------------- + +static PyObject* wxPyGetMethod(PyObject* py, char* name) +{ + if (!PyObject_HasAttrString(py, name)) + return NULL; + PyObject* o = PyObject_GetAttrString(py, name); + if (!PyMethod_Check(o) && !PyCFunction_Check(o)) { + Py_DECREF(o); + return NULL; + } + return o; +} + +#define wxPyBlock_t_default PyGILState_UNLOCKED + + +// This class can wrap a Python file-like object and allow it to be used +// as a wxInputStream. +class wxPyInputStream : public wxInputStream +{ +public: + + // Make sure there is at least a read method + static bool Check(PyObject* fileObj) + { + PyObject* method = wxPyGetMethod(fileObj, "read"); + bool rval = method != NULL; + Py_XDECREF(method); + return rval; + } + + wxPyInputStream(PyObject* fileObj, bool block=true) + { + m_block = block; + wxPyBlock_t blocked = wxPyBlock_t_default; + if (block) blocked = wxPyBeginBlockThreads(); + + m_read = wxPyGetMethod(fileObj, "read"); + m_seek = wxPyGetMethod(fileObj, "seek"); + m_tell = wxPyGetMethod(fileObj, "tell"); + + if (block) wxPyEndBlockThreads(blocked); + } + + virtual ~wxPyInputStream() + { + wxPyBlock_t blocked = wxPyBlock_t_default; + if (m_block) blocked = wxPyBeginBlockThreads(); + Py_XDECREF(m_read); + Py_XDECREF(m_seek); + Py_XDECREF(m_tell); + if (m_block) wxPyEndBlockThreads(blocked); + } + + wxPyInputStream(const wxPyInputStream& other) + { + m_read = other.m_read; + m_seek = other.m_seek; + m_tell = other.m_tell; + m_block = other.m_block; + Py_INCREF(m_read); + Py_INCREF(m_seek); + Py_INCREF(m_tell); + } + +protected: + + // implement base class virtuals + + wxFileOffset GetLength() const + { + wxPyInputStream* self = (wxPyInputStream*)this; // cast off const + if (m_seek && m_tell) { + wxFileOffset temp = self->OnSysTell(); + wxFileOffset ret = self->OnSysSeek(0, wxFromEnd); + self->OnSysSeek(temp, wxFromStart); + return ret; + } + else + return wxInvalidOffset; + } + + size_t OnSysRead(void *buffer, size_t bufsize) + { + if (bufsize == 0) + return 0; + + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = Py_BuildValue("(i)", bufsize); + PyObject* result = PyEval_CallObject(m_read, arglist); + Py_DECREF(arglist); + + size_t o = 0; + if ((result != NULL) && PyString_Check(result)) { + o = PyString_Size(result); + if (o == 0) + m_lasterror = wxSTREAM_EOF; + if (o > bufsize) + o = bufsize; + memcpy((char*)buffer, PyString_AsString(result), o); // strings only, not unicode... + Py_DECREF(result); + + } + else + m_lasterror = wxSTREAM_READ_ERROR; + wxPyEndBlockThreads(blocked); + return o; + } + + size_t OnSysWrite(const void *buffer, size_t bufsize) + { + m_lasterror = wxSTREAM_WRITE_ERROR; + return 0; + } + + wxFileOffset OnSysSeek(wxFileOffset off, wxSeekMode mode) + { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = PyTuple_New(2); + + if (sizeof(wxFileOffset) > sizeof(long)) + // wxFileOffset is a 64-bit value... + PyTuple_SET_ITEM(arglist, 0, PyLong_FromLongLong(off)); + else + PyTuple_SET_ITEM(arglist, 0, PyInt_FromLong(off)); + + PyTuple_SET_ITEM(arglist, 1, PyInt_FromLong(mode)); + + + PyObject* result = PyEval_CallObject(m_seek, arglist); + Py_DECREF(arglist); + Py_XDECREF(result); + wxPyEndBlockThreads(blocked); + return OnSysTell(); + } + + wxFileOffset OnSysTell() const + { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = Py_BuildValue("()"); + PyObject* result = PyEval_CallObject(m_tell, arglist); + Py_DECREF(arglist); + wxFileOffset o = 0; + if (result != NULL) { + if (PyLong_Check(result)) + o = PyLong_AsLongLong(result); + else + o = PyInt_AsLong(result); + Py_DECREF(result); + }; + wxPyEndBlockThreads(blocked); + return o; + } + + bool IsSeekable() const + { + return (m_seek != NULL); + } + + +private: + PyObject* m_read; + PyObject* m_seek; + PyObject* m_tell; + bool m_block; +}; + +//-------------------------------------------------------------------------- diff --git a/src/stream_output.cpp b/src/stream_output.cpp new file mode 100644 index 00000000..9bc0f807 --- /dev/null +++ b/src/stream_output.cpp @@ -0,0 +1,161 @@ +//-------------------------------------------------------------------------- + +static PyObject* wxPyGetMethod(PyObject* py, char* name) +{ + if (!PyObject_HasAttrString(py, name)) + return NULL; + PyObject* o = PyObject_GetAttrString(py, name); + if (!PyMethod_Check(o) && !PyCFunction_Check(o)) { + Py_DECREF(o); + return NULL; + } + return o; +} + +#define wxPyBlock_t_default PyGILState_UNLOCKED + + +// This class can wrap a Python file-like object and allow it to be used +// as a wxInputStream. +class wxPyOutputStream : public wxOutputStream +{ +public: + + // Make sure there is at least a write method + static bool Check(PyObject* fileObj) + { + PyObject* method = wxPyGetMethod(fileObj, "write"); + bool rval = method != NULL; + Py_XDECREF(method); + return rval; + } + + wxPyOutputStream(PyObject* fileObj, bool block=true) + { + m_block = block; + wxPyBlock_t blocked = wxPyBlock_t_default; + if (block) blocked = wxPyBeginBlockThreads(); + + m_write = wxPyGetMethod(fileObj, "write"); + m_seek = wxPyGetMethod(fileObj, "seek"); + m_tell = wxPyGetMethod(fileObj, "tell"); + + if (block) wxPyEndBlockThreads(blocked); + } + + virtual ~wxPyOutputStream() + { + wxPyBlock_t blocked = wxPyBlock_t_default; + if (m_block) blocked = wxPyBeginBlockThreads(); + Py_XDECREF(m_write); + Py_XDECREF(m_seek); + Py_XDECREF(m_tell); + if (m_block) wxPyEndBlockThreads(blocked); + } + + wxPyOutputStream(const wxPyOutputStream& other) + { + m_write = other.m_write; + m_seek = other.m_seek; + m_tell = other.m_tell; + m_block = other.m_block; + Py_INCREF(m_write); + Py_INCREF(m_seek); + Py_INCREF(m_tell); + } + +protected: + + // implement base class virtuals + + wxFileOffset GetLength() const + { + wxPyOutputStream* self = (wxPyOutputStream*)this; // cast off const + if (m_seek && m_tell) { + wxFileOffset temp = self->OnSysTell(); + wxFileOffset ret = self->OnSysSeek(0, wxFromEnd); + self->OnSysSeek(temp, wxFromStart); + return ret; + } + else + return wxInvalidOffset; + } + + size_t OnSysRead(void *buffer, size_t bufsize) + { + m_lasterror = wxSTREAM_READ_ERROR; + return 0; + } + + size_t OnSysWrite(const void *buffer, size_t bufsize) + { + if (bufsize == 0) + return 0; + + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = PyTuple_New(1); + PyTuple_SET_ITEM(arglist, 0, PyString_FromStringAndSize((char*)buffer, bufsize)); + + PyObject* result = PyEval_CallObject(m_write, arglist); + Py_DECREF(arglist); + + if (result != NULL) + Py_DECREF(result); + else + m_lasterror = wxSTREAM_WRITE_ERROR; + wxPyEndBlockThreads(blocked); + return bufsize; + } + + wxFileOffset OnSysSeek(wxFileOffset off, wxSeekMode mode) + { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = PyTuple_New(2); + + if (sizeof(wxFileOffset) > sizeof(long)) + // wxFileOffset is a 64-bit value... + PyTuple_SET_ITEM(arglist, 0, PyLong_FromLongLong(off)); + else + PyTuple_SET_ITEM(arglist, 0, PyInt_FromLong(off)); + + PyTuple_SET_ITEM(arglist, 1, PyInt_FromLong(mode)); + + + PyObject* result = PyEval_CallObject(m_seek, arglist); + Py_DECREF(arglist); + Py_XDECREF(result); + wxPyEndBlockThreads(blocked); + return OnSysTell(); + } + + wxFileOffset OnSysTell() const + { + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + PyObject* arglist = Py_BuildValue("()"); + PyObject* result = PyEval_CallObject(m_tell, arglist); + Py_DECREF(arglist); + wxFileOffset o = 0; + if (result != NULL) { + if (PyLong_Check(result)) + o = PyLong_AsLongLong(result); + else + o = PyInt_AsLong(result); + Py_DECREF(result); + }; + wxPyEndBlockThreads(blocked); + return o; + } + + bool IsSeekable() const + { + return (m_seek != NULL); + } + +private: + PyObject* m_write; + PyObject* m_seek; + PyObject* m_tell; + bool m_block; +}; + +//-------------------------------------------------------------------------- diff --git a/unittests/test_stream.py b/unittests/test_stream.py new file mode 100644 index 00000000..32a7dfec --- /dev/null +++ b/unittests/test_stream.py @@ -0,0 +1,44 @@ +import imp_unittest, unittest +import wtc +import wx +import os +from cStringIO import StringIO + + +pngFile = os.path.join(os.path.dirname(__file__), 'toucan.png') + +#--------------------------------------------------------------------------- + +class stream_Tests(wtc.WidgetTestCase): + + def test_inputStreamParam(self): + # This tests being able to pass a Python file-like object to a + # wrapped function expecting a wxInputStream. + + # First, load the image data into a StringIO object + stream = StringIO(open(pngFile, 'rb').read()) + + # Then use it to create a wx.Image + img = wx.Image(stream) + self.assertTrue(img.IsOk()) + + def test_outputStreamParam(self): + # This tests being able to pass a Python file-like object to a + # wrapped function expecting a wxOytputStream. + + image = wx.Image(pngFile) + stream = StringIO() + image.SaveFile(stream, wx.BITMAP_TYPE_PNG) + del image + + stream = StringIO(stream.getvalue()) + image = wx.Image(stream) + self.assertTrue(image.IsOk()) + + + +#--------------------------------------------------------------------------- + + +if __name__ == '__main__': + unittest.main()