pythonc++qtpyqt4python-sip

How can I make a python module of two or more C++/Qt classes using SIP?


I have two C++/Qt classes (A.h, A.cpp, B.h, B.cpp):

class A : public QObject
{
    // bla-bla
};
class B : public A
{
    // bla-bla
};

And I want to use in Python classes A and B something like this:

import mymodule

class MyB(mymodule.B):
    pass

a = mymodule.A()

I can make a module with one class and successfully use it in Python, but I don't understand what to do with 2 classes or more.

This is how my files for building module look for one class:

*.pro:

TEMPLATE = lib

CONFIG   += qt warn_on release

HEADERS  = A.h
SOURCES  = A.cpp
TARGET   = mymodule

DESTDIR  = /home/alex/tmp/lib

*.sip:

%Module A 0

%Import QtCore/QtCoremod.sip

class A : QObject
{
%TypeHeaderCode
#include "A.h"
%End

public:
  A();

// bla-bla
};

configure.py:

import os
import sipconfig
from PyQt4 import pyqtconfig

build_file = "A.sbf"

config = pyqtconfig.Configuration()

qt_sip_flags = config.pyqt_sip_flags

os.system(" ".join([config.sip_bin, "-c", ".", "-b", build_file, "-I", 
config.pyqt_sip_dir, qt_sip_flags, "A.sip"]))

installs = []
installs.append(["A.sip", os.path.join(config.default_sip_dir, "A")])

makefile = pyqtconfig.QtCoreModuleMakefile(
                      configuration=config,
                      build_file=build_file,
                      installs=installs)

makefile.LFLAGS.append("-L/home/alex/tmp/lib")
makefile.extra_libs = ["A"]

makefile.generate()

After running Makefile I have "A" module that I can use in Python. How to make 2 or more classes in one python module?


