Skip to content
Mikaël Capelle edited this page May 21, 2020 · 2 revisions

FAQ

1. Why is MO2 throwing an exception when I try to create a type inheriting one of MO2 class?

Note: This should probably also be in the plugin creation tutorial.

This often happens if you forget to call super().__init__() with the right arguments. Even if the list of arguments is empty (as in the example), it must be called:

class MySaveGame(mobase.ISaveGame):
    def __init__(self):
		    super().__init__()  # Mandatory!

2. Why are my get_override calls failing with MissingImplementationException when the Python class has the right methods?

In order for get_override to work properly, a reference to the initial python object must exists at any time. This can be on the python side (e.g., by having an attribute), or on the C++ side. For easier plugin creation, I would recommend storing on the C++ side.

Some C++ examples are:

  • The PythonRunner implementation that holds boost::python::object for all the created plugins.
  • The IPluginGame that holds (on the python side), a dict object containing the game features.
  • The SaveGameInfoWrapper (game feature) that holds the widget (m_SaveGameWidget) but also all the saves that were created (m_SaveGames).

3. Why are QString and QVariant converters not registered using register_qclass_converter?

register_qclass_converter is used to register conversion for Qt class that have PyQt equivalent. Since PyQt uses standard Python str instead of QString, we need to use a custom converter.

While PyQt does have QVariant, it is not very convenient since Python developer would have to manually cast to QVariant, so instead we use a custom converter that can create a QVariant from a multitude of python types such as int, str, List[str], etc.

4. What is Q_DELEGATE? And how do I use it?

Q_DELEGATE is a macro that can be used within a bpy::class_ declaration, e.g.:

bpy::class_<IDownloadManager, boost::noncopyable>("IDownloadManager", bpy::no_init)
    .def("startDownloadURLs", &IDownloadManager::startDownloadURLs)
    .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile)
    .def("downloadPath", &IDownloadManager::downloadPath)

    Q_DELEGATE(IDownloadManager, QObject, "_object")
    ;

In this case, we indicate that we want to expose the QObject interface for IDownloadManager. The Q_DELEGATE macro will:

  • Create a __getattr__ method that is used by Python to delegate attribute lookup to QObject when the attribute is not found directly in the IDownloadManager python class.
  • Create a _object method to access the underlying QObject.

It makes it possible to do the following in python:

dm = ...  # Instance of IDownloadManager

# We can connect signals declared in the C++ class:
dm.downloadComplete.connect(lambda i: print("Download {} complete!", i))

wm = ...  # Instance of ISaveGameInfoWidget

# We can call QWidget method on a ISaveGameInfoWidget object:
wm.setLayout(QtWidgets.QHBoxLayout())

Most of the QObject interface has no reason to be exposed, so the only cases where you should need Q_DELEGATE would be when:

  • You need to expose Qt signals to python - this is the case for IDownloadManager and IModRepositoryBridge.
  • You need to expose a class that inherits QWidget. If you do not use Q_DELEGATE in this case, python developers will not be able to call the QWidget method on objects of this class.

5. Why is it not possible to do bpy::bases<QObject>?

I will explain the reason for this here, but you should see the FAQ item above for troubleshooting.

QObject (or any Qt class) is exposed in python using sip, while everything in MO2 is exposed using boost::python. When doing bpy::bases<QObject>, boost::python does not find the PyTypeObject that corresponds to QObject since it is not exposed through a bpy::class_ declaration (registering a converter for it is not enough). It is possible, by playing with internal boost::python stuff, to make boost::python find the PyTypeObject for QObject but... that is not sufficient.

boost::python and sip create classes using their own meta-classes. For boost::python, it is Boost.Python.class. And all classes created by boost::python inherits Boost.Python.instance which is the "top" boost class. Unfortunately, it is not possible to do inheritance between classes that have different meta-classes in python, so it is not possible to inherit both QObject and Boost.Python.instance. The only way would be to provide our own meta-class, but this is not possible with boost::python.

6. Why is the tr method not exposed to avoid having to declare it manually on the python side?

One issue with QObject.tr in PyQt5 is that the context is dynamic, i.e. if class B inherits class A, strings declared in class A will not be translated by an instance of B since the context is the dynamic object (not the static one like in C++), see, https://doc.bccnsoft.com/docs/PyQt5/i18n.html.

It would be quite easy to provide tr since all plugins inherit IPlugin:

.def("tr", +[](bpy::object obj, const char *str) {
    std::string className = bpy::extract<std::string>(
		    obj.attr("__class__").attr("__name__"));
    return QCoreApplication::translate(className.data(), str);
})

...but the issue is the same, since className will be the name of the actual class, not the class containing the strings. It could be possible to go up the class chain to find the first available translation, but I am not sure that it is worth the hassle.