Creating the Python wrapper for a new class
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Be nice to others! Respect the FreeCAD code of conduct!
Creating the Python wrapper for a new class
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?
Re: Creating the Python wrapper for a new class
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
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
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
Re: Creating the Python wrapper for a new class
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?
Re: Creating the Python wrapper for a new class
Yes you're right.
Re: Creating the Python wrapper for a new class
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:
What do I need to do to get a Py::Object from a Metadata object? (Metadata is the class I'm wrapping here).
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);
}
}
}
Re: Creating the Python wrapper for a new class
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.
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.
Re: Creating the Python wrapper for a new class
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...)
Re: Creating the Python wrapper for a new class
// 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));
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"
Re: Creating the Python wrapper for a new class
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
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;