Solution

  • It is better to structure a project when a module is developed, in this case I will use the following:

    ├── configure.py
    ├── examples
    │   └── main.py
    ├── sip
    │   ├── base.sip
    │   ├── derived.sip
    │   └── pyfoomodule.sip
    └── src
        ├── base.cpp
        ├── base.h
        ├── derived.cpp
        ├── derived.h
        ├── foomodule_global.h
        └── FooModule.pro
    

    base.sip and derived.sip does not create a module, it only defines the class:

    base.sip

    %Import QtCore/QtCoremod.sip
    class Base: public QObject
    {
    %TypeHeaderCode
    #include "base.h"
    %End
    public:
        Base(QObject *parent=nullptr);
        virtual QString doStuff();
    };
    

    derived.sip

    %Import QtCore/QtCoremod.sip
    class Derived: public Base
    {
    %TypeHeaderCode
    #include "derived.h"
    %End
    public:
        Derived(QObject *parent=nullptr);
        QString doStuff();
    };
    

    and in pyfoomodule.sip the project is created including the other .sip

    pyfoomodule.sip

    %Module(name=PyFooModule, call_super_init=True, keyword_arguments="Optional")
    %DefaultMetatype PyQt4.QtCore.pyqtWrapperType
    %DefaultSupertype sip.simplewrapper
    %Include base.sip
    %Include derived.sip
    

    I have also created a script that is responsible for compiling the project.

    configure.py

    from PyQt4.QtCore import PYQT_CONFIGURATION as pyqt_config
    from distutils import sysconfig
    import os, sipconfig, sys
    
    
    class HostPythonConfiguration(object):
        def __init__(self):
            self.platform=sys.platform
            self.version=sys.hexversion>>8
    
            self.inc_dir=sysconfig.get_python_inc()
            self.venv_inc_dir=sysconfig.get_python_inc(prefix=sys.prefix)
            self.module_dir=sysconfig.get_python_lib(plat_specific=1)
    
            if sys.platform=='win32':
                self.data_dir=sys.prefix
                self.lib_dir=sys.prefix+'\\libs'
            else:
                self.data_dir=sys.prefix+'/share'
                self.lib_dir=sys.prefix+'/lib'
    
    class TargetQtConfiguration(object):
        def __init__(self, qmake):
            pipe=os.popen(' '.join([qmake, '-query']))
    
            for l in pipe:
                l=l.strip()
    
                tokens=l.split(':', 1)
                if isinstance(tokens, list):
                    if len(tokens) != 2:
                        error("Unexpected output from qmake: '%s'\n" % l)
    
                    name,value=tokens
                else:
                    name=tokens
                    value=None
    
                name=name.replace('/', '_')
                setattr(self, name, value)
    
            pipe.close()        
    
    if __name__=="__main__":
        from argparse import ArgumentParser
    
        parser=ArgumentParser(description="Configure PyAnalogClock module.")
        parser.add_argument(
            '-q', '--qmake',
            dest="qmake",
            type=str,
            default="qmake-qt4",
            help="Path to qmake executable"
        )
        parser.add_argument(
            '-s', '--sip-extras',
            dest="sip_extras",
            type=str,
            default="",
            help="Extra arguments to sip"
        )
        args=parser.parse_args()
    
        qmake_exe=args.qmake
        if not qmake_exe.endswith('qmake-qt4'):
            qmake_exe=os.path.join(qmake_exe,'qmake')
    
        if os.system(' '.join([qmake_exe, '-v']))!=0:
    
            if sys.platform=='win32':
                print("Make sure you have a working Qt qmake on your PATH.")
            else:
                print(
                    "Use the --qmake argument to explicitly specify a "
                    "working Qt qmake."
                )
            exit(1)
    
        sip_args=args.sip_extras
    
        pyconfig=HostPythonConfiguration()
        py_sip_dir=os.path.join(pyconfig.data_dir, 'sip', 'PyQt4')
        sip_inc_dir=pyconfig.venv_inc_dir
    
        qtconfig=TargetQtConfiguration(qmake_exe)
    
        inc_dir=os.path.abspath(os.path.join(".","src"))
        lib_dir=inc_dir
    
        sip_files_dir=os.path.abspath(os.path.join(".","sip"))
        output_dir =os.path.abspath(os.path.join(".", "modules"))
        build_file="pyfoomodule.sbf"
        build_path = os.path.join(output_dir, build_file)
    
        if not os.path.exists(output_dir): os.mkdir(output_dir)
        sip_file = os.path.join(sip_files_dir, "pyfoomodule.sip")
    
        config=sipconfig.Configuration()    
    
        cmd=" ".join([
            config.sip_bin,
            pyqt_config['sip_flags'],
            sip_args,
            '-I', sip_files_dir,
            '-I', py_sip_dir,
            '-I', config.sip_inc_dir,
            '-I', inc_dir,
            "-c", output_dir,
            "-b", build_path,
            "-w",
            "-o",
            sip_file,
        ])
    
        print(cmd)
        if os.system(cmd)!=0: sys.exit(1)
    
        installs = []
        installs.append([os.path.join(sip_files_dir, "pyfoomodule.sip"), 
            os.path.join(config.default_sip_dir, "PyFooModule")])
    
        makefile=sipconfig.SIPModuleMakefile(
            config,
            build_file,
            dir=output_dir,
            installs=installs
        )
    
        makefile.extra_defines+=['MYMODULE_LIBRARY','QT_CORE_LIB', 'QT_GUI_LIB']
        makefile.extra_include_dirs+=[os.path.abspath(inc_dir), qtconfig.QT_INSTALL_HEADERS]
        makefile.extra_lib_dirs+=[qtconfig.QT_INSTALL_LIBS, os.path.join('..','src')]
        makefile.extra_libs+=['FooModule']
    
        if sys.platform=='darwin':
            makefile.extra_cxxflags+=['-F'+qtconfig.QT_INSTALL_LIBS]        
            makefile.extra_include_dirs+=[
                os.path.join(qtconfig.QT_INSTALL_LIBS,'QtCore.framework','Headers'),
                os.path.join(qtconfig.QT_INSTALL_LIBS,'QtGui.framework','Headers')
            ]
    
            makefile.extra_lflags+=[      
                '-F'+qtconfig.QT_INSTALL_LIBS,
                "-framework QtGui",
                "-framework QtCore",
                "-framework DiskArbitration",
                "-framework IOKit",
                "-framework OpenGL",
                "-framework AGL",
            ]
    
        else:
            makefile.extra_include_dirs+=[
                os.path.join(qtconfig.QT_INSTALL_HEADERS, "QtCore"),
                os.path.join(qtconfig.QT_INSTALL_HEADERS, "QtGui"),
            ]
            makefile.extra_lib_dirs+=[os.path.join('..','src','release')]
            # makefile.extra_libs+=['Qt4Core','Qt4Gui']
    
        makefile.generate()
    
        sipconfig.ParentMakefile(
            configuration = config,
            subdirs = ["src", output_dir],
        ).generate()
    
        os.chdir("src")    
        qmake_cmd=qmake_exe
        if sys.platform=="win32": qmake_cmd+=" -spec win32-msvc2010"
        print()
        print(qmake_cmd)
        os.system(qmake_cmd)
        sys.exit()
    

    Then execute the following commands:

    python configure.py --qmake /path/of/qmake
    make 
    sudo make install
    

    And finally an example of how to use it:

    from PyQt4 import QtCore
    from PyFooModule import Base, Derived
    
    class PyDerived(Base):
        def doStuff(self):
            return "PyDerived"
    
    if __name__ == '__main__':
        print("==============================")
        b = Base()
        pd = PyDerived()
        d = Derived()
        print("b.doStuff(): ", b.doStuff())
        print("pd.doStuff(): ", pd.doStuff())
        print("d.doStuff(): ", d.doStuff())
        print("==============================")
        print("Base is subclass of QObject: ", issubclass(Base, QtCore.QObject))
        print("PyDerived is subclass of Base: ", issubclass(PyDerived, Base))
        print("Derived is subclass of Base: ", issubclass(Derived, Base))
        print("==============================")
    

    Output:

    ==============================
    b.doStuff():  Base
    pd.doStuff():  PyDerived
    d.doStuff():  Derived
    ==============================
    Base is subclass of QObject:  True
    PyDerived is subclass of Base:  True
    Derived is subclass of Base:  True
    ==============================
    

    The complete example can be found here.