Created F.A.Q. (markdown)

Mikaël Capelle
2020-05-21 16:08:59 +02:00
parent 49a68e1470
commit a73870b1cc
+119
@@ -0,0 +1,119 @@
## FAQ
- [Mod Organizer 2 - Plugin Python Proxy](#mod-organizer-2---plugin-python-proxy)
- [TOC](#toc)
- [Introduction](#introduction)
- [The `runner` project](#the-runner-project)
- [Forewords](#forewords)
- [Project organization](#project-organization)
- [`pythonrunner`](#pythonrunner)
- [Registering converters](#registering-converters)
- [Exposing classes](#exposing-classes)
- [Enumerations](#enumerations)
- [Classes](#classes)
- [`proxypluginwrappers`](#proxypluginwrappers)
- [`gamefeatureswrappers`](#gamefeatureswrappers)
- [`uibasewrappers`](#uibasewrappers)
- [The `proxy` project](#the-proxy-project)
- [FAQ](#faq)
- [1. Why is MO2 throwing an exception when I try to create a type inheriting one of MO2 class?](#1-why-is-mo2-throwing-an-exception-when-i-try-to-create-a-type-inheriting-one-of-mo2-class)
- [2. Why are my `get_override` calls failing with `MissingImplementationException` when the Python class has the right methods?](#2-why-are-my-getoverride-calls-failing-with-missingimplementationexception-when-the-python-class-has-the-right-methods)
- [3. Why are `QString` and `QVariant` converters not registered using `register_qclass_converter`?](#3-why-are-qstring-and-qvariant-converters-not-registered-using-registerqclassconverter)
- [4. What is `Q_DELEGATE`? And how do I use it?](#4-what-is-qdelegate-and-how-do-i-use-it)
- [5. Why is it not possible to do `bpy::bases<QObject>`?](#5-why-is-it-not-possible-to-do-bpybasesqobject)
- [6. Why is the `tr` method not exposed to avoid having to declare it manually on the python side?](#6-why-is-the-tr-method-not-exposed-to-avoid-having-to-declare-it-manually-on-the-python-side)
### 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:
```python
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.:
```cpp
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:
```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](https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python). 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`:
```cpp
.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.