Creating the Python wrapper for a new class

Here's the place for discussion related to coding in FreeCAD, C++ or Python. Design, interfaces and structures.
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
User avatar
chennes
Veteran
Posts: 3910
Joined: Fri Dec 23, 2016 3:38 pm
Location: Norman, OK, USA
Contact:

Creating the Python wrapper for a new class

Post by chennes »

As discussed in this topic, I'm working on a package metadata format. So I've got a new class, MetadataReader, that does the ingest and parse of that file and provides access to the data in it. It's written in C++. Now I'd like to wrap it in Python so it's accessible from the add-on manager. Can someone point me to documentation on how to do that?
Chris Hennes
Pioneer Library System
GitHub profile, LinkedIn profile, chrishennes.com
User avatar
Kunda1
Veteran
Posts: 13434
Joined: Thu Jan 05, 2017 9:03 pm

Re: Creating the Python wrapper for a new class

Post by Kunda1 »

Unfortunately I haven't come across any docs on how to do this. But if you search the commits in the repo you can see how it's been implemented many times before:
https://github.com/FreeCAD/FreeCAD/sear ... pe=commits
Alone you go faster. Together we go farther
Please mark thread [Solved]
Want to contribute back to FC? Checkout:
'good first issues' | Open TODOs and FIXMEs | How to Help FreeCAD | How to report Bugs
User avatar
chennes
Veteran
Posts: 3910
Joined: Fri Dec 23, 2016 3:38 pm
Location: Norman, OK, USA
Contact:

Re: Creating the Python wrapper for a new class

Post by chennes »

Thanks to both of you for the links: I think I'm on the right track now. This seems like something that would be useful to document on the Wiki, so I am going to start writing stuff down. First, though: am I correct in saying that this is a custom-build wrapping mechanism? I know we use PyCXX for some things, but the main wrapping, with these XML files that create *Py.h and *Py.cpp files, is a FreeCAD creation, right?
Chris Hennes
Pioneer Library System
GitHub profile, LinkedIn profile, chrishennes.com
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Creating the Python wrapper for a new class

Post by openBrain »

Yes you're right.
User avatar
chennes
Veteran
Posts: 3910
Joined: Fri Dec 23, 2016 3:38 pm
Location: Norman, OK, USA
Contact:

Re: Creating the Python wrapper for a new class

Post by chennes »

I almost have this wrapper working, but I'm missing a detail someplace, I think because I don't exactly understand what the class that is getting generated by the XML is. I have a complication in my C++ class: it's recursive. That is, a Metadata object can contain other Metadata objects. So for all of the simple data types, creating the MetadataPyImp.cpp functions is simple. But I can't see how to write the code that returns another Metadata object as a Py::Object.

Here's what I've got (omitting the details of how I got to the Metadata object:

Code: Select all

Py::Object MetadataPy::getContent(void) const
{
// Details omitted...
    {
        Py::List pyContentForKey;
        auto elements = content.equal_range(key);
        for (auto element = elements.first; element != elements.second; ++element) {
        
            // I thought what I need to do was construct another MetadataPy object...
            auto contentMetadataItem = MetadataPy (&(element->second));
            
            // But this call fails because MetadataPy is not a Py::Object
            pyContentForKey.append(contentMetadataItem);
        }
    }
}
What do I need to do to get a Py::Object from a Metadata object? (Metadata is the class I'm wrapping here).
Chris Hennes
Pioneer Library System
GitHub profile, LinkedIn profile, chrishennes.com
openBrain
Veteran
Posts: 9041
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: Creating the Python wrapper for a new class

Post by openBrain »

Does that help if you look at the compare of the simple case I implemented as a test ?
https://github.com/FreeCAD/FreeCAD/comp ... d3200ca67f

There isn't really something special except implementing "representation", "setCustomAttribute" & "getCustomAttribute" in the xxxxImp.cpp file.

Just notice I found the best method was to create the XML file then run kind of "dry-run" compilation that will fail but create the Imp.ccp file skeleton that you then just have to populate. ;)

EDIT : I also remember that I messes up the compilation after (lot of) failed attempt, and I had to make clean to have it to work.
User avatar
chennes
Veteran
Posts: 3910
Joined: Fri Dec 23, 2016 3:38 pm
Location: Norman, OK, USA
Contact:

Re: Creating the Python wrapper for a new class

Post by chennes »

I have the simple stuff working fine -- I based it on your comments and code in the discussion that you linked to above (so thanks for that!). But I presume that what this wrapper class is creating on the Python side is a Python object with the various attributes/functions that my C++ Metadata class has. Maybe looking at a copy constructor will help, though (I'm thinking aloud now...)
Chris Hennes
Pioneer Library System
GitHub profile, LinkedIn profile, chrishennes.com
wmayer
Founder
Posts: 20309
Joined: Thu Feb 19, 2009 10:32 am
Contact:

Re: Creating the Python wrapper for a new class

Post by wmayer »

// But this call fails because MetadataPy is not a Py::Object
pyContentForKey.append(contentMetadataItem);

Code: Select all

auto contentMetadataItem = new MetadataPy (new Metadata(element->second));
pyContentForKey.append(Py::asObject(contentMetadataItem));
NOTE: It's important that you create a copy of the Metadata object because it can happen that the original instance of Metadata will be destroyed while the Python wrapper still is alive. When passing a pointer to the original instance instead you may get a segmentation fault.
In order to make sure the copy of Metadata is deleted when the Python wrapper is getting destroyed you have to set the attribute

Code: Select all

Delete="true"
inside the PythonExport element in the XML file.
wmayer
Founder
Posts: 20309
Joined: Thu Feb 19, 2009 10:32 am
Contact:

Re: Creating the Python wrapper for a new class

Post by wmayer »

Btw, in C++ you use a multimap but in Python a simple list. If you also want a multimap in Python have a look at this:
https://stackoverflow.com/questions/173 ... on/1731989

This is the C++ code using the PyCXX API

Code: Select all

    PyObject* mod = PyImport_ImportModule("collections");
    if (!mod) {
        std::string error = "Cannot load collections module";
        throw Py::Exception(PyExc_ImportError, error);
    }

    Py::Module module(mod, true);
    Py::Callable call = module.getDict().getItem("defaultdict");

    Py::Tuple arg1(1), arg2(1);
    arg1.setItem(0, Py::List().type());
    Py::Object defaultdict(call.apply(arg1));

    arg1.setItem(0, Py::Int(1));
    arg2.setItem(0, Py::String("a"));
    defaultdict.callMemberFunction("__getitem__", arg1).callMemberFunction("append", arg2);
    arg2.setItem(0, Py::String("b"));
    defaultdict.callMemberFunction("__getitem__", arg1).callMemberFunction("append", arg2);
    arg1.setItem(0, Py::Int(2));
    arg2.setItem(0, Py::String("c"));
    defaultdict.callMemberFunction("__getitem__", arg1).callMemberFunction("append", arg2);

    return defaultdict;
Post Reply