diff --git a/src/CPyCppyyModule.cxx b/src/CPyCppyyModule.cxx index d21a85d..6c12c3a 100644 --- a/src/CPyCppyyModule.cxx +++ b/src/CPyCppyyModule.cxx @@ -14,6 +14,7 @@ #include "PyStrings.h" #include "TemplateProxy.h" #include "TupleOfInstances.h" +#include "TypeHints.h" #include "Utility.h" #define CPYCPPYY_INTERNAL 1 @@ -1060,6 +1061,8 @@ static PyMethodDef gCPyCppyyMethods[] = { METH_NOARGS, (char*) "Begin capturing stderr to a in memory buffer."}, {(char*) "_end_capture_stderr", (PyCFunction)EndCaptureStderr, METH_NOARGS, (char*) "End capturing stderr and returns the captured buffer."}, + {(char*) "generate_typehints", (PyCFunction)generate_typehints, + METH_VARARGS, (char*) "Generates typehits (.pyi file) for the given C/C++ type name."}, {nullptr, nullptr, 0, nullptr} }; diff --git a/src/Cppyy.h b/src/Cppyy.h index 2212ff3..40647fb 100644 --- a/src/Cppyy.h +++ b/src/Cppyy.h @@ -90,6 +90,8 @@ namespace Cppyy { CPPYY_IMPORT TCppScope_t GetTypeScope(TCppScope_t klass); CPPYY_IMPORT + std::string GetDoc(TCppScope_t scope); + CPPYY_IMPORT TCppScope_t GetNamed(const std::string& scope_name, TCppScope_t parent_scope = 0); CPPYY_IMPORT @@ -179,8 +181,16 @@ namespace Cppyy { CPPYY_IMPORT bool IsNamespace(TCppScope_t scope); CPPYY_IMPORT + bool IsPureNamespace(TCppScope_t scope); + CPPYY_IMPORT bool IsClass(TCppScope_t scope); CPPYY_IMPORT + bool IsFunction(TCppScope_t handle); + CPPYY_IMPORT + bool IsMethod(TCppScope_t handle); + CPPYY_IMPORT + bool IsTemplateClass(TCppScope_t handle); + CPPYY_IMPORT bool IsTemplate(TCppScope_t handle); CPPYY_IMPORT bool IsTemplateInstantiation(TCppScope_t handle); @@ -262,10 +272,14 @@ namespace Cppyy { CPPYY_IMPORT std::string GetMethodReturnTypeAsString(TCppMethod_t); CPPYY_IMPORT + TCppIndex_t GetTemplateNumArgs(TCppScope_t);; + CPPYY_IMPORT TCppIndex_t GetMethodNumArgs(TCppMethod_t); CPPYY_IMPORT TCppIndex_t GetMethodReqArgs(TCppMethod_t); CPPYY_IMPORT + std::string GetTemplateArgName(TCppMethod_t, TCppIndex_t iarg); + CPPYY_IMPORT std::string GetMethodArgName(TCppMethod_t, TCppIndex_t iarg); CPPYY_IMPORT TCppType_t GetMethodArgType(TCppMethod_t, TCppIndex_t iarg); @@ -292,6 +306,8 @@ namespace Cppyy { CPPYY_IMPORT bool ExistsMethodTemplate(TCppScope_t scope, const std::string& name); CPPYY_IMPORT + bool IsPureTemplatedMethod(TCppMethod_t method); + CPPYY_IMPORT bool IsTemplatedMethod(TCppMethod_t method); CPPYY_IMPORT bool IsStaticTemplate(TCppScope_t scope, const std::string& name); @@ -331,6 +347,8 @@ namespace Cppyy { CPPYY_IMPORT TCppScope_t AdaptFunctionForLambdaReturn(TCppScope_t fn); CPPYY_IMPORT + void GetMemberInNamespace(TCppScope_t ns, std::vector& members); + CPPYY_IMPORT TCppType_t GetDatamemberType(TCppScope_t var); CPPYY_IMPORT std::string GetTypeAsString(TCppType_t type); diff --git a/src/TypeHints.cxx b/src/TypeHints.cxx new file mode 100644 index 0000000..9ee0598 --- /dev/null +++ b/src/TypeHints.cxx @@ -0,0 +1,384 @@ + +#include "CPyCppyy.h" +#include "Cppyy.h" + +#include +#include +#include +#include + +inline std::string replace_all(std::string str, const std::string& from, const std::string& to) { + size_t start_pos = 0; + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // Handles case where 'to' is a substring of 'from' + } + return str; +} + +inline std::string trim(const std::string& line) +{ + if (line.empty()) return ""; + const char* WhiteSpace = " \t\v\r\n"; + std::size_t start = line.find_first_not_of(WhiteSpace); + std::size_t end = line.find_last_not_of(WhiteSpace); + if (start == std::string::npos) return ""; + return line.substr(start, end - start + 1); +} + +inline std::string indentation(int indent = 0) { + std::ostringstream ostream_indentation; + for (size_t i = 0; i < indent * 4; i++) + ostream_indentation << " "; + return ostream_indentation.str(); +} + +std::string pythonize_method_name(std::string &method_name, std::string &class_name) { + if (method_name == class_name) + return "__init__"; + if (method_name == ("~" + class_name)) + return "__del__"; + if (method_name == "operator=") + return ""; + if (method_name == "operator+") + return "__add__"; + if (method_name == "operator-") + return "__sub__"; + if (method_name == "operator*") + return "__mul__"; + if (method_name == "operator/") + return "__div__"; + if (method_name == "operator[]") + return "__getitem__"; + if (method_name == "operator()") + return "__call__"; + return method_name; +} +std::string pythonize_type_name(std::string cpptype) { + // TODO: handle complex numerical type types + // TODO: change this to Pythonize and replace `<> -> []` && `:: -> .` + if (cpptype == "short" || + cpptype == "unsigned short" || + cpptype == "signed char" || + cpptype == "unsigned char" || + cpptype == "unsigned int" || + cpptype == "long" || + cpptype == "long long" || + cpptype == "long int" || + cpptype == "long long int" || + cpptype == "unsigned long" || + cpptype == "unsigned long long" || + cpptype == "unsigned long int" || + cpptype == "unsigned long long int" + ) { + return "int"; + } + if (cpptype == "double") + return "float"; + if (cpptype == "char") + return "str"; + if (cpptype == "void") + return "None"; + + cpptype = replace_all(cpptype, "&", ""); + cpptype = replace_all(cpptype, "const", ""); + cpptype = replace_all(cpptype, "*", ""); + cpptype = trim(cpptype); + std::string result = replace_all(cpptype, "<", "["); // FIXME: what if the template agruments are short ... + result = replace_all(result, ">", "]"); + result = replace_all(result, "::", "."); + if (result != cpptype) + return "\"" + result + "\""; + return result; +} + +// TODO: extract docs from C++ code +void handle_scope(Cppyy::TCppScope_t typ, std::ostringstream &file, int indent = 0); +void handle_namespace(Cppyy::TCppScope_t ns, std::ostringstream &file, int indent = 0); +void handle_class(Cppyy::TCppScope_t cls, std::ostringstream &file, int indent = 0); +void handle_templates(Cppyy::TCppScope_t tmpl, std::ostringstream &file, int indent = 0); +void handle_function(Cppyy::TCppScope_t fn, std::ostringstream &file, int indent = 0); +void handle_typedef(Cppyy::TCppScope_t var, std::ostringstream &file, int indent = 0); +void handle_variable(Cppyy::TCppScope_t var, std::ostringstream &file, int indent); + +void handle_variable(Cppyy::TCppScope_t var, std::ostringstream &file, int indent = 0) { + std::string indentation_str = indentation(indent); + + std::string doc = Cppyy::GetDoc(var); + if (!doc.empty()) { + file << indentation_str + << "# " + << replace_all(doc, "\n", "\n# ") + << "\n"; + } + file << indentation_str + << Cppyy::GetFinalName(var) + << ": " + << pythonize_type_name(Cppyy::GetTypeAsString(Cppyy::GetDatamemberType(var))) + << "\n"; +} + +void handle_typedef(Cppyy::TCppScope_t var, std::ostringstream &file, int indent) { + std::string indentation_str = indentation(indent); + + std::string doc = Cppyy::GetDoc(var); + if (!doc.empty()) { + file << indentation_str + << "# " + << replace_all(doc, "\n", "\n# ") + << "\n"; + } + + file << indentation_str + << Cppyy::GetMethodName(var) + << " = " + << pythonize_type_name(Cppyy::GetFinalName(var)) + << "\n"; +} + +void handle_function(Cppyy::TCppScope_t fn, std::ostringstream &file, int indent) { + static int A = 0; + std::string indentation_str = indentation(indent); + + std::string func_name = Cppyy::GetFinalName(fn); + std::string class_name; + if (Cppyy::IsMethod(fn)) { + class_name = Cppyy::GetMethodName(Cppyy::GetParentScope(fn)); + func_name = pythonize_method_name(func_name, class_name); + } + if (func_name.empty()) + return; // this is a overload that cannot be represented in python + + file << indentation_str + << "@overload\n"; + if (Cppyy::IsStaticMethod(fn)) { + file << indentation_str + << "@staticmethod\n"; + } + file << indentation_str + << "def " + << func_name + << "("; + bool prepend_comma = false; + if (Cppyy::IsMethod(fn) && !Cppyy::IsStaticMethod(fn)) { + // add the implicit self argument + file << "self: Self"; + prepend_comma = true; + } + for (size_t i = 0, nArgs = Cppyy::GetMethodNumArgs(fn); i < nArgs; i++) { + if (prepend_comma) { + file << ", "; + prepend_comma = false; + } + std::string arg_name = Cppyy::GetMethodArgName(fn, i); + file << (arg_name.empty() ? ("_" + std::to_string(++A)) : arg_name) + << ": " + << pythonize_type_name(Cppyy::GetMethodArgTypeAsString(fn, i)); + std::string default_value = Cppyy::GetMethodArgDefault(fn, i); // FIXME: Pythonize this value + if (!default_value.empty()) + file << " = " << default_value; + if (i != nArgs - 1) + file << ", "; + } + file << ") -> " + << pythonize_type_name(Cppyy::GetMethodReturnTypeAsString(fn)) + << ":\n"; + std::string doc = Cppyy::GetDoc(fn); + if (!doc.empty()) { + file << indentation_str + << " \"\"\"" + << replace_all(doc, "\"\"\"", "\\\"\\\"\\\"") // replace """ -> \"\"\" + << "\"\"\"\n"; + } + file << indentation_str + << " ...\n"; +} + +void handle_templates(Cppyy::TCppScope_t tmpl, std::ostringstream &file, int indent) { + static int A = 0; + std::string indentation_str = indentation(indent); + + // TODO: handle parameter pack + // TODO: handle defaults + + if (Cppyy::IsPureTemplatedMethod(tmpl)) { + std::string func_name = Cppyy::GetFinalName(tmpl); + std::string class_name; + Cppyy::TCppScope_t parent = Cppyy::GetParentScope(tmpl); + bool is_method = false; + if (Cppyy::IsClass(parent)) { + class_name = Cppyy::GetMethodName(parent); + func_name = pythonize_method_name(func_name, class_name); + is_method = true; + } + if (func_name.empty()) + return; // this is a overload that cannot be represented in python + file << indentation_str + << "@overload\n"; + if (Cppyy::IsStaticMethod(tmpl)) { + file << indentation_str + << "@staticmethod\n"; + } + file << indentation_str + << "def " + << func_name + << "["; + // write template args + for (size_t i = 0, nArgs = Cppyy::GetTemplateNumArgs(tmpl); i < nArgs; i++) { + if (i != 0) + file << ", "; + std::string arg_name = Cppyy::GetTemplateArgName(tmpl, i); + file << (arg_name.empty() ? ("_" + std::to_string(++A)) : arg_name); + } + file << "]("; + bool prepend_comma = false; + if (is_method && !Cppyy::IsStaticMethod(tmpl)) { + // add the implicit self argument + file << "self: Self"; // ??? + prepend_comma = true; + } + for (size_t i = 0, nArgs = Cppyy::GetMethodNumArgs(tmpl); i < nArgs; i++) { + if (prepend_comma) { + file << ", "; + prepend_comma = false; + } + std::string arg_name = Cppyy::GetMethodArgName(tmpl, i); + file << (arg_name.empty() ? ("_" + std::to_string(++A)) : arg_name) + << ": " + << pythonize_type_name(Cppyy::GetMethodArgTypeAsString(tmpl, i)); + std::string default_value = Cppyy::GetMethodArgDefault(tmpl, i); // FIXME: Pythonize this value + if (!default_value.empty()) + file << " = " << default_value; + if (i != nArgs - 1) + file << ", "; + } + file << ") -> " + << pythonize_type_name(Cppyy::GetMethodReturnTypeAsString(tmpl)) + << ":\n"; + std::string doc = Cppyy::GetDoc(tmpl); + if (!doc.empty()) { + file << indentation_str + << " \"\"\"" + << replace_all(doc, "\"\"\"", "\\\"\\\"\\\"") // replace """ -> \"\"\" + << "\"\"\"\n"; + } + file << indentation_str + << " ...\n"; + } else { + // is templated class ??? + } +} + +void handle_class(Cppyy::TCppScope_t cls, std::ostringstream &file, int indent) { + static size_t A = 0; + std::string indentation_str = indentation(indent); + + bool is_templated = Cppyy::IsTemplateClass(cls); + // TODO: handle inheritance + file << indentation_str + << "class " + << Cppyy::GetMethodName(cls); + if (is_templated) { + file << "["; + for (size_t i = 0, nArgs = Cppyy::GetTemplateNumArgs(cls); i < nArgs; i++) { + if (i != 0) + file << ", "; + std::string arg_name = Cppyy::GetTemplateArgName(cls, i); + file << (arg_name.empty() ? ("_" + std::to_string(++A)) : arg_name); + } + file << "]"; + } + file << ":\n"; + std::string doc = Cppyy::GetDoc(cls); + if (!doc.empty()) { + file << indentation_str + << " \"\"\"" + << replace_all(doc, "\"\"\"", "\\\"\\\"\\\"") // replace """ -> \"\"\" + << "\"\"\"\n\n"; + } + + std::vector members; + Cppyy::GetDatamembers(cls, members); + for (Cppyy::TCppScope_t datamember: members) + handle_variable(datamember, file, indent + 1); + + members.clear(); + Cppyy::GetClassMethods(cls, members); + file << "\n"; + for (Cppyy::TCppScope_t datamember: members) { + file << "\n"; + handle_function(datamember, file, indent + 1); + } + + members.clear(); + Cppyy::GetTemplatedMethods(cls, members); + file << "\n"; + for (Cppyy::TCppScope_t datamember: members) { + file << "\n"; + handle_templates(datamember, file, indent + 1); + } + file << "\n\n"; + + // FIXME: typedefs, enums, & nexted classes missing +} + +void handle_namespace(Cppyy::TCppScope_t ns, std::ostringstream &file, int indent) { + std::string indentation_str = indentation(indent); + std::vector members; + Cppyy::GetMemberInNamespace(ns, members); + file << "class " << Cppyy::GetMethodName(ns) << ":\n"; + std::string doc = Cppyy::GetDoc(ns); + if (!doc.empty()) { + file << indentation_str + << " \"\"\"" + << replace_all(doc, "\"\"\"", "\\\"\\\"\\\"") // replace """ -> \"\"\" + << "\"\"\"\n\n"; + } + for (Cppyy::TCppScope_t s: members) + handle_scope(s, file, indent + 1); +} + +void handle_scope(Cppyy::TCppScope_t typ, std::ostringstream &file, int indent) { + if (Cppyy::IsNamespace(typ)) { + handle_namespace(typ, file, indent); + } + if (Cppyy::IsClass(typ) || Cppyy::IsTemplateClass(typ)) { + handle_class(typ, file, indent); + } + if (Cppyy::IsTypedefed(typ)) { + handle_typedef(typ, file, indent); + } + if (Cppyy::IsFunction(typ) && !Cppyy::IsMethod(typ) && !Cppyy::IsPureNamespace(Cppyy::GetParentScope(typ))) { + handle_function(typ, file, indent); + } + if (Cppyy::IsVariable(typ) && !Cppyy::IsPureNamespace(Cppyy::GetParentScope(typ)) && !Cppyy::IsClass(Cppyy::GetParentScope(typ))) { + handle_variable(typ, file, indent); + } +} + +PyObject *generate_typehints(PyObject *, PyObject *args) { + const char *type_name = nullptr; + const char *file_name = nullptr; + if (!PyArg_ParseTuple(args, "s|s", &type_name, &file_name)) + return nullptr; + Cppyy::TCppScope_t typ = Cppyy::GetScope(type_name); + if (!typ) + typ = Cppyy::GetNamed(type_name); + if (!typ) + return PyErr_Format(PyExc_TypeError, "Unknown Type. Try Parent."); + + std::ostringstream code; + handle_scope(typ, code); + code << "\n"; + + std::string typehint = code.str(); + if (typehint.empty()) + return PyErr_Format(PyExc_TypeError, "Cannot generate typehints for the given type <%s>. Try Parent.", Cppyy::GetScopedFinalName(typ).c_str()); + + if (!file_name) + return PyUnicode_FromString(typehint.c_str()); + + std::fstream file(file_name, std::ios::out | std::ios::ate); // append + file << typehint; + Py_RETURN_NONE; +} diff --git a/src/TypeHints.h b/src/TypeHints.h new file mode 100644 index 0000000..9d50d65 --- /dev/null +++ b/src/TypeHints.h @@ -0,0 +1,8 @@ +#ifndef CPYCPPYY_TYPEHINTS_H +#define CPYCPPYY_TYPEHINTS_H + +#include "Python.h" + +PyObject *generate_typehints(PyObject *, PyObject *args); + +#endif // CPYCPPYY_TYPEHINTS_H