Skip to content

Commit

Permalink
ENH: Add ScriptedFileReader for implementing file readers in Python
Browse files Browse the repository at this point in the history
Also fixed workarounds of adding dummy file writers to scripted modules to avoid logging error messages.
  • Loading branch information
lassoan committed Jun 22, 2020
1 parent 9e71ccf commit 21fa3d0
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 38 deletions.
3 changes: 3 additions & 0 deletions Base/QTCore/CMakeLists.txt
Expand Up @@ -119,6 +119,8 @@ endif()

if(Slicer_USE_PYTHONQT)
list(APPEND KIT_SRCS
qSlicerScriptedFileReader.cxx
qSlicerScriptedFileReader.h
qSlicerScriptedFileWriter.cxx
qSlicerScriptedFileWriter.h
qSlicerScriptedUtils.cxx
Expand Down Expand Up @@ -160,6 +162,7 @@ endif()

if(Slicer_USE_PYTHONQT)
list(APPEND KIT_MOC_SRCS
qSlicerScriptedFileReader.h
qSlicerScriptedFileWriter.h
)
endif()
Expand Down
260 changes: 260 additions & 0 deletions Base/QTCore/qSlicerScriptedFileReader.cxx
@@ -0,0 +1,260 @@
/*==============================================================================
Program: 3D Slicer
Copyright (c) Kitware Inc.
See COPYRIGHT.txt
or http://www.slicer.org/copyright/copyright.txt for details.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file was originally developed by Julien Finet, Kitware Inc.
and was partially funded by NIH grant 3P41RR013218-12S1
==============================================================================*/

// Qt includes
#include <QFileInfo>

// PythonQt includes
#include <PythonQt.h>
#include <PythonQtConversion.h>

// Slicer includes
#include "qSlicerScriptedFileReader.h"
#include "qSlicerScriptedUtils_p.h"

// VTK includes
#include <vtkObject.h>
#include <vtkPythonUtil.h>

//-----------------------------------------------------------------------------
class qSlicerScriptedFileReaderPrivate
{
public:
typedef qSlicerScriptedFileReaderPrivate Self;
qSlicerScriptedFileReaderPrivate();
virtual ~qSlicerScriptedFileReaderPrivate();

enum {
DescriptionMethod = 0,
FileTypeMethod,
ExtensionsMethod,
LoadMethod,
};

mutable qSlicerPythonCppAPI PythonCppAPI;

QString PythonSource;
QString PythonClassName;
};

//-----------------------------------------------------------------------------
// qSlicerScriptedFileReaderPrivate methods

//-----------------------------------------------------------------------------
qSlicerScriptedFileReaderPrivate::qSlicerScriptedFileReaderPrivate()
{
this->PythonCppAPI.declareMethod(Self::DescriptionMethod, "description");
this->PythonCppAPI.declareMethod(Self::FileTypeMethod, "fileType");
this->PythonCppAPI.declareMethod(Self::ExtensionsMethod, "extensions");
this->PythonCppAPI.declareMethod(Self::LoadMethod, "load");
}

//-----------------------------------------------------------------------------
qSlicerScriptedFileReaderPrivate::~qSlicerScriptedFileReaderPrivate() = default;

//-----------------------------------------------------------------------------
// qSlicerScriptedFileReader methods

//-----------------------------------------------------------------------------
qSlicerScriptedFileReader::qSlicerScriptedFileReader(QObject* parent)
: Superclass(parent)
, d_ptr(new qSlicerScriptedFileReaderPrivate)
{
}

//-----------------------------------------------------------------------------
qSlicerScriptedFileReader::~qSlicerScriptedFileReader() = default;

//-----------------------------------------------------------------------------
QString qSlicerScriptedFileReader::pythonSource()const
{
Q_D(const qSlicerScriptedFileReader);
return d->PythonSource;
}

//-----------------------------------------------------------------------------
bool qSlicerScriptedFileReader::setPythonSource(const QString& newPythonSource, const QString& _className, bool missingClassIsExpected)
{
Q_D(qSlicerScriptedFileReader);

if (!Py_IsInitialized())
{
return false;
}

if(!newPythonSource.endsWith(".py") && !newPythonSource.endsWith(".pyc"))
{
return false;
}

// Extract moduleName from the provided filename
QString moduleName = QFileInfo(newPythonSource).baseName();

QString className = _className;
if (className.isEmpty())
{
className = moduleName;
if (!moduleName.endsWith("FileReader"))
{
className.append("FileReader");
}
}
d->PythonClassName = className;

d->PythonCppAPI.setObjectName(className);

// Get a reference (or create if needed) the <moduleName> python module
PyObject * module = PyImport_AddModule(moduleName.toUtf8());

// Get a reference to the python module class to instantiate
PythonQtObjectPtr classToInstantiate;
if (PyObject_HasAttrString(module, className.toUtf8()))
{
classToInstantiate.setNewRef(PyObject_GetAttrString(module, className.toUtf8()));
}
else if (missingClassIsExpected)
{
// Class is not defined for this object, but this is expected, not an error
return false;
}
if (!classToInstantiate)
{
PythonQt::self()->handleError();
PyErr_SetString(PyExc_RuntimeError,
QString("qSlicerScriptedFileReader::setPythonSource - "
"Failed to load scripted file Reader: "
"class %1 was not found in file %2").arg(className).arg(newPythonSource).toUtf8());
return false;
}

PyObject* self = d->PythonCppAPI.instantiateClass(this, className, classToInstantiate);
if (!self)
{
return false;
}

d->PythonSource = newPythonSource;

return true;
}

//-----------------------------------------------------------------------------
PyObject* qSlicerScriptedFileReader::self() const
{
Q_D(const qSlicerScriptedFileReader);
return d->PythonCppAPI.pythonSelf();
}

//-----------------------------------------------------------------------------
QString qSlicerScriptedFileReader::description()const
{
Q_D(const qSlicerScriptedFileReader);

PyObject * result = d->PythonCppAPI.callMethod(d->DescriptionMethod);
if (!result)
{
return QString();
}
if (!PyString_Check(result))
{
qWarning() << d->PythonSource
<< " - In" << d->PythonClassName << "class, function 'description' "
<< "is expected to return a string !";
return QString();
}
QString fileType = QString(PyString_AsString(result));
return fileType;
}

//-----------------------------------------------------------------------------
qSlicerIO::IOFileType qSlicerScriptedFileReader::fileType()const
{
Q_D(const qSlicerScriptedFileReader);

PyObject * result = d->PythonCppAPI.callMethod(d->FileTypeMethod);
if (!result)
{
return IOFileType();
}
if (!PyString_Check(result))
{
qWarning() << d->PythonSource
<< " - In" << d->PythonClassName << "class, function 'fileType' "
<< "is expected to return a string !";
return IOFileType();
}
return IOFileType(PyString_AsString(result));
}

//-----------------------------------------------------------------------------
QStringList qSlicerScriptedFileReader::extensions()const
{
Q_D(const qSlicerScriptedFileReader);
PyObject * result = d->PythonCppAPI.callMethod(d->ExtensionsMethod);
if (!result)
{
return QStringList();
}
if (!PyList_Check(result))
{
qWarning() << d->PythonSource
<< " - In" << d->PythonClassName << "class, function 'extensions' "
<< "is expected to return a string list !";
return QStringList();
}
PyObject* resultAsTuple = PyList_AsTuple(result);
QStringList extensionList;
Py_ssize_t size = PyTuple_Size(resultAsTuple);
for (Py_ssize_t i = 0; i < size; ++i)
{
if (!PyString_Check(PyTuple_GetItem(resultAsTuple, i)))
{
qWarning() << d->PythonSource
<< " - In" << d->PythonClassName << "class, function 'extensions' "
<< "is expected to return a string list !";
break;
}
extensionList << PyString_AsString(PyTuple_GetItem(resultAsTuple, i));
}
Py_DECREF(resultAsTuple);
return extensionList;
}

//-----------------------------------------------------------------------------
bool qSlicerScriptedFileReader::load(const qSlicerIO::IOProperties& properties)
{
Q_D(qSlicerScriptedFileReader);
PyObject * arguments = PyTuple_New(1);
PyTuple_SET_ITEM(arguments, 0, PythonQtConv::QVariantMapToPyObject(properties));
PyObject * result = d->PythonCppAPI.callMethod(d->LoadMethod, arguments);
Py_DECREF(arguments);
if (!result)
{
return false;
}
if (!PyBool_Check(result))
{
qWarning() << d->PythonSource
<< " - In" << d->PythonClassName << "class, function 'write' "
<< "is expected to return a string boolean !";
return false;
}
return result == Py_True;
}
79 changes: 79 additions & 0 deletions Base/QTCore/qSlicerScriptedFileReader.h
@@ -0,0 +1,79 @@
/*==============================================================================
Program: 3D Slicer
Copyright (c) Kitware Inc.
See COPYRIGHT.txt
or http://www.slicer.org/copyright/copyright.txt for details.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
and was partially funded by NIH grant 3P41RR013218-12S1
==============================================================================*/

#ifndef __qSlicerScriptedFileReader_h
#define __qSlicerScriptedFileReader_h

// Slicer includes
#include "qSlicerFileReader.h"
#include "qSlicerBaseQTCoreExport.h"

// Forward Declare PyObject*
#ifndef PyObject_HEAD
struct _object;
typedef _object PyObject;
#endif
class qSlicerScriptedFileReaderPrivate;
class vtkObject;

class Q_SLICER_BASE_QTCORE_EXPORT qSlicerScriptedFileReader
: public qSlicerFileReader
{
Q_OBJECT

public:
typedef qSlicerFileReader Superclass;
qSlicerScriptedFileReader(QObject* parent = nullptr);
~qSlicerScriptedFileReader() override;

QString pythonSource()const;

/// \warning Setting the source is a no-op. See detailed comment in the source code.
/// If missingClassIsExpected is true (default) then missing class is expected and not treated as an error.
bool setPythonSource(const QString& newPythonSource, const QString& className = QLatin1String(""), bool missingClassIsExpected = true);

/// Convenience method allowing to retrieve the associated scripted instance
Q_INVOKABLE PyObject* self() const;

/// Reimplemented to propagate to python methods
/// \sa qSlicerIO::description()
QString description()const override;

/// Reimplemented to propagate to python methods
/// \sa qSlicerIO::fileType()
IOFileType fileType()const override;

/// Reimplemented to propagate to python methods
/// \sa qSlicerFileReader::extensions()
QStringList extensions()const override;

/// Reimplemented to propagate to python methods
/// \sa qSlicerFileReader::write()
bool load(const qSlicerIO::IOProperties& properties) override;

protected:
QScopedPointer<qSlicerScriptedFileReaderPrivate> d_ptr;

private:
Q_DECLARE_PRIVATE(qSlicerScriptedFileReader);
Q_DISABLE_COPY(qSlicerScriptedFileReader);
};

#endif
7 changes: 6 additions & 1 deletion Base/QTCore/qSlicerScriptedFileWriter.cxx
Expand Up @@ -92,7 +92,7 @@ QString qSlicerScriptedFileWriter::pythonSource()const
}

//-----------------------------------------------------------------------------
bool qSlicerScriptedFileWriter::setPythonSource(const QString& newPythonSource, const QString& _className)
bool qSlicerScriptedFileWriter::setPythonSource(const QString& newPythonSource, const QString& _className, bool missingClassIsExpected)
{
Q_D(qSlicerScriptedFileWriter);

Expand Down Expand Up @@ -131,6 +131,11 @@ bool qSlicerScriptedFileWriter::setPythonSource(const QString& newPythonSource,
{
classToInstantiate.setNewRef(PyObject_GetAttrString(module, className.toUtf8()));
}
else if (missingClassIsExpected)
{
// Class is not defined for this object, but this is expected, not an error
return false;
}
if (!classToInstantiate)
{
PythonQt::self()->handleError();
Expand Down
3 changes: 2 additions & 1 deletion Base/QTCore/qSlicerScriptedFileWriter.h
Expand Up @@ -45,7 +45,8 @@ class Q_SLICER_BASE_QTCORE_EXPORT qSlicerScriptedFileWriter
QString pythonSource()const;

/// \warning Setting the source is a no-op. See detailed comment in the source code.
bool setPythonSource(const QString& newPythonSource, const QString& className = QLatin1String(""));
/// If missingClassIsExpected is true (default) then missing class is expected and not treated as an error.
bool setPythonSource(const QString& newPythonSource, const QString& className = QLatin1String(""), bool missingClassIsExpected = true);

/// Convenience method allowing to retrieve the associated scripted instance
Q_INVOKABLE PyObject* self() const;
Expand Down

0 comments on commit 21fa3d0

Please sign in to comment